<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Sat, 26 Sep 2026 02:36:04 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=maliming" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/vector-and-hybrid-search-with-sql-server-in-ef-core-11-8s06bcm7</guid>
      <link>https://abp.io/community/posts/vector-and-hybrid-search-with-sql-server-in-ef-core-11-8s06bcm7</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>sql</category>
      <category>EfCore</category>
      <category>entity-framework-core</category>
      <category>dotnet</category>
      <category>new-features</category>
      <title>Vector and Hybrid Search with SQL Server in EF Core 11</title>
      <description>SQL Server 2025 added a vector type, distance functions, and DiskANN vector indexes. EF Core 10 made the storage side usable from .NET. EF Core 11 adds the remaining query-side building blocks: approximate search over a vector index, full-text search table-valued functions, and translation for .NET 11's new FullJoin operator.

This article follows that help center from the first embedding to a working hybrid search, adding each feature at the point where the previous step stops being enough.</description>
      <pubDate>Fri, 18 Sep 2026 06:14:26 Z</pubDate>
      <a10:updated>2026-09-26T01:03:07Z</a10:updated>
      <content:encoded><![CDATA[<h1>Vector and Hybrid Search with SQL Server in EF Core 11</h1>
<blockquote>
<p>Requires .NET 11 and EF Core 11. Verified against EF Core <code>11.0.0-rc.1.26425.128</code>.</p>
</blockquote>
<p>A help center has a few thousand articles in SQL Server. Users type what went wrong in their own words — &quot;login keeps timing out&quot; — and the search returns nothing, because the article that answers them is titled &quot;session expires too quickly&quot; and shares no words with the query.</p>
<p>The usual fix is to put a vector database next to the application: pick one, sync the rows into it, keep the two stores consistent, and accept a second system to operate, back up, and secure. That is a lot of infrastructure for one feature, and the articles are already sitting in SQL Server.</p>
<p>SQL Server 2025 added a <code>vector</code> type, distance functions, and DiskANN vector indexes. EF Core 10 made the storage side usable from .NET. EF Core 11 adds the remaining query-side building blocks: approximate search over a vector index, full-text search table-valued functions, and translation for .NET 11's new <code>FullJoin</code> operator.</p>
<p>This article follows that help center from the first embedding to a working hybrid search, adding each feature at the point where the previous step stops being enough.</p>
<h2>Storing Embeddings Next to the Articles</h2>
<p>Add a <code>SqlVector&lt;float&gt;</code> property and specify how many dimensions the column holds. The dimension count is mandatory; leaving it out fails model validation.</p>
<pre><code class="language-csharp">public class Article
{
    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Content { get; set; } = null!;

    [Column(TypeName = &quot;vector(1536)&quot;)]
    public SqlVector&lt;float&gt; Embedding { get; set; }
}
</code></pre>
<p>Generating the embedding happens outside the database. <a href="https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai"><code>Microsoft.Extensions.AI</code></a> provides a provider-agnostic <code>IEmbeddingGenerator&lt;string, Embedding&lt;float&gt;&gt;</code>, so which model produces the vector stays a configuration decision:</p>
<pre><code class="language-csharp">IEmbeddingGenerator&lt;string, Embedding&lt;float&gt;&gt; embeddingGenerator = /* your provider */;

article.Embedding = new SqlVector&lt;float&gt;(
    await embeddingGenerator.GenerateVectorAsync(article.Content));

await context.SaveChangesAsync();
</code></pre>
<p>The user's query goes through the same generator. That is not optional: distances are only meaningful between vectors from the same model, and a different dimension count will not fit the <code>vector(1536)</code> column at all.</p>
<p>Keeping the vectors in the same database removes the sync problem between two stores, but the embedding is still derived data with a lifecycle. Regenerate it whenever the source text changes, or the search keeps matching the old wording. Version the model and dimension count, because switching models means backfilling every row. And long articles usually need chunking rather than one vector for the whole <code>Content</code>.</p>
<pre><code class="language-csharp">var query = new SqlVector&lt;float&gt;(
    await embeddingGenerator.GenerateVectorAsync(queryText));
</code></pre>
<h2>Searching by Meaning</h2>
<p>With embeddings stored, the first working version of the search is an <code>OrderBy</code> on distance:</p>
<pre><code class="language-csharp">var hits = await context.Articles
    .OrderBy(a =&gt; EF.Functions.VectorDistance(&quot;cosine&quot;, a.Embedding, query))
    .Select(a =&gt; new { a.Id, a.Title })
    .Take(5)
    .ToListAsync();
</code></pre>
<pre><code class="language-sql">SELECT TOP(@p) [a].[Id], [a].[Title]
FROM [Articles] AS [a]
ORDER BY VECTOR_DISTANCE('cosine', [a].[Embedding], @query)
</code></pre>
<p>This is exact k-nearest-neighbor search. Every row is compared, so it returns the true nearest neighbors for the chosen metric. &quot;login keeps timing out&quot; now finds &quot;session expires too quickly&quot;, and the help center ships.</p>
<p>The supported metrics are <code>cosine</code>, <code>euclidean</code>, and <code>dot</code>. Cosine is common for text embeddings, but use whichever the model's documentation recommends, and note whether it expects normalized vectors.</p>
<p>This is also where a lot of applications stop. Exact search stays reasonable up to roughly 50,000 vectors — or any table size, as long as a <code>WHERE</code> clause narrows the candidates first — and it is all stable EF Core 10 API.</p>
<h2>When the Scan Becomes the Bottleneck</h2>
<p>The help center grows. Every search now computes a distance for every row, and the scan shows up in traces.</p>
<p>Vector indexes exist to avoid that. SQL Server uses <a href="https://www.microsoft.com/en-us/research/publication/diskann-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node">DiskANN</a>, a graph index built for vectors that do not fit in memory, and a search walks a small part of the graph instead of the whole table.</p>
<p>The trade-off is in the name: <strong>approximate</strong> nearest neighbor search can miss a true neighbor. That is usually acceptable for ranking, where a different item somewhere in the top-k changes nothing a user would notice — but it is worth measuring recall on your own corpus rather than assuming it.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-16-vector-search-and-hybrid-search-in-ef-core-11/images/exact-vs-approximate.png" alt="Exact versus approximate search — conceptual example" /></p>
<p>Configure the index in the model:</p>
<pre><code class="language-csharp">modelBuilder.Entity&lt;Article&gt;()
    .HasVectorIndex(a =&gt; a.Embedding)
    .HasMetric(&quot;cosine&quot;)
    .HasType(&quot;DiskANN&quot;);
</code></pre>
<p><code>HasMetric</code> is required, and the metric configured here must match the one passed at query time or the index is not used. A vector index also covers exactly one column.</p>
<p>Note that the second argument to <code>HasVectorIndex</code> is the index name, not the metric — the <a href="https://learn.microsoft.com/en-us/ef/core/providers/sql-server/vector-search#vector-indexes">documentation</a> currently passes <code>&quot;cosine&quot;</code> there. Written that way the index is called <code>&quot;cosine&quot;</code>, no metric is set, and model validation fails.</p>
<h3>Getting the Index Created at All</h3>
<p>Deploying that index is harder than it looks. A vector index cannot be created on a table with fewer than 100 non-null vectors, and creating it too early fails with <code>Msg 42266</code>.</p>
<p><code>UseSeeding</code> runs after the whole migration batch, so you cannot rely on it to prepare data for a later migration in the same update. Split the work into explicit ordered steps instead: create the table, write at least 100 non-null vectors with a data migration, then create the index in the migration after that.</p>
<p>Three more constraints are worth knowing before you design the deployment. The table must have a clustered primary key — and SQL Server 2025 is stricter than the limitations page reads: error 42217 asks for that key to be <em>a single 4 byte INT column</em>. Azure SQL Database, which carries the newer index version, accepted <code>bigint</code> and <code>uniqueidentifier</code> keys when I tried it, so this one is worth testing on your own target rather than assuming either way. <code>TRUNCATE TABLE</code> is blocked while a vector index exists. And vector indexes cannot be deployed through DacPac or BACPAC, because the import creates schema before loading data and therefore hits the 100-row rule — drop the index before exporting and recreate it after the import.</p>
<h3>Querying the Index</h3>
<pre><code class="language-csharp">var hits = await context.Articles
    .VectorSearch(a =&gt; a.Embedding, query, &quot;cosine&quot;)
    .OrderBy(r =&gt; r.Distance)
    .Take(5)
    .WithApproximate()
    .ToListAsync();
</code></pre>
<pre><code class="language-sql">SELECT TOP(@p1) WITH APPROXIMATE [a].[Id], [a].[Content], [a].[Title], [v].[Distance]
FROM VECTOR_SEARCH(
    TABLE = [Articles] AS [a],
    COLUMN = [Embedding],
    SIMILAR_TO = @p,
    METRIC = 'cosine'
) AS [v]
ORDER BY [v].[Distance]
</code></pre>
<p><code>VectorSearch()</code> returns <code>IQueryable&lt;VectorSearchResult&lt;T&gt;&gt;</code>, which carries both the entity (<code>Value</code>) and the computed <code>Distance</code>. <code>WithApproximate()</code> must come after <code>Take()</code> — it is what turns <code>TOP(n)</code> into <code>TOP(n) WITH APPROXIMATE</code>. On the latest index version, a <code>Where()</code> placed before <code>Take()</code> is applied inside the search, so filters narrow the candidate set rather than trimming results afterwards.</p>
<p>The failure mode to guard against is forgetting <code>WithApproximate()</code>. On a platform that accepts the generated syntax at all — see the next section — the query still compiles, still runs, and still returns correct results, by scanning every row with the index unused. EF only warns in the log, so turn that warning into an error while developing:</p>
<pre><code class="language-csharp">options.UseSqlServer(connectionString)
    .ConfigureWarnings(w =&gt; w.Throw(SqlServerEventId.VectorSearchWithoutApproximateIndexWarning));
</code></pre>
<h2>Where This Actually Runs</h2>
<p>Before committing to that path, check that it runs on your target at all. The new approximate-search APIs — <code>VectorSearch()</code>, <code>WithApproximate()</code> and the vector index builder — carry <code>[Experimental(&quot;EF9105&quot;)]</code>, so your code does not compile until the diagnostic is acknowledged with <code>&lt;NoWarn&gt;$(NoWarn);EF9105&lt;/NoWarn&gt;</code>. <code>VectorDistance()</code>, <code>FreeTextTable()</code>, <code>ContainsTable()</code> and the full-text modelling APIs are not marked.</p>
<p>The attribute itself only affects compilation. What decides whether a query runs is the server product, its region, and the version of the vector index.</p>
<p>| Capability | SQL Server 2025 container <code>17.0.5005.3</code> | Azure SQL Database |
|---|---|---|
| <code>vector</code> column, <code>SqlVector&lt;float&gt;</code> | ✅ | ✅ |
| <code>EF.Functions.VectorDistance()</code> | ✅ | ✅ |
| <code>CREATE VECTOR INDEX</code> from a migration | ✅ | ✅ |
| <strong><code>VectorSearch()</code> + <code>WithApproximate()</code></strong> | ❌ | ✅ |
| Full-text search | ❌ not installed in the image | ✅ |
| <code>PREVIEW_FEATURES</code> required for indexes and <code>VECTOR_SEARCH</code> | ✅ | ❌ |</p>
<p>The SQL Server 2025 column reports one tested build, the stock <code>mcr.microsoft.com/mssql/server:2025-latest</code> image. Full-text search is missing because that image does not ship the component, not because SQL Server 2025 lacks it.</p>
<p>On SQL Server 2025, <code>CREATE VECTOR INDEX</code> and <code>VECTOR_SEARCH</code> are documented as preview features that need the database scoped configuration turned on first:</p>
<pre><code class="language-sql">ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON;
</code></pre>
<p>Azure SQL Database does not require it.</p>
<p>Even with that enabled, the generated query is rejected on the tested <code>17.0.5005.3</code> build with <code>Incorrect syntax near 'APPROXIMATE'</code>. Dropping <code>WithApproximate()</code> gives a more informative failure, <code>Incorrect syntax near 'TOP_N'</code>, even though the generated SQL contains no <code>TOP_N</code> anywhere.</p>
<p>That build requires <code>TOP_N</code> inside the function call, which is the older calling convention:</p>
<pre><code class="language-sql">SELECT TOP(3) t.Id, s.distance
FROM VECTOR_SEARCH(TABLE = Articles AS t, COLUMN = Embedding, SIMILAR_TO = @qv,
                   METRIC = 'cosine', TOP_N = 3) AS s
ORDER BY s.distance;
</code></pre>
<p>That form runs. The server supports approximate search; it speaks the older dialect, and EF generates only the newer one, with no compatibility switch. On Azure SQL Database — and on SQL database in Microsoft Fabric, which also carries the latest index version — the EF query runs unchanged.</p>
<p>The two index versions differ in behaviour, not only in syntax. On the older version the table becomes read-only once the index exists. <code>ALLOW_STALE_VECTOR_INDEX</code> lifts that on Azure SQL Database and SQL database in Microsoft Fabric, but that database scoped configuration does not exist on SQL Server 2025, so on the build tested here there is no way back to a writable table. <code>WHERE</code> predicates are also applied after retrieval rather than during it, so <code>TOP_N</code> has to be widened by hand to survive filtering. The iterative filtering described above belongs to the latest index version.</p>
<p>Two more environment notes. As of September 2026, Azure SQL Database vector search is live in North Europe and UK South only, so check region availability before planning around it. And <code>SERVERPROPERTY('IsFullTextInstalled')</code> returns 0 on <code>mcr.microsoft.com/mssql/server:2025-latest</code>, so full-text search needs a derived image that installs <code>mssql-server-fts</code>.</p>
<h2>The Embeddings Come Back Empty</h2>
<p>A bug report arrives: an admin edits an article's title, saves, and the search stops finding it — or a background job that re-reads embeddings gets nothing.</p>
<p>EF Core 11 excludes vector columns from the <code>SELECT</code> list by default when materializing entities:</p>
<pre><code class="language-sql">SELECT TOP(@p) [a].[Id], [a].[Content], [a].[Title]
FROM [Articles] AS [a]
ORDER BY [a].[Id]
</code></pre>
<p>There is no <code>[Embedding]</code>, and the property comes back with a length of zero even though every row has a value.</p>
<p>The reasoning is sound. A 1,536-dimensional float vector is 6 KB per row, and embeddings are written rarely and searched against constantly but almost never read back. In a minimal benchmark, Microsoft measured roughly a 9x throughput improvement locally from not shipping them over the wire, and 22x against a remote Azure SQL database.</p>
<p>Vectors still work in <code>WHERE</code> and <code>ORDER BY</code>. Reading one requires an explicit projection:</p>
<pre><code class="language-csharp">var embeddings = await context.Articles
    .Select(a =&gt; new { a.Id, a.Embedding })
    .ToListAsync();
</code></pre>
<p>So the title edit is safe: saving a tracked entity leaves the stored embedding untouched. That is correct here because the vector is generated from <code>Content</code> alone. The sample that ships with this article embeds the title as well, and there the same edit does leave a stale vector behind — which input feeds the embedding decides whether an edit invalidates it. What breaks is code that <em>reads</em> <code>Embedding</code> from a tracked entity: it gets an empty vector, and ordinary property access returns it without any warning. Check <code>context.Entry(article).Property(a =&gt; a.Embedding).IsLoaded</code> when it matters, or project the property explicitly.</p>
<h2>Exact Terms Still Fail</h2>
<p>The next complaint is different. Users searching for an error number, a plan name, or a specific setting get articles that are thematically close and factually wrong. Vector search finds meaning and misses precision — that is what it is for.</p>
<p>EF Core has translated <code>EF.Functions.FreeText()</code> and <code>EF.Functions.Contains()</code> for years, but those are predicates: they filter without ranking, which is no help when you need a merged ordering later. EF Core 11 adds the table-valued versions, which return SQL Server's relevance score:</p>
<pre><code class="language-csharp">var hits = await context.Articles
    .FreeTextTable&lt;Article, int&gt;(&quot;session expires too quickly&quot;, topN: 20)
    .Join(context.Articles, fts =&gt; fts.Key, a =&gt; a.Id, (fts, a) =&gt; new { a.Title, fts.Rank })
    .OrderByDescending(x =&gt; x.Rank)
    .ToListAsync();
</code></pre>
<p><code>FullTextSearchResult&lt;TKey&gt;</code> exposes only <code>Key</code> and <code>Rank</code>, so the join back to the table is explicit, and both generic type arguments must be supplied explicitly because <code>TKey</code> cannot be inferred. <code>ContainsTable()</code> has the same shape but takes a search condition (<code>&quot;nebula OR quasar&quot;</code>, <code>NEAR</code>, prefix terms) instead of free text.</p>
<p>The catalog and index can be configured in the model as of EF Core 11:</p>
<pre><code class="language-csharp">modelBuilder.HasFullTextCatalog(&quot;ftCatalog&quot;);

modelBuilder.Entity&lt;Article&gt;()
    .HasFullTextIndex(a =&gt; new { a.Title, a.Content })
    .UseKeyIndex(&quot;PK_Articles&quot;)
    .UseCatalog(&quot;ftCatalog&quot;);
</code></pre>
<p><code>UseKeyIndex</code> has to name a unique, single-column, non-nullable index — SQL Server uses it as the full-text key, which is what <code>FullTextSearchResult&lt;TKey&gt;.Key</code> returns.</p>
<p>That configuration has a side effect in the table definition rather than the index. <code>HasFullTextIndex</code> is built on <code>HasIndex</code>, so EF applies its ordinary index key size limit to the string properties involved, and a <code>Content</code> property that should map to <code>nvarchar(max)</code> comes out as <code>nvarchar(450)</code> — truncating the column you most wanted to search. Set the column type explicitly to keep the full length:</p>
<pre><code class="language-csharp">modelBuilder.Entity&lt;Article&gt;()
    .Property(a =&gt; a.Content)
    .HasColumnType(&quot;nvarchar(max)&quot;);
</code></pre>
<p>One more thing that looks like a bug in tests: full-text population is asynchronous. In a 120-row sample it took about 20 seconds to become searchable after <code>CREATE FULLTEXT INDEX</code>, so a seed routine that queries immediately gets an empty result. Wait until the indexed row count catches up:</p>
<pre><code class="language-sql">SELECT OBJECTPROPERTYEX(OBJECT_ID('Articles'), 'TableFulltextItemCount');
</code></pre>
<p>Give that loop an interval and a timeout, and check <code>TableFulltextFailCount</code> as well — rows that fail indexing never arrive, so waiting for the count to reach the table total can hang forever. The catalog-level <code>FULLTEXTCATALOGPROPERTY('ftCatalog', 'PopulateStatus')</code> is the more obvious check, but Microsoft has marked that property for removal and advises against polling it in a tight loop.</p>
<h2>Merging the Two Rankings</h2>
<p>Now the help center has two searches that each fail differently, and neither one alone is the product. They have to be merged.</p>
<p>The standard merge is Reciprocal Rank Fusion: each document scores <code>1 / (k + position)</code> for every list it appears in, where <code>k</code> is a smoothing constant, and the scores are summed. With suitable parameters, a document that both retrievers rank moderately well can beat one that a single retriever ranks first.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-16-vector-search-and-hybrid-search-in-ef-core-11/images/hybrid-search.png" alt="Hybrid search with reciprocal rank fusion" /></p>
<p>Evaluating a fusion needs a corpus where the two retrievers disagree. The one used here has 120 documents, a query text of <code>nebula quasar</code>, and an astronomy query vector: 12 documents match on both sides, 24 are semantically on topic but share no query words, 12 are lexical false friends — dessert menu items named <em>Nebula</em> and <em>Quasar</em>, embedded in a cooking topic — and 72 are unrelated. That corpus, and the console probe that produced every measurement below, are in the sample repository: <a href="https://github.com/maliming/efcore11-vector-hybrid-search">maliming/efcore11-vector-hybrid-search</a>.</p>
<p>Each retriever on its own:</p>
<pre><code class="language-text">FreeTextTable    Deep sky survey 4 (484), 9 (447), 6 (269), 8 (269), 2 (189), …
VectorSearch     Observation log 26 (0.0112), 34 (0.0125), 18 (0.0134), 32 (0.0145), …
</code></pre>
<p>Both lists lead with one group each: full-text with documents the corpus makes match on both sides, vector search with the semantic-only ones. Neither puts a doubly-corroborated document first.</p>
<h3>The Documented Single-Query Shape</h3>
<p>The documentation builds the fusion as a single query, joining the two retrievers with .NET 11's <code>FullJoin</code> and computing the score in the projection. As observed on <code>11.0.0-rc.1.26425.128</code>, that shape has three practical problems. Re-check the first two against GA before building on them.</p>
<p>The first stops the build. The sample compares <code>vs == null</code>, but <code>VectorSearchResult&lt;T&gt;</code> is a <code>readonly struct</code>, so that is <code>CS0019</code>. Projecting the vector side to a reference type first — <code>.Select(r =&gt; new { Article = r.Value, r.Distance })</code> — fixes it.</p>
<p>The second is materialization. Once the fused score is computed in the projection, reading back a result set that contains rows matched on one side only throws <code>InvalidOperationException: Nullable object must have a value</code>.</p>
<p>Projecting the same entity and nullable scalars without that score expression works, so it is the fused projection that breaks rather than the full join itself.</p>
<p>The third is the score, and it is wrong in two separate ways.</p>
<p>It inverts the full-text ranking. SQL Server's <code>RANK</code> is higher-is-better, so feeding it into <code>1 / (k + Rank)</code> gives the <em>least</em> relevant keyword matches the highest score. In the measured run, a document with <code>RANK</code> 119 scored above one with <code>RANK</code> 140.</p>
<p>It also turns vector membership into a flat bonus. Cosine distances sit near zero, so with the documented <code>k</code> of 20 the vector term is about <code>1 / 20 = 0.05</code> for anything in the vector list, while the entire full-text term spans roughly 0.002 to 0.007 across the ranks in this run. Simply appearing in the vector results is worth seven to twenty-five times more than any difference in keyword relevance.</p>
<p>Neither side uses rank positions, which is what RRF is defined over.</p>
<h3>Fusing on Rank Positions</h3>
<p>Fetching the two ranked lists separately and applying RRF in memory avoids all three:</p>
<pre><code class="language-csharp">const int candidateCount = 20;
const int rrfK = 60; // the constant from the original RRF paper

var lexical = await context.Articles
    .FreeTextTable&lt;Article, int&gt;(queryText, topN: candidateCount)
    .Join(context.Articles, fts =&gt; fts.Key, a =&gt; a.Id, (fts, a) =&gt; new { a.Id, a.Title, fts.Rank })
    .OrderByDescending(x =&gt; x.Rank)
    .ThenBy(x =&gt; x.Id)
    .ToListAsync();

var semantic = await context.Articles
    .OrderBy(a =&gt; EF.Functions.VectorDistance(&quot;cosine&quot;, a.Embedding, queryVector))
    .ThenBy(a =&gt; a.Id)
    .Select(a =&gt; new { a.Id, a.Title })
    .Take(candidateCount)
    .ToListAsync();

var lexicalPosition = lexical.Select((x, i) =&gt; (x.Id, Position: i + 1)).ToDictionary(x =&gt; x.Id, x =&gt; x.Position);
var semanticPosition = semantic.Select((x, i) =&gt; (x.Id, Position: i + 1)).ToDictionary(x =&gt; x.Id, x =&gt; x.Position);

var titles = lexical.Select(x =&gt; (x.Id, x.Title))
    .Concat(semantic.Select(x =&gt; (x.Id, x.Title)))
    .DistinctBy(x =&gt; x.Id)
    .ToDictionary(x =&gt; x.Id, x =&gt; x.Title);

var fused = titles.Keys
    .Select(id =&gt; new
    {
        Id = id,
        Title = titles[id],
        LexicalPosition = lexicalPosition.TryGetValue(id, out var l) ? l : (int?)null,
        SemanticPosition = semanticPosition.TryGetValue(id, out var s) ? s : (int?)null,
        Score = (lexicalPosition.TryGetValue(id, out var lp) ? 1.0 / (rrfK + lp) : 0.0)
              + (semanticPosition.TryGetValue(id, out var sp) ? 1.0 / (rrfK + sp) : 0.0)
    })
    .OrderByDescending(x =&gt; x.Score)
    .ThenBy(x =&gt; x.Id)
    .Take(10)
    .ToList();
</code></pre>
<p>The <code>ThenBy</code> calls matter at both levels. Full-text ranks tie often — two documents above share <code>RANK</code> 269 — and the positions those lists produce are what the score is built from, so an unstable input order moves the final score. Ties in the score itself are just as common: two pairs below land on exactly <code>0.01639</code> and <code>0.01613</code>.</p>
<p>The semantic list here comes from <code>VectorDistance()</code>, which keeps the whole pipeline on stable API and runs on any SQL Server 2025 instance. On Azure SQL Database in a supported region, swap that one query for the indexed version and nothing else changes:</p>
<pre><code class="language-csharp">var semantic = await context.Articles
    .VectorSearch(a =&gt; a.Embedding, queryVector, &quot;cosine&quot;)
    .OrderBy(r =&gt; r.Distance)
    .Take(candidateCount)
    .WithApproximate()
    .Select(r =&gt; new { r.Value.Id, r.Value.Title, r.Distance })
    .ToListAsync();

// VECTOR_SEARCH accepts a single ascending ordering on the distance, so the tie-break the
// exact query does in SQL has to happen here instead.
semantic = [.. semantic.OrderBy(x =&gt; x.Distance).ThenBy(x =&gt; x.Id)];
</code></pre>
<p>That last line is not decoration: <code>VECTOR_SEARCH</code> only takes one ordering key, so the approximate path cannot express the tie-break in SQL and has to apply it after the rows come back. Against this corpus both versions produce the same fused ranking, which is what you would hope for at 120 documents — the approximate path earns its keep at a scale where the exact scan hurts, not by changing the answer.</p>
<p>Two round trips instead of one, and the fusion itself is a dictionary lookup over at most <code>2 * candidateCount</code> rows:</p>
<pre><code class="language-text">title                  lexical#  semantic#     score
Deep sky survey 8             4          5   0.03101
Deep sky survey 1             7         12   0.02881
Deep sky survey 10           10          9   0.02878
Deep sky survey 5             9         16   0.02765
Deep sky survey 3             8         19   0.02736
Deep sky survey 4             1          -   0.01639
Observation log 26            -          1   0.01639
Deep sky survey 9             2          -   0.01613
Observation log 34            -          2   0.01613
Deep sky survey 6             3          -   0.01587
</code></pre>
<p><code>Deep sky survey 8</code> is ranked 4th by keywords and 5th by vectors — first by neither — and wins outright because both retrievers agree on it.</p>
<p>That ordering is a parameter choice, not a universal truth. With <code>rrfK = 60</code> and <code>candidateCount = 20</code>, the worst possible two-list score is <code>2 / (60 + 20) = 0.025</code> and the best possible one-list score is <code>1 / (60 + 1) = 0.0164</code>, so anything both retrievers return outranks anything only one of them found. For the help center that pushes keyword-only false positives down — but it would push down a correct error number just as hard, if the vector side happened to miss it. When exact identifiers matter, lower <code>rrfK</code>, widen the candidate lists, or add an explicit boost for exact matches.</p>
<p>The companion web application in that same repository puts all three strategies on one page. It ships the help center corpus from the opening — 32 articles, a separate set from the 120 documents used for the measurements above:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-16-vector-search-and-hybrid-search-in-ef-core-11/images/demo-three-strategies.png" alt="The three retrieval strategies side by side in the sample application" /></p>
<p>The opening query behaves as described. Semantic search returns <em>Session expires too quickly</em> first — a title that shares no words with the query. Keyword search mixes in <em>Sign in sheet export template</em> — a reporting article about attendance sheets — and a billing article about declined cards. The fused list puts four authentication articles on top and drops both false friends below them. <em>Session expires too quickly</em>, which only the vector side found, lands at 8: the trade-off from the paragraph above, on this sample corpus.</p>
<p>Clicking a result expands it:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-16-vector-search-and-hybrid-search-in-ef-core-11/images/demo-article-detail.png" alt="The expanded article panel, showing that the vector did not come back with the entity" /></p>
<p><code>Came back with the entity: no</code> is the earlier section made visible — loading the article never fetched its 1,536 floats, and the values in the panel come from a second query that projects the property explicitly.</p>
<h2>What to Ship</h2>
<p>Following that path end to end, the version worth deploying is smaller than the feature list suggests.</p>
<p>Store embeddings in a <code>vector</code> column and search with <code>VectorDistance()</code>. That API is stable, exact, runs on any SQL Server 2025 instance, and is fast enough for tens of thousands of candidate articles. Add a full-text index when exact terms matter, and fuse the two ranked lists on positions in application code. Both halves are non-experimental, and the fusion works with either vector retriever.</p>
<p>Reach for <code>VectorSearch()</code> and a vector index when profiling shows the scan is actually the bottleneck, and only on Azure SQL Database or SQL database in Microsoft Fabric in a region that supports it. Treat that path as preview on both sides.</p>
<h2>Summary</h2>
<p>EF Core 11 lets you build semantic, lexical, and hybrid search on SQL Server without a separate vector database. Start with exact search, add full-text search when exact terms matter, and use approximate search only when scale justifies its preview constraints. And remember that vector columns are no longer loaded by default when materializing entities.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-11.0/whatsnew">What's New in EF Core 11</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/providers/sql-server/vector-search">Vector search in the SQL Server EF Core Provider</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/providers/sql-server/full-text-search">Full-text search in the SQL Server EF Core Provider</a></li>
<li><a href="https://learn.microsoft.com/en-us/sql/sql-server/ai/vectors">Vector search and vector indexes in the SQL Database Engine</a></li>
<li><a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/vector-search-transact-sql"><code>VECTOR_SEARCH</code> (Transact-SQL)</a></li>
<li><a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-vector-index-transact-sql"><code>CREATE VECTOR INDEX</code> (Transact-SQL)</a></li>
<li><a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/region-availability">Azure SQL Database feature availability by region</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai">Microsoft.Extensions.AI</a></li>
<li><a href="https://github.com/maliming/efcore11-vector-hybrid-search">Sample code for this article</a> — the web application and the measurement probe</li>
<li><a href="https://cormack.uwaterloo.ca/cormacksigir09-rrf.pdf">Reciprocal Rank Fusion outperforms Condorcet and individual rank learning methods</a> — the original RRF paper</li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a23c553-bef3-7854-d80b-cc3ec8528b63" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a23c553-bef3-7854-d80b-cc3ec8528b63" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-10.5.0-expands-blazor-ui-options-with-mudblazor-support-03rzmlpm</guid>
      <link>https://abp.io/community/posts/abp-10.5.0-expands-blazor-ui-options-with-mudblazor-support-03rzmlpm</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>MudBlazor</category>
      <category>Blazorise</category>
      <category>abp-suite</category>
      <category>abp-studio</category>
      <title>ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support</title>
      <description>With ABP 10.5.0, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.5.0 packages are live on nuget.org.</description>
      <pubDate>Wed, 01 Jul 2026 08:25:31 Z</pubDate>
      <a10:updated>2026-09-26T00:25:35Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support</h1>
<p>With ABP 10.5.0, new Blazor projects can now use <strong>MudBlazor</strong> (Material Design) as an alternative to the long-standing default, <strong>Blazorise</strong> (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.5.0 packages are live on nuget.org.</p>
<h2>Why add another Blazor UI library?</h2>
<p>Blazorise has been ABP's default Blazor UI library for years and <strong>remains the default and is fully supported</strong> — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.5.0 does not change anything for them.</p>
<p>We added MudBlazor because one Blazor UI choice cannot fit every team:</p>
<ul>
<li><strong>Design language</strong> — Bootstrap and Material Design serve different audiences, and forcing a single choice does not fit every team</li>
<li><strong>Open-source preference</strong> — MudBlazor is MIT-licensed, which works well for teams that want an open-source frontend component stack without extra component-library licensing or compliance overhead</li>
<li><strong>Ecosystem fit</strong> — Material Design third-party components (charts, rich text editors, data visualization, and so on) tend to integrate more naturally with a MudBlazor project</li>
</ul>
<p>For new projects you can start with MudBlazor right away. Existing Blazorise projects do not need to be rewritten just to switch UI libraries.</p>
<h3>Who should consider MudBlazor?</h3>
<ul>
<li>Teams that want the frontend component stack <strong>fully open source</strong> with no licensing to manage (individual developers, open-source community projects, education / learning settings)</li>
<li>Organizations with internal <strong>third-party dependency or supply-chain compliance</strong> requirements that prefer MIT-licensed components</li>
<li>New Blazor projects that want to start with <strong>Material Design</strong></li>
<li>Teams already comfortable with the <strong>MudBlazor ecosystem</strong> (charts, rich text, rich UI components)</li>
</ul>
<h2>What the MudBlazor option covers</h2>
<h3>Framework core</h3>
<p><code>Volo.Abp.MudBlazorUI</code> provides the MudBlazor implementation of ABP's UI service abstractions, so code written against <code>IUiMessageService</code> / <code>IUiNotificationService</code> / <code>IUiPageProgressService</code> runs unchanged in a MudBlazor project. Key building blocks:</p>
<ul>
<li><code>MudBlazorUiMessageService</code> — <code>Info</code> / <code>Success</code> / <code>Warn</code> / <code>Error</code> / <code>Confirm</code> rendered through <code>MudDialog</code></li>
<li><code>MudBlazorUiNotificationService</code> — toast notifications via <code>MudSnackbar</code></li>
<li><code>MudBlazorUiPageProgressService</code> — top progress bar via <code>MudProgressLinear</code></li>
<li><code>AbpMudCrudPageBase&lt;...&gt;</code> — the MudBlazor counterpart to Blazorise's <code>AbpCrudPageBase</code></li>
<li><code>AbpMudExtensibleDataGrid&lt;TItem&gt;</code> — a <code>MudDataGrid</code> wrapper integrated with Object Extension and time-zone conversion</li>
<li><code>UiMessageAlert</code> / <code>UiNotificationAlert</code> / <code>PageAlert</code> — page-level alert and notification containers</li>
</ul>
<p>Theming is split across three hosts — Blazor Server, WebAssembly, and MauiBlazor — each shipped with matching bundling contributors and modules that wire MudBlazor's JS and CSS into the ABP bundle system.</p>
<h3>Three themes</h3>
<ul>
<li><strong>LeptonX MudBlazor</strong></li>
<li><strong>LeptonX Lite MudBlazor</strong></li>
<li><strong>Basic Theme MudBlazor</strong></li>
</ul>
<p>Each theme's layout adopts MudBlazor components such as <code>MudAppBar</code>, <code>MudDrawer</code>, <code>MudNavLink</code>, and <code>MudMenu</code>, while keeping the theme's original color palette, dim / light / system modes, and RTL support.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-dashboard.png" alt="LeptonX MudBlazor Dashboard" />
<em>LeptonX rendered with MudBlazor</em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png" alt="LeptonX Lite MudBlazor Dashboard" />
<em>LeptonX Lite rendered with MudBlazor</em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png" alt="Basic Theme MudBlazor Dashboard" />
<em>Basic Theme rendered with MudBlazor</em></p>
<p>The LeptonX themes reuse the same <code>lpx-*</code> CSS classes across both UI libraries, so the overall information architecture, page layout, and theme experience stay consistent with the Blazorise version. Individual controls follow each UI library's own conventions.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png" alt="Blazorise vs MudBlazor on the same LeptonX theme" />
<em>The same LeptonX theme — MudBlazor on the left, Blazorise on the right</em></p>
<h3>Module coverage</h3>
<p>Open-source modules in <code>abpframework/abp</code> that ship with a MudBlazor implementation:</p>
<ul>
<li><strong>Account</strong></li>
<li><strong>Identity</strong> — Users / Roles / OUs / ClaimTypes</li>
<li><strong>Permission Management</strong> — parent/child permissions with <code>MudTreeView</code> and tri-state <code>MudCheckBox</code></li>
<li><strong>Setting Management</strong> — grouped settings with <code>MudTabs</code> (including theme switching)</li>
<li><strong>Tenant Management</strong></li>
<li><strong>Feature Management</strong></li>
</ul>
<p>Additional MudBlazor implementations available on the Pro side, for example:</p>
<ul>
<li><strong>Identity Pro</strong> — extra management around Sessions, SecurityLogs, and more</li>
<li><strong>OpenIddict Pro</strong> — Application / Scope management</li>
<li><strong>Saas</strong> — Tenant / Edition management with a connection-string dialog</li>
<li><strong>Audit Logging</strong> — <code>MudDataGrid</code> with a detail <code>MudDialog</code></li>
<li><strong>Language Management</strong> / <strong>Text Template Management</strong></li>
<li><strong>File Management</strong> / <strong>Chat</strong> / <strong>CMS Kit Pro</strong></li>
<li><strong>AI Management</strong> / <strong>GDPR</strong> / <strong>Payment</strong>, and more</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-identity-users.png" alt="Identity user management with MudDataGrid" />
<em>Identity user management built on <code>AbpMudExtensibleDataGrid</code></em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-permission-management.png" alt="Permission management modal" />
<em>Permission Management uses <code>MudTreeView</code> and tri-state <code>MudCheckBox</code> for parent/child permissions</em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-saas-tenants.png" alt="Saas tenants list" />
<em>Saas module: tenant list with a &quot;New tenant&quot; dialog that includes connection-string editing</em></p>
<h3>Component mapping at a glance</h3>
<p>If you already know Blazorise, here are the most common mappings:</p>
<p>| Blazorise | MudBlazor |
|-----------|-----------|
| <code>TextEdit @bind-Text</code> | <code>MudTextField @bind-Value</code> |
| <code>Select / SelectItem</code> | <code>MudSelect / MudSelectItem</code> |
| <code>DataGrid</code> | <code>MudDataGrid</code> (wrapped by ABP as <code>AbpMudExtensibleDataGrid</code>) |
| <code>Modal Show()/Hide()</code> | <code>MudDialog ShowAsync()/CloseAsync()</code> |
| <code>Validations</code> | <code>MudForm</code> + built-in validation |
| <code>Row / Column ColumnSize.Is6</code> | <code>MudGrid / MudItem xs=&quot;12&quot; sm=&quot;6&quot;</code> |
| Bootstrap Icons <code>bi-*</code> | <code>Icons.Material.Filled.*</code> |</p>
<p>A full mapping table with razor examples lives in the <a href="https://abp.io/docs/latest/framework/ui/blazor">ABP Blazor UI documentation</a>.</p>
<h3>Supported Blazor project types</h3>
<p>ABP's MudBlazor support covers the Blazor project types you can create and run directly:</p>
<ul>
<li><strong>Blazor Server</strong> (<code>-u blazor-server</code>)</li>
<li><strong>Blazor WebAssembly</strong> (<code>-u blazor</code>)</li>
<li><strong>Blazor WebApp</strong> (<code>-u blazor-webapp</code>, including InteractiveAuto)</li>
</ul>
<h3>ABP Suite</h3>
<p>ABP Suite detects the solution's UI library and generates the matching CRUD page automatically:</p>
<pre><code class="language-csharp">public partial class Books : AbpMudCrudPageBase&lt;IBookAppService, BookDto, Guid, GetBookListInput, CreateUpdateBookDto&gt;
{
    private MudDialog _createDialog;
    private MudForm _createFormRef;
}
</code></pre>
<p>The razor templates also split by UI library — Blazorise uses <code>&lt;DataGrid&gt;</code> + <code>&lt;Modal&gt;</code> + <code>&lt;Validations&gt;</code>, MudBlazor uses <code>&lt;MudDataGrid&gt;</code> + <code>&lt;MudDialog&gt;</code> + <code>&lt;MudForm&gt;</code>.</p>
<h2>Choosing between Blazorise and MudBlazor</h2>
<p>Both UI libraries are production-ready and neither is strictly better. Common factors:</p>
<ul>
<li><strong>Familiarity</strong> — teams comfortable with Bootstrap tend to stay on Blazorise; teams comfortable with Material Design pick MudBlazor</li>
<li><strong>Design system</strong> — Bootstrap-style products lean toward Blazorise, Material Design products lean toward MudBlazor</li>
<li><strong>Ecosystem</strong> — existing Bootstrap component libraries or design assets fit Blazorise; Material Design third-party components fit MudBlazor more naturally</li>
<li><strong>Existing projects</strong> — keep maintaining live Blazorise projects as they are; if you want to try MudBlazor, start a new project with it</li>
<li><strong>Licensing</strong> — the two UI libraries have different license terms, so check each library's official license page before making a choice (<a href="https://blazorise.com/license">Blazorise</a> / <a href="https://github.com/MudBlazor/MudBlazor/blob/dev/LICENSE">MudBlazor</a>)</li>
</ul>
<p>Do not mix the two libraries within a single project — the choice is per solution, not per file.</p>
<h2>Creating a MudBlazor project in ABP Studio</h2>
<h3>ABP Studio (recommended)</h3>
<p>Open ABP Studio → <strong>New Solution</strong> → pick a template → in the UI configuration step, select <strong>Blazor UI library = MudBlazor</strong>. Everything else works the same as a Blazorise project. After Build &amp; Run you land on a MudBlazor-styled application.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png" alt="ABP Studio New Solution wizard with MudBlazor selected" />
<em>New Solution wizard: pick MudBlazor for the Blazor UI library</em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-first-run.png" alt="First run after creation" />
<em>Studio Build &amp; Run brings up a MudBlazor + LeptonX dashboard in the embedded browser</em></p>
<h3>CLI</h3>
<pre><code class="language-bash"># Blazorise (default; --blazor-ui-library can be omitted)
abp new MyApp -u blazor

# MudBlazor
abp new MyApp -u blazor --blazor-ui-library mudblazor

# Tiered + WebApp + LeptonX + MudBlazor
abp new MyApp -t app --tiered -u blazor-webapp --blazor-ui-library mudblazor --theme leptonx

# Microservice + MudBlazor + Blazor Server
abp new MyApp -t microservice -u blazor-server --blazor-ui-library mudblazor

# Reusable Module + MudBlazor
abp new My.Module -t module -u blazor --blazor-ui-library mudblazor
</code></pre>
<p>Run <code>abp new --help</code> for the full option list.</p>
<p>Suite-generated MudBlazor CRUD pages are covered in the <strong>ABP Suite</strong> section above.</p>
<hr />
<h2>Try it out</h2>
<pre><code class="language-bash">abp new MyMudApp -u blazor-server --blazor-ui-library mudblazor --theme leptonx
</code></pre>
<p>Documentation:</p>
<ul>
<li><a href="https://abp.io/docs/latest/framework/ui/blazor/forms-validation?BlazorUI=MudBlazor">Forms &amp; Validation (MudBlazor)</a></li>
<li><a href="https://abp.io/docs/latest/ui-themes/lepton-x/blazor">LeptonX with MudBlazor</a></li>
<li><a href="https://abp.io/docs/latest/framework/ui/blazor/basic-theme">Basic Theme MudBlazor variant</a></li>
<li><a href="https://abp.io/docs/latest/framework/ui/blazor/page-header">Page Header (MudBlazor)</a></li>
</ul>
<h2>FAQ</h2>
<p><strong>I'm already using Blazorise — will upgrading to 10.5.0 break my project?</strong>
No. Blazorise stays the default, and package paths, type names, and namespaces are fully compatible. Follow the standard ABP upgrade flow.</p>
<p><strong>Can I use Blazorise and MudBlazor in the same project?</strong>
We don't recommend it. The UI library is a project-level choice — themes, bundling, and module dependencies all switch with it. Mixing both within a single solution leads to bundle conflicts, duplicated layouts, and similar issues.</p>
<p><strong>What about my custom razor pages?</strong>
Your custom Razor pages are tied to the UI library they were built with, so switching libraries means rewriting those pages using the component mapping above. Template-generated pages and module-provided pages don't need to be touched.</p>
<h2>Wrapping up</h2>
<p>MudBlazor is now a first-class Blazor UI library in ABP. With 10.5.0 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single <code>abp new</code> command.</p>
<p>If you hit a bug, have a suggestion, or want a particular module's MudBlazor UX prioritized, let us know via <a href="https://github.com/abpframework/abp/issues">GitHub Issues</a> or <a href="https://abp.io/support">abp.io support</a>.</p>
<h2>References</h2>
<ul>
<li><a href="https://mudblazor.com">MudBlazor official site</a></li>
<li><a href="https://abp.io/docs/latest/framework/ui/blazor">ABP Blazor UI documentation</a></li>
<li><a href="https://abp.io/themes/leptonx">ABP LeptonX theme</a></li>
<li><a href="https://abp.io/studio">ABP Studio download</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a222ef5-5d22-d617-4324-e8794e5b19ac" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a222ef5-5d22-d617-4324-e8794e5b19ac" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/dynamic-events-in-abp-dukq95m1</guid>
      <link>https://abp.io/community/posts/dynamic-events-in-abp-dukq95m1</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>event-bus</category>
      <category>abp-framework</category>
      <category>inter-microservice-communication</category>
      <category>distributed-events</category>
      <category>abp</category>
      <title>Dynamic Events in ABP</title>
      <description>ABP's Event Bus is a core infrastructure piece. The Local Event Bus handles in-process communication between services. The Distributed Event Bus handles cross-service communication over message brokers like RabbitMQ, Kafka, Azure Service Bus, and Rebus.

Both are fully type-safe — you define event types at compile time, register handlers via DI, and everything is wired up automatically. This works great, but it has one assumption: you know all your event types at compile time.</description>
      <pubDate>Tue, 24 Mar 2026 13:23:36 Z</pubDate>
      <a10:updated>2026-09-25T20:03:02Z</a10:updated>
      <content:encoded><![CDATA[<h1>Dynamic Events in ABP</h1>
<blockquote>
<p>This feature is available since ABP 10.3.</p>
</blockquote>
<p>ABP's Event Bus is a core infrastructure piece. The <strong>Local Event Bus</strong> handles in-process communication between services. The <strong>Distributed Event Bus</strong> handles cross-service communication over message brokers like RabbitMQ, Kafka, Azure Service Bus, and Rebus.</p>
<p>Both are fully type-safe — you define event types at compile time, register handlers via DI, and everything is wired up automatically. This works great, but it has one assumption: <strong>you know all your event types at compile time</strong>.</p>
<p>In practice, that assumption breaks down in several scenarios:</p>
<ul>
<li>You're building a <strong>plugin system</strong> where third-party modules register their own event types at runtime — you can't pre-define an <code>IDistributedEventHandler&lt;TEvent&gt;</code> for every possible plugin event</li>
<li>Your system receives events from <strong>external systems</strong> (webhooks, IoT devices, partner APIs) where the event schema is defined by the external party, not by your codebase</li>
<li>You're building a <strong>low-code platform</strong> where end users define event-driven workflows through a visual designer — the event names and payloads are entirely determined at runtime</li>
</ul>
<p>ABP's <strong>Dynamic Events</strong> extend the existing <code>IEventBus</code> and <code>IDistributedEventBus</code> interfaces with string-based publishing and subscription. You can publish events by name, subscribe to events by name, and handle payloads without any compile-time type binding — all while coexisting seamlessly with the existing typed event system.</p>
<h2>Publishing Events by Name</h2>
<p>The most straightforward use case: publish an event using a string name and an arbitrary payload.</p>
<pre><code class="language-csharp">public class OrderAppService : ApplicationService
{
    private readonly IDistributedEventBus _eventBus;

    public OrderAppService(IDistributedEventBus eventBus)
    {
        _eventBus = eventBus;
    }

    public async Task PlaceOrderAsync(PlaceOrderInput input)
    {
        // Business logic...

        // Publish a dynamic event — no event class needed
        await _eventBus.PublishAsync(
            &quot;OrderPlaced&quot;,
            new { OrderId = input.Id, CustomerEmail = input.Email }
        );
    }
}
</code></pre>
<p>The payload can be any serializable object — an anonymous type, a <code>Dictionary&lt;string, object&gt;</code>, or even an existing typed class. The event bus serializes the payload and sends it to the broker with the string name as the routing key.</p>
<h3>What If a Typed Event Already Exists?</h3>
<p>If the string name matches an existing typed event (via <code>EventNameAttribute</code>), the framework automatically converts the payload to the typed class and routes it through the <strong>typed pipeline</strong>. Both typed handlers and dynamic handlers are triggered.</p>
<pre><code class="language-csharp">[EventName(&quot;OrderPlaced&quot;)]
public class OrderPlacedEto
{
    public Guid OrderId { get; set; }
    public string CustomerEmail { get; set; }
}

// This handler will still receive the event, with auto-converted data
public class OrderEmailHandler : IDistributedEventHandler&lt;OrderPlacedEto&gt;
{
    public Task HandleEventAsync(OrderPlacedEto eventData)
    {
        // eventData.OrderId and eventData.CustomerEmail are populated
        return Task.CompletedTask;
    }
}
</code></pre>
<p>Publishing by name with <code>new { OrderId = ..., CustomerEmail = ... }</code> triggers this typed handler — the framework handles the serialization round-trip. This is especially useful for scenarios where a service needs to emit events without taking a dependency on the project that defines the event type.</p>
<h2>Subscribing to Dynamic Events</h2>
<p>Dynamic subscription lets you register event handlers at runtime, using a string event name.</p>
<p>The recommended approach is to use <code>IocEventHandlerFactory</code>, which is the same mechanism ABP uses internally for typed handlers. It creates a new DI scope for each event, resolves a fresh handler instance, calls <code>HandleEventAsync</code>, then disposes the scope — so the handler can use normal constructor injection without any manual scope management:</p>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddTransient&lt;PartnerOrderHandler&gt;();
}

public override void OnApplicationInitialization(
    ApplicationInitializationContext context)
{
    var eventBus = context.ServiceProvider
        .GetRequiredService&lt;IDistributedEventBus&gt;();
    var scopeFactory = context.ServiceProvider
        .GetRequiredService&lt;IServiceScopeFactory&gt;();

    // Subscribe to a dynamic event — no event class needed
    eventBus.Subscribe(&quot;PartnerOrderReceived&quot;,
        new IocEventHandlerFactory(scopeFactory, typeof(PartnerOrderHandler)));
}
</code></pre>
<p>The handler implements <code>IDistributedEventHandler&lt;DynamicEventData&gt;</code> and injects its dependencies normally:</p>
<pre><code class="language-csharp">public class PartnerOrderHandler : IDistributedEventHandler&lt;DynamicEventData&gt;
{
    private readonly IPartnerOrderProcessor _orderProcessor;

    public PartnerOrderHandler(IPartnerOrderProcessor orderProcessor)
    {
        _orderProcessor = orderProcessor;
    }

    public async Task HandleEventAsync(DynamicEventData eventData)
    {
        // eventData.EventName = &quot;PartnerOrderReceived&quot;
        // eventData.Data = the raw payload from the broker
        await _orderProcessor.ProcessAsync(eventData.EventName, eventData.Data);
    }
}
</code></pre>
<p><code>DynamicEventData</code> is a simple POCO with two properties:</p>
<ul>
<li><strong><code>EventName</code></strong> — the string name that identifies the event</li>
<li><strong><code>Data</code></strong> — the raw event data payload (the deserialized <code>object</code> from the broker)</li>
</ul>
<blockquote>
<p><code>Subscribe</code> returns an <code>IDisposable</code>. Call <code>Dispose()</code> to unsubscribe the handler at runtime. For application-lifetime subscriptions, prefer module initialization (<code>OnApplicationInitialization</code> / <code>OnApplicationInitializationAsync</code>) over subscribing inside an application service.</p>
</blockquote>
<h2>Mixed Typed and Dynamic Handlers</h2>
<p>Typed and dynamic handlers coexist naturally. When both are registered for the same event name, <strong>both are triggered</strong> — the framework automatically converts the data to the appropriate format for each handler.</p>
<pre><code class="language-csharp">var eventBus = context.ServiceProvider.GetRequiredService&lt;IDistributedEventBus&gt;();
var scopeFactory = context.ServiceProvider.GetRequiredService&lt;IServiceScopeFactory&gt;();

// Typed handler — receives OrderPlacedEto
eventBus.Subscribe&lt;OrderPlacedEto, OrderEmailHandler&gt;();

// Dynamic handler — receives DynamicEventData for the same event
eventBus.Subscribe(&quot;OrderPlaced&quot;,
    new IocEventHandlerFactory(scopeFactory, typeof(AuditLogHandler)));
</code></pre>
<p>When <code>OrderPlacedEto</code> is published (by type or by name), both handlers fire. The typed handler receives a fully deserialized <code>OrderPlacedEto</code> object. The dynamic handler receives a <code>DynamicEventData</code> wrapping the raw payload.</p>
<p>This enables a powerful pattern: the core business logic uses typed handlers for safety, while infrastructure concerns (auditing, logging, plugin hooks) use dynamic handlers for flexibility.</p>
<h2>Outbox Support</h2>
<p>Dynamic events go through the same <strong>outbox/inbox pipeline</strong> as typed events. If you have outbox configured, dynamic events benefit from the same reliability guarantees — they are stored in the outbox table within the same database transaction as your business data, then reliably delivered to the broker by the background worker.</p>
<p>No additional configuration is needed. The outbox works transparently for both typed and dynamic events:</p>
<pre><code class="language-csharp">// This dynamic event goes through the outbox if configured
using var uow = _unitOfWorkManager.Begin();
await _eventBus.PublishAsync(
    &quot;OrderPlaced&quot;,
    new { OrderId = orderId },
    onUnitOfWorkComplete: true,
    useOutbox: true
);
await uow.CompleteAsync();
</code></pre>
<h2>Local Event Bus</h2>
<p>Dynamic events work on the local event bus too, not just the distributed bus. The API is the same:</p>
<pre><code class="language-csharp">var localEventBus = context.ServiceProvider
    .GetRequiredService&lt;ILocalEventBus&gt;();

// Subscribe dynamically
localEventBus.Subscribe(&quot;UserActivityTracked&quot;,
    new SingleInstanceHandlerFactory(
        new ActionEventHandler&lt;DynamicEventData&gt;(eventData =&gt;
        {
            // Handle the event
            return Task.CompletedTask;
        })));

// Publish dynamically
await localEventBus.PublishAsync(&quot;UserActivityTracked&quot;, new
{
    UserId = currentUser.Id,
    Action = &quot;PageView&quot;,
    Url = &quot;/products/42&quot;
});
</code></pre>
<h2>Provider Support</h2>
<p>Dynamic events work with all distributed event bus providers:</p>
<p>| Provider | Dynamic Subscribe | Dynamic Publish |
|---|---|---|
| LocalDistributedEventBus (default) | ✅ | ✅ |
| RabbitMQ | ✅ | ✅ |
| Kafka | ✅ | ✅ |
| Rebus | ✅ | ✅ |
| Azure Service Bus | ✅ | ✅ |
| Dapr | ❌ | ❌ |</p>
<p>Dapr requires topic subscriptions to be declared at application startup and cannot add subscriptions at runtime. Calling <code>Subscribe(string, ...)</code> on the Dapr provider throws an <code>AbpException</code>.</p>
<h2>Summary</h2>
<p><code>IEventBus.PublishAsync(string, object)</code> and <code>IEventBus.Subscribe(string, handler)</code> let you publish and subscribe to events by name at runtime — no compile-time types required. If the event name matches a typed event, the framework auto-converts the payload and triggers both typed and dynamic handlers. Dynamic events go through the same outbox/inbox pipeline as typed events, so reliability guarantees are preserved. This works across all providers except Dapr, and coexists seamlessly with the existing typed event system.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/event-bus/local">Local Event Bus</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed">Distributed Event Bus</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/rabbitmq">RabbitMQ Integration</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/kafka">Kafka Integration</a></li>
<li><a href="https://github.com/abpframework/abp-samples/tree/master/DynamicDistributedEvents">Dynamic Distributed Events Sample</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a203230-b02b-5b28-c2ac-d0200b205d15" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a203230-b02b-5b28-c2ac-d0200b205d15" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/dynamic-background-jobs-and-workers-in-abp-wfdkdsq9</guid>
      <link>https://abp.io/community/posts/dynamic-background-jobs-and-workers-in-abp-wfdkdsq9</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>background-worker</category>
      <category>ABP Framework</category>
      <category>background-jobs</category>
      <title>Dynamic Background Jobs and Workers in ABP</title>
      <description>ABP's Background Jobs and Background Workers are two well-established infrastructure pieces. Background jobs handle fire-and-forget async tasks — sending emails, generating reports, processing orders. Background workers handle continuously running periodic tasks — syncing inventory, cleaning up expired data, pushing scheduled notifications.

This works great, but it has one assumption: you know all your job and worker types at compile time.</description>
      <pubDate>Tue, 24 Mar 2026 13:16:40 Z</pubDate>
      <a10:updated>2026-09-26T00:49:37Z</a10:updated>
      <content:encoded><![CDATA[<h1>Dynamic Background Jobs and Workers in ABP</h1>
<blockquote>
<p>This feature is available since ABP 10.3.</p>
</blockquote>
<p>ABP's Background Jobs and Background Workers are two well-established infrastructure pieces. Background jobs handle fire-and-forget async tasks — sending emails, generating reports, processing orders. Background workers handle continuously running periodic tasks — syncing inventory, cleaning up expired data, pushing scheduled notifications.</p>
<p>This works great, but it has one assumption: <strong>you know all your job and worker types at compile time</strong>.</p>
<p>In practice, that assumption breaks down more often than you'd expect:</p>
<ul>
<li>You're building a <strong>plugin system</strong> where third-party plugins need to register their own background processing logic at runtime — you can't pre-define an <code>IBackgroundJob&lt;TArgs&gt;</code> implementation in the host project for every possible plugin</li>
<li>Your system needs to execute background tasks based on <strong>external configuration</strong> (database, API responses) — the task types and parameters are entirely unknown at compile time</li>
<li>Your <strong>multi-tenant SaaS platform</strong> needs different sync intervals for different tenants — some every 30 seconds, some every 5 minutes — and you need to adjust these without restarting the application</li>
<li>You're building a <strong>low-code/no-code platform</strong> where end users define automation workflows through a visual designer, and those workflows need to run as background jobs or scheduled tasks — the job types and scheduling parameters are entirely determined by end users at runtime, unknowable to developers at compile time</li>
</ul>
<p>ABP's <strong>Dynamic Background Jobs</strong> (<code>IDynamicBackgroundJobManager</code>) and <strong>Dynamic Background Workers</strong> (<code>IDynamicBackgroundWorkerManager</code>) are designed for exactly these scenarios. They let you register, enqueue, schedule, and manage background tasks by name at runtime, with no compile-time type binding required.</p>
<h2>Dynamic Background Jobs</h2>
<p><code>IDynamicBackgroundJobManager</code> offers two usage patterns, covering different levels of runtime flexibility.</p>
<h3>Enqueue an Existing Typed Job by Name</h3>
<p>If you already have a typed background job (say, an <code>EmailSendingJob</code> registered via <code>[BackgroundJobName(&quot;emails&quot;)]</code>), you can enqueue it by name without referencing its args type:</p>
<pre><code class="language-csharp">public class OrderAppService : ApplicationService
{
    private readonly IDynamicBackgroundJobManager _dynamicJobManager;

    public OrderAppService(IDynamicBackgroundJobManager dynamicJobManager)
    {
        _dynamicJobManager = dynamicJobManager;
    }

    public async Task PlaceOrderAsync(PlaceOrderInput input)
    {
        // Business logic...

        // Enqueue a confirmation email — no reference to EmailSendingJobArgs needed
        await _dynamicJobManager.EnqueueAsync(&quot;emails&quot;, new
        {
            EmailAddress = input.CustomerEmail,
            Subject = &quot;Order Confirmed&quot;,
            Body = $&quot;Your order {input.OrderId} has been placed.&quot;
        });
    }
}
</code></pre>
<p>The framework looks up the typed job configuration by name, serializes the anonymous object, deserializes it into the correct args type, and feeds it through the standard typed job pipeline. The caller doesn't need to <code>using</code> any specific project namespace.</p>
<h3>Register a Runtime Dynamic Handler</h3>
<p>When you don't even have a job type — say a plugin decides at startup what processing logic to register — you can register a handler directly:</p>
<pre><code class="language-csharp">public override async Task OnApplicationInitializationAsync(
    ApplicationInitializationContext context)
{
    var dynamicJobManager = context.ServiceProvider
        .GetRequiredService&lt;IDynamicBackgroundJobManager&gt;();

    // A plugin registers its own processing logic at startup
    dynamicJobManager.RegisterHandler(&quot;SyncExternalCatalog&quot;, async (jobContext, ct) =&gt;
    {
        using var doc = JsonDocument.Parse(jobContext.JsonData);
        var catalogUrl = doc.RootElement.GetProperty(&quot;url&quot;).GetString();

        var httpClient = jobContext.ServiceProvider
            .GetRequiredService&lt;IHttpClientFactory&gt;()
            .CreateClient();

        var catalog = await httpClient.GetStringAsync(catalogUrl, ct);
        // Process catalog data...
    });

    // Now you can enqueue jobs for this handler
    await dynamicJobManager.EnqueueAsync(&quot;SyncExternalCatalog&quot;, new
    {
        Url = &quot;https://partner-api.example.com/catalog&quot;
    });
}
</code></pre>
<p>The handler receives a context object containing <code>JsonData</code> (the raw JSON string) and <code>ServiceProvider</code> (a scoped container). Resolving dependencies from <code>ServiceProvider</code> is the recommended approach — avoid capturing external state in the handler closure.</p>
<p>There's one priority rule to keep in mind: <strong>if a name matches both a typed job and a dynamic handler, the typed job wins</strong>. Dynamic handlers never accidentally override existing typed jobs.</p>
<blockquote>
<p>Dynamic jobs ultimately go through the standard typed job pipeline, so they <strong>work with every background job provider</strong> — Default, Hangfire, Quartz, RabbitMQ, TickerQ — without any provider-specific code.</p>
</blockquote>
<h2>Dynamic Background Workers</h2>
<p><code>IDynamicBackgroundWorkerManager</code> lets you register periodic tasks at runtime and manage their full lifecycle: add, remove, update schedule.</p>
<pre><code class="language-csharp">public override async Task OnApplicationInitializationAsync(
    ApplicationInitializationContext context)
{
    var workerManager = context.ServiceProvider
        .GetRequiredService&lt;IDynamicBackgroundWorkerManager&gt;();

    await workerManager.AddAsync(
        &quot;InventorySyncWorker&quot;,
        new DynamicBackgroundWorkerSchedule
        {
            Period = 30000 // 30 seconds
        },
        async (workerContext, cancellationToken) =&gt;
        {
            var syncService = workerContext.ServiceProvider
                .GetRequiredService&lt;IInventorySyncAppService&gt;();

            await syncService.SyncAsync(cancellationToken);
        }
    );
}
</code></pre>
<p>If you're using Hangfire or Quartz as your provider, you can use a cron expression instead of a fixed interval:</p>
<pre><code class="language-csharp">await workerManager.AddAsync(
    &quot;DailyReportWorker&quot;,
    new DynamicBackgroundWorkerSchedule
    {
        CronExpression = &quot;0 2 * * *&quot; // Every day at 2:00 AM
    },
    async (workerContext, cancellationToken) =&gt;
    {
        var reportService = workerContext.ServiceProvider
            .GetRequiredService&lt;IReportAppService&gt;();

        await reportService.GenerateDailyReportAsync(cancellationToken);
    }
);
</code></pre>
<h3>Runtime Schedule Management</h3>
<p>Adding a worker is just the beginning. The real value of dynamic workers is that the entire lifecycle is controllable at runtime:</p>
<pre><code class="language-csharp">// Check if a worker is currently registered
bool exists = workerManager.IsRegistered(&quot;InventorySyncWorker&quot;);

// A tenant upgrades their plan — speed up sync from 30s to 10s
await workerManager.UpdateScheduleAsync(
    &quot;InventorySyncWorker&quot;,
    new DynamicBackgroundWorkerSchedule { Period = 10000 }
);

// Tenant disables the sync feature — remove the worker entirely
await workerManager.RemoveAsync(&quot;InventorySyncWorker&quot;);
</code></pre>
<p><code>UpdateScheduleAsync</code> only changes the schedule — the handler itself stays the same. For persistent providers like Hangfire and Quartz, <code>UpdateScheduleAsync</code> and <code>RemoveAsync</code> can operate on the persistent scheduling record even after an application restart, when the handler is no longer in memory.</p>
<h3>Stopping All Workers</h3>
<p>When you need to stop all dynamic workers at once (e.g., as part of a graceful shutdown), call <code>StopAllAsync</code>:</p>
<pre><code class="language-csharp">await workerManager.StopAllAsync(cancellationToken);
</code></pre>
<p>All registered workers are stopped and cleaned up, and the handler registry is cleared. Calling <code>AddAsync</code> or <code>UpdateScheduleAsync</code> after this throws <code>ObjectDisposedException</code> — this is intentional, preventing new workers from being added during a shutdown sequence.</p>
<h2>Provider Support</h2>
<p>Dynamic background jobs and dynamic background workers have different levels of provider support.</p>
<p><strong>Dynamic background jobs</strong> are compatible with all providers because they reuse the standard typed job pipeline:</p>
<p>| Provider | Supported |
|---|---|
| Default (In-Memory) | ✅ |
| Hangfire | ✅ |
| Quartz | ✅ |
| RabbitMQ | ✅ |
| TickerQ | ✅ |</p>
<p><strong>Dynamic background workers</strong> have per-provider implementations:</p>
<p>| Provider | AddAsync | RemoveAsync | UpdateScheduleAsync | Period | CronExpression |
|---|---|---|---|---|---|
| Default (In-Memory) | ✅ | ✅ | ✅ | ✅ | ❌ |
| Hangfire | ✅ | ✅ | ✅ | ✅ | ✅ |
| Quartz | ✅ | ✅ | ✅ | ✅ | ✅ |
| TickerQ | ❌ | ❌ | ❌ | — | — |</p>
<p>TickerQ uses <code>FrozenDictionary</code> for function registration, which requires all functions to be registered before the application starts. Runtime dynamic registration is not possible.</p>
<h2>Restart Behavior</h2>
<p>Dynamic handlers are stored <strong>in memory</strong> and are not persisted across application restarts. This is a deliberate design choice — handlers are code logic (delegates), and code logic is inherently not serializable.</p>
<p>For persistent providers (Hangfire, Quartz), this means: enqueued jobs and recurring job entries survive a restart in the database, but the handlers need to be re-registered. If a handler is not re-registered, the job executor throws an exception (background jobs) or skips the execution with a warning log (background workers).</p>
<p>The recommended approach is to register handlers in <code>OnApplicationInitializationAsync</code>, so they are automatically restored on every startup:</p>
<pre><code class="language-csharp">public override async Task OnApplicationInitializationAsync(
    ApplicationInitializationContext context)
{
    var dynamicJobManager = context.ServiceProvider
        .GetRequiredService&lt;IDynamicBackgroundJobManager&gt;();

    // Re-registered on every startup — persistent jobs will find their handler
    dynamicJobManager.RegisterHandler(&quot;SyncExternalCatalog&quot;, async (jobContext, ct) =&gt;
    {
        // handler logic...
    });
}
</code></pre>
<h2>Summary</h2>
<p><code>IDynamicBackgroundJobManager</code> lets you enqueue jobs and register handlers by name at runtime, compatible with all background job providers, no compile-time types required. <code>IDynamicBackgroundWorkerManager</code> lets you add, remove, and update the schedule of periodic workers at runtime — Hangfire and Quartz providers also support cron expressions. Register handlers in <code>OnApplicationInitializationAsync</code> to ensure automatic recovery on every startup.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/background-jobs">Background Jobs</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/background-workers">Background Workers</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire">Hangfire Background Job Manager</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/background-jobs/quartz">Quartz Background Job Manager</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a20322a-56ea-3287-5193-40901a83a224" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a20322a-56ea-3287-5193-40901a83a224" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/shared-user-accounts-in-abp-multitenancy-mf3bkg79</guid>
      <link>https://abp.io/community/posts/shared-user-accounts-in-abp-multitenancy-mf3bkg79</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>multi-tenancy</category>
      <category>saas</category>
      <category>abp-framework</category>
      <category>abp</category>
      <title>Shared User Accounts in ABP Multi-Tenancy</title>
      <description>Multi-tenancy is built on isolation — isolated data, isolated permissions, isolated users. ABP's default behavior has always followed this assumption: one user belongs to exactly one tenant. Clean, simple, no ambiguity. For most SaaS applications, that's exactly what you want. 

But isolation is the system's concern, not the user's. In practice, people's work doesn't always line up neatly with tenant boundaries.</description>
      <pubDate>Tue, 17 Mar 2026 11:35:38 Z</pubDate>
      <a10:updated>2026-09-25T12:41:20Z</a10:updated>
      <content:encoded><![CDATA[<h1>Shared User Accounts in ABP Multi-Tenancy</h1>
<p>Multi-tenancy is built on <strong>isolation</strong> — isolated data, isolated permissions, isolated users. ABP's default behavior has always followed this assumption: one user belongs to exactly one tenant. Clean, simple, no ambiguity. For most SaaS applications, that's exactly what you want. (The new <code>TenantUserSharingStrategy</code> enum formally names this default behavior <code>Isolated</code>.)</p>
<p>But isolation is <strong>the system's</strong> concern, not <strong>the user's</strong>. In practice, people's work doesn't always line up neatly with tenant boundaries.</p>
<p>Think about a financial consultant who works with three different companies — each one a tenant in your system. Under the Isolated model, she needs three separate accounts, three passwords. Forgot which password goes with which company? Good luck. Worse, the system sees three unrelated people — there's nothing linking those accounts to the same human being.</p>
<p>This comes up more often than you'd think:</p>
<ul>
<li>In a <strong>corporate group</strong>, an IT admin manages multiple subsidiaries, each running as its own tenant. Every day means logging out, logging back in with different credentials, over and over</li>
<li>A <strong>SaaS platform's ops team</strong> needs to hop into different customer tenants to debug issues. Each time they create a throwaway account, then delete it — or just share one account and lose all audit trail</li>
<li>Some users resort to email aliases (<code>alice+company1@example.com</code>) to work around uniqueness constraints — that's not a solution, that's a hack</li>
</ul>
<p>The common thread here: the user's <strong>identity</strong> is global, but their <strong>working context</strong> is per-tenant. The problem isn't a technical limitation — it's that the Isolated assumption (&quot;one user, one tenant&quot;) simply doesn't hold in these scenarios.</p>
<p>What's needed is not &quot;one account per tenant&quot; but &quot;one account, multiple tenants.&quot;</p>
<p>ABP's <strong>Shared User Accounts</strong> (<code>TenantUserSharingStrategy.Shared</code>) does exactly this. It makes user identity global and turns tenants into workspaces that a user can join and switch between — similar to how one person can belong to multiple workspaces in Slack.</p>
<blockquote>
<p>This is a <strong>commercial</strong> feature, available starting from <strong>ABP 10.2</strong>, provided by the Account.Pro and Identity.Pro modules.</p>
</blockquote>
<h2>Enabling the Shared Strategy</h2>
<p>A single configuration is all it takes:</p>
<pre><code class="language-csharp">Configure&lt;AbpMultiTenancyOptions&gt;(options =&gt;
{
    options.IsEnabled = true;
    options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
</code></pre>
<p>The most important behavior change after switching to Shared: <strong>username and email uniqueness become global</strong> instead of per-tenant. This follows naturally — if the same account needs to be recognized across tenants, its identifiers must be unique across the entire system.</p>
<p>Security-related settings (2FA, account lockout, password policies, captcha, etc.) are also managed at the <strong>Host</strong> level. This makes sense too: if user identity is global, the security rules around it should be global as well.</p>
<h2>One Account, Multiple Tenants</h2>
<p>With the Shared strategy enabled, the day-to-day user experience changes fundamentally.</p>
<p>When a user is associated with only one tenant, the system recognizes it automatically and signs them in directly — the user doesn't even notice that tenants exist. When the user belongs to multiple tenants, the login flow presents a tenant selection screen after credentials are verified:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/tenant-selection.png" alt="tenant-selection" /></p>
<p>After signing into a tenant, a tenant switcher appears in the user menu — click it anytime to jump to another tenant without signing out. ABP re-issues the authentication ticket (with the new <code>TenantId</code> in the claims) on each switch, so the permission system is fully independent per tenant.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/switch-tenant.png" alt="switch-tenant" /></p>
<p>Users can also leave a tenant. Leaving doesn't delete the association record — it marks it as inactive. This preserves foreign key relationships with other entities. If the user is invited back later, the association is simply reactivated instead of recreated.</p>
<p>The same soft removal is available to a tenant admin from the user list — a <strong>Remove from tenant</strong> action that takes a user off the tenant without touching the global account. Useful for the obvious case: an employee leaves the company, the admin removes them from the tenant, but their account (and any other tenant they belong to) stays intact.</p>
<p>Back to our earlier scenario: the financial consultant now has one account, one password. She picks which company to work in at login, switches between them during the day. The system knows it's the same person, and the audit log can trace her actions across every tenant.</p>
<h2>Invitations</h2>
<p>Users don't just appear in a tenant — someone has to invite them. This is the core operation from the administrator's perspective.</p>
<p>A tenant admin opens the invitation dialog, enters one or more email addresses (batch invitations are supported), and can pre-assign roles — so the user gets the right permissions the moment they join, no extra setup needed:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-user.png" alt="invite-user" /></p>
<p>The invited person receives an email with a link. What happens next depends on whether they already have an account.</p>
<p>If they <strong>already have an account</strong>, they see a confirmation page and can join the tenant with a single click:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/exist-user-accept.png" alt="exist-user-accept" /></p>
<p>If they <strong>don't have an account yet</strong>, the link takes them to a registration form. Once they register, they're automatically added to the tenant:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-accept.png" alt="new-user-accept" /></p>
<p>Admins can also manage pending invitations at any time — resend emails or revoke invitations.</p>
<blockquote>
<p>The invitation feature is also available under the Isolated strategy, but invited users can only join a single tenant.</p>
</blockquote>
<h2>Setting Up a New Tenant</h2>
<p>There's a notable shift in how new tenants are bootstrapped.</p>
<p>Under the Isolated model, creating a tenant typically seeds an <code>admin</code> user automatically. With Shared, this no longer happens — because users are global, and it doesn't make sense to create one out of thin air for a specific tenant.</p>
<p>Instead, you create the tenant first, then invite someone in and grant them the admin role.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant.png" alt="invite-admin-user-to-join-tenant" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/invite-admin-user-to-join-tenant-modal.png" alt="invite-admin-user-to-join-tenant-modal" /></p>
<p>This is a natural fit — the admin is just a global user who happens to hold the admin role in this particular tenant.</p>
<h2>Where Do Newly Registered Users Go?</h2>
<p>Under the Shared strategy, self-registration runs into an interesting problem: the system doesn't know which tenant the user wants to join. Without being signed in, tenant context is usually determined by subdomain or a tenant switcher on the login page — but for a brand-new user, those signals might not exist at all.</p>
<p>So ABP's approach is: <strong>don't establish any tenant association at registration time</strong>. A newly registered user doesn't belong to any tenant, and doesn't belong to the Host either — this is an entirely new state. ABP still lets these users sign in, change their password, and manage their account, but they can't access any permission-protected features within a tenant.</p>
<p><code>AbpIdentityPendingTenantUserOptions.Strategy</code> controls what happens in this &quot;pending&quot; state.</p>
<p><strong>CreateTenant</strong> — automatically creates a tenant for the new user. This fits the &quot;sign up and get your own workspace&quot; pattern, like how Slack or Notion handles registration: you register, the system spins up a workspace for you.</p>
<pre><code class="language-csharp">Configure&lt;AbpIdentityPendingTenantUserOptions&gt;(options =&gt;
{
    options.Strategy = AbpIdentityPendingTenantUserStrategy.CreateTenant;
});
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-create-tenant.png" alt="new-user-join-strategy-create-tenant" /></p>
<p><strong>Inform</strong> (the default) — shows a message telling the user to contact an administrator to join a tenant. This is the right choice for invite-only platforms where users must be brought in by an existing tenant admin.</p>
<pre><code class="language-csharp">Configure&lt;AbpIdentityPendingTenantUserOptions&gt;(options =&gt;
{
    options.Strategy = AbpIdentityPendingTenantUserStrategy.Inform;
});
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-17-Shared-User-Accounts-in-ABP/new-user-join-strategy-inform.png" alt="new-user-join-strategy-inform" /></p>
<p>There's also a <strong>Redirect</strong> strategy that sends the user to a custom URL for more complex flows.</p>
<blockquote>
<p>See the <a href="https://abp.io/docs/latest/modules/account/shared-user-accounts">official documentation</a> for full configuration details.</p>
</blockquote>
<h2>Database Considerations</h2>
<p>The Shared strategy introduces some mechanisms and constraints at the database level that are worth understanding.</p>
<h3>Global Uniqueness: Enforced in Code, Not by Database Indexes</h3>
<p>Username and email uniqueness checks must span all tenants. ABP disables the tenant filter (<code>TenantFilter.Disable()</code>) during validation and searches globally for conflicts.</p>
<p>A notable design choice here: <strong>global uniqueness is enforced at the application level, not through database unique indexes</strong>. The reason is practical — in a database-per-tenant setup, users live in separate physical databases, so a cross-database unique index simply isn't possible. Even in a shared database, soft-delete complicates unique indexes (you'd need a composite index on &quot;username + deletion time&quot;). So ABP handles this in application code instead.</p>
<p>To keep things safe under concurrency — say two tenant admins invite the same email address at the same time — ABP uses a <strong>distributed lock</strong> to serialize uniqueness validation. This means your production environment needs a distributed lock provider configured (such as Redis).</p>
<p>The uniqueness check goes beyond just &quot;no duplicate usernames.&quot; ABP also checks for <strong>cross-field conflicts</strong>: a user's username can't match another user's email, and vice versa. This prevents identity confusion in edge cases.</p>
<h3>Tenants with Separate Databases</h3>
<p>If some of your tenants use their own database (database-per-tenant), the Shared strategy requires extra attention.</p>
<p>The login flow and tenant selection happen on the <strong>Host side</strong>. This means the Host database's <code>AbpUsers</code> table must contain records for all users — even those originally created in a tenant's separate database. ABP's approach is replication: it saves the primary user record in the Host context and creates a copy in the tenant context. In a shared-database setup, both records live in the same table; in a database-per-tenant setup, they live in different physical databases. Updates and deletes are kept in sync automatically.</p>
<p>If your application uses social login or passkeys, the <code>AbpUserLogins</code> and <code>AbpUserPasskeys</code> tables also need to be synced in the Host database.</p>
<h3>Migrating from the Isolated Strategy</h3>
<p>If you're moving an existing multi-tenant application from Isolated to Shared, ABP automatically runs a global uniqueness check when you switch the strategy and reports any conflicts.</p>
<p>The most common conflict: the same email address registered as separate users in different tenants. You'll need to resolve these first — merge the accounts or change one side's email — before the Shared strategy can be enabled.</p>
<h2>Summary</h2>
<p>ABP's Shared User Accounts addresses a real-world need in multi-tenant systems: one person working across multiple tenants.</p>
<ul>
<li>One configuration switch to <code>TenantUserSharingStrategy.Shared</code></li>
<li>User experience: pick a tenant at login, switch between tenants anytime, one password for everything</li>
<li>Admin experience: invite users by email, pre-assign roles on invitation</li>
<li>Database notes: configure a distributed lock provider for production; tenants with separate databases need user records replicated in the Host database</li>
</ul>
<p>ABP takes care of global uniqueness validation, tenant association management, and login flow adaptation under the hood.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/modules/account/shared-user-accounts">Shared User Accounts</a></li>
<li><a href="https://abp.io/docs/latest/framework/architecture/multi-tenancy">ABP Multi-Tenancy</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a200dc1-54da-92b4-6482-07c244ca5cb8" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a200dc1-54da-92b4-6482-07c244ca5cb8" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/secure-client-authentication-with-privatekeyjwt-in-abp-10.3-b2rf18bc</guid>
      <link>https://abp.io/community/posts/secure-client-authentication-with-privatekeyjwt-in-abp-10.3-b2rf18bc</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>openiddict</category>
      <category>authentication</category>
      <category>abp-framework</category>
      <category>abp</category>
      <category>openiddict-module</category>
      <title>Secure Client Authentication with private_key_jwt in ABP 10.3</title>
      <description>If you've built a confidential client with ABP's OpenIddict module, you know the drill: create an application in the management UI, set a client_id, generate a client_secret, and paste that secret into your client's appsettings.json or environment variables. It works. It's familiar. And for a lot of projects, it's perfectly fine.</description>
      <pubDate>Fri, 13 Mar 2026 00:51:11 Z</pubDate>
      <a10:updated>2026-09-25T22:31:04Z</a10:updated>
      <content:encoded><![CDATA[<h1>Secure Client Authentication with private_key_jwt in ABP 10.3</h1>
<p>If you've built a confidential client with ABP's OpenIddict module, you know the drill: create an application in the management UI, set a <code>client_id</code>, generate a <code>client_secret</code>, and paste that secret into your client's <code>appsettings.json</code> or environment variables. It works. It's familiar. And for a lot of projects, it's perfectly fine.</p>
<p>But <code>client_secret</code> is a <strong>shared secret</strong> — and shared secrets carry an uncomfortable truth: the same value exists in two places at once. The authorization server stores a hash of it in the database, and your client stores the raw value in configuration. That means two potential leak points. Worse, the secret has no inherent identity. Anyone who obtains the string can impersonate your client and the server has no way to tell the difference.</p>
<p>For many teams, this tradeoff is acceptable. But certain scenarios make it hard to ignore:</p>
<ul>
<li><strong>Microservice-to-microservice calls</strong>: A backend mesh of a dozen services, each with its own <code>client_secret</code> scattered across deployment configs and CI/CD pipelines. Rotating them across environments without missing one becomes a coordination problem.</li>
<li><strong>Multi-tenant SaaS platforms</strong>: Every tenant's client application deserves truly isolated credentials. With shared secrets, the database holds hashed copies for all tenants — a breach of that table is a breach of everyone's credentials.</li>
<li><strong>Financial-grade API (FAPI) compliance</strong>: Standards like <a href="https://openid.net/specs/fapi-2_0-security-profile.html">FAPI 2.0</a> explicitly require asymmetric client authentication. <code>client_secret</code> doesn't make the cut.</li>
<li><strong>Zero-trust architectures</strong>: In a zero-trust model, identity must be cryptographically provable, not based on a string that can be copied and pasted.</li>
</ul>
<p>The underlying problem is that a shared secret is just a password. It can be stolen, replicated, and used without leaving a trace. The fix has existed in cryptography for decades: <strong>asymmetric keys</strong>.</p>
<p>With asymmetric key authentication, the client generates a key pair. The public key is registered with the authorization server. The private key never leaves the client. Each time the client needs a token, it signs a short-lived JWT — called a <em>client assertion</em> — with the private key. The server verifies the signature using the registered public key. There is no secret on the server side that could be used to forge a request, because the private key is never transmitted or stored remotely.</p>
<p>This is exactly what the <strong><code>private_key_jwt</code></strong> client authentication method, defined in <a href="https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OpenID Connect Core</a>, provides. ABP's OpenIddict module now supports it end-to-end: you register a <strong>JSON Web Key Set (JWKS)</strong> containing your public key through the application management UI (ABP Commercial), and your client authenticates using the corresponding private key. The key generation tooling (<code>abp generate-jwks</code>) ships as part of the open-source ABP CLI.</p>
<blockquote>
<p>This feature is available starting from <strong>ABP Framework 10.3</strong>.</p>
</blockquote>
<h2>How It Works</h2>
<p>The flow is straightforward:</p>
<ol>
<li>The client holds an RSA key pair — <strong>private key</strong> (kept locally) and <strong>public key</strong> (registered on the authorization server as a JWKS).</li>
<li>On each token request, the client uses the private key to sign a JWT with a short expiry and a unique <code>jti</code> claim.</li>
<li>The authorization server verifies the signature against the registered public key and issues a token if it checks out.</li>
</ol>
<p>The private key never leaves the client. Even if someone obtains the authorization server's database, there's nothing there that can be used to generate a valid client assertion.</p>
<h2>Generating a Key Pair</h2>
<p>ABP CLI includes a <code>generate-jwks</code> command that creates an RSA key pair in the right formats:</p>
<pre><code class="language-bash">abp generate-jwks
</code></pre>
<p>This produces two files in the current directory:</p>
<ul>
<li><code>jwks.json</code> — the public key in JWKS format, to be uploaded to the server</li>
<li><code>jwks-private.pem</code> — the private key in PKCS#8 PEM format, to be kept on the client</li>
</ul>
<p>You can customize the output directory, key size, and signing algorithm:</p>
<pre><code class="language-bash">abp generate-jwks --alg RS512 --key-size 4096 -o ./keys -f myapp
</code></pre>
<blockquote>
<p>Supported algorithms: <code>RS256</code>, <code>RS384</code>, <code>RS512</code>, <code>PS256</code>, <code>PS384</code>, <code>PS512</code>. The default is <code>RS256</code> with a 2048-bit key.</p>
</blockquote>
<p>The command also prints the contents of <code>jwks.json</code> to the console so you can copy it directly.</p>
<h2>Registering the JWKS in the Management UI</h2>
<p>Open <strong>OpenIddict → Applications</strong> in the ABP admin panel and create or edit a confidential application (Client Type: <code>Confidential</code>).</p>
<p>In the <strong>Client authentication method</strong> section, you'll find the new <strong>JSON Web Key Set</strong> field.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-12-OpenIddict-private-key-jwt/create-edit-ui.png" alt="" /></p>
<p>Paste the contents of <code>jwks.json</code> into the <strong>JSON Web Key Set</strong> field:</p>
<pre><code class="language-json">{
  &quot;keys&quot;: [
    {
      &quot;kty&quot;: &quot;RSA&quot;,
      &quot;use&quot;: &quot;sig&quot;,
      &quot;kid&quot;: &quot;6444...&quot;,
      &quot;alg&quot;: &quot;RS256&quot;,
      &quot;n&quot;: &quot;tx...&quot;,
      &quot;e&quot;: &quot;AQAB&quot;
    }
  ]
}
</code></pre>
<p>Save the application. It's now configured for <code>private_key_jwt</code> authentication. You can set either <code>client_secret</code> or a JWKS, or both — ABP enforces that a confidential application always has at least one credential.</p>
<h2>Requesting a Token with the Private Key</h2>
<p>On the client side, each token request requires building a <em>client assertion</em> JWT signed with the private key. Here's a complete <code>client_credentials</code> example:</p>
<pre><code class="language-csharp">// Discover the authorization server endpoints (including the issuer URI).
var client = new HttpClient();
var configuration = await client.GetDiscoveryDocumentAsync(&quot;https://your-auth-server/&quot;);

// Load the private key generated by `abp generate-jwks`.
using var rsaKey = RSA.Create();
rsaKey.ImportFromPem(await File.ReadAllTextAsync(&quot;jwks-private.pem&quot;));

// Read the kid from jwks.json so it stays in sync with the server-registered public key.
string? signingKid = null;
if (File.Exists(&quot;jwks.json&quot;))
{
    using var jwksDoc = JsonDocument.Parse(await File.ReadAllTextAsync(&quot;jwks.json&quot;));
    if (jwksDoc.RootElement.TryGetProperty(&quot;keys&quot;, out var keysElem) &amp;&amp;
        keysElem.GetArrayLength() &gt; 0 &amp;&amp;
        keysElem[0].TryGetProperty(&quot;kid&quot;, out var kidElem))
    {
        signingKid = kidElem.GetString();
    }
}

var signingKey = new RsaSecurityKey(rsaKey) { KeyId = signingKid };
var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256);

// Build the client assertion JWT.
var now = DateTime.UtcNow;
var jwtHandler = new JsonWebTokenHandler();
var clientAssertionToken = jwtHandler.CreateToken(new SecurityTokenDescriptor
{
    // OpenIddict requires typ = &quot;client-authentication+jwt&quot; for client assertion JWTs.
    TokenType = &quot;client-authentication+jwt&quot;,
    Issuer = &quot;MyClientId&quot;,
    // aud must equal the authorization server's issuer URI from the discovery document,
    // not the token endpoint URL.
    Audience = configuration.Issuer,
    Subject = new ClaimsIdentity(new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, &quot;MyClientId&quot;),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
    }),
    IssuedAt = now,
    NotBefore = now,
    Expires = now.AddMinutes(5),
    SigningCredentials = signingCredentials,
});

// Request a token using the client_credentials flow.
var tokenResponse = await client.RequestClientCredentialsTokenAsync(
    new ClientCredentialsTokenRequest
    {
        Address = configuration.TokenEndpoint,
        ClientId = &quot;MyClientId&quot;,
        ClientCredentialStyle = ClientCredentialStyle.PostBody,
        ClientAssertion = new ClientAssertion
        {
            Type = OidcConstants.ClientAssertionTypes.JwtBearer,
            Value = clientAssertionToken,
        },
        Scope = &quot;MyAPI&quot;,
    });
</code></pre>
<p>A few things worth paying attention to:</p>
<ul>
<li><strong><code>TokenType</code></strong> must be <code>&quot;client-authentication+jwt&quot;</code>. OpenIddict rejects client assertion JWTs that don't carry this header.</li>
<li><strong><code>Audience</code></strong> must match the authorization server's issuer URI exactly — use <code>configuration.Issuer</code> from the discovery document, not the token endpoint URL.</li>
<li><strong><code>Jti</code></strong> must be unique per request to prevent replay attacks.</li>
<li>Keep <strong><code>Expires</code></strong> short (five minutes or less). A client assertion is a one-time proof of identity, not a long-lived credential.</li>
</ul>
<p>This example uses <a href="https://github.com/IdentityModel/IdentityModel">IdentityModel</a> for the token request helpers and <a href="https://www.nuget.org/packages/Microsoft.IdentityModel.JsonWebTokens">Microsoft.IdentityModel.JsonWebTokens</a> for JWT creation.</p>
<h2>Key Rotation Without Downtime</h2>
<p>One of the practical advantages of JWKS is that it can hold multiple public keys simultaneously. This makes <strong>zero-downtime key rotation</strong> straightforward:</p>
<ol>
<li>Run <code>abp generate-jwks</code> to produce a new key pair.</li>
<li>Append the new public key to the <code>keys</code> array in your existing <code>jwks.json</code> and update the JWKS in the management UI.</li>
<li>Switch the client to sign assertions with the new private key.</li>
<li>Once the transition is complete, remove the old public key from the JWKS.</li>
</ol>
<p>During the transition window, both the old and new public keys are registered on the server, so any in-flight requests signed with either key will still validate correctly.</p>
<h2>Summary</h2>
<p>To use <code>private_key_jwt</code> authentication in an ABP Pro application:</p>
<ol>
<li>Run <code>abp generate-jwks</code> to generate an RSA key pair.</li>
<li>Paste the <code>jwks.json</code> contents into the <strong>JSON Web Key Set</strong> field in the OpenIddict application management UI.</li>
<li>On the client side, sign a short-lived <em>client assertion</em> JWT with the private key — making sure to set the correct <code>typ</code>, <code>aud</code> (from the discovery document), and a unique <code>jti</code> — then use it to request a token.</li>
</ol>
<p>ABP handles public key storage and validation automatically. OpenIddict handles the signature verification on the token endpoint. As a developer, you only need to keep the private key file secure — there's no shared secret to synchronize between client and server.</p>
<h2>References</h2>
<ul>
<li><a href="https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OpenID Connect Core — Client Authentication</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc7523">RFC 7523 — JWT Profile for Client Authentication</a></li>
<li><a href="https://abp.io/docs/latest/modules/openiddict">ABP OpenIddict Module Documentation</a></li>
<li><a href="https://abp.io/docs/latest/cli">ABP CLI Documentation</a></li>
<li><a href="https://documentation.openiddict.com/">OpenIddict Documentation</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1ff6d9-e144-1837-30c7-d0c7f92c0e03" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1ff6d9-e144-1837-30c7-d0c7f92c0e03" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/operation-rate-limiting-in-abp-framework-f4jtd6sn</guid>
      <link>https://abp.io/community/posts/operation-rate-limiting-in-abp-framework-f4jtd6sn</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp</category>
      <category>security</category>
      <title>Operation Rate Limiting in ABP Framework</title>
      <description>Almost every user-facing system eventually runs into the same problem: some operations cannot be allowed to run without limits.

Sometimes it's a cost issue — sending an SMS costs money, and generating a report hammers the database. Sometimes it's security — a login endpoint with no attempt limit is an open invitation for brute-force attacks. And sometimes it's a matter of fairness — your paid plan says "up to 100 data exports per month," and you need to actually enforce that.

</description>
      <pubDate>Tue, 10 Mar 2026 06:38:00 Z</pubDate>
      <a10:updated>2026-09-26T00:40:23Z</a10:updated>
      <content:encoded><![CDATA[<h1>Operation Rate Limiting in ABP</h1>
<p>Almost every user-facing system eventually runs into the same problem: <strong>some operations cannot be allowed to run without limits</strong>.</p>
<p>Sometimes it's a cost issue — sending an SMS costs money, and generating a report hammers the database. Sometimes it's security — a login endpoint with no attempt limit is an open invitation for brute-force attacks. And sometimes it's a matter of fairness — your paid plan says &quot;up to 100 data exports per month,&quot; and you need to actually enforce that.</p>
<p>What all these cases have in common is that the thing being limited isn't an HTTP request — it's a <em>business operation</em>, performed by a specific <em>who</em>, doing a specific <em>what</em>, against a specific <em>resource</em>.</p>
<p>ASP.NET Core ships with a built-in <a href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit">rate limiting middleware</a> that sits in the HTTP pipeline. It's excellent for broad API protection — throttling requests per IP to fend off bots or DDoS traffic. But it only sees HTTP requests. It can tell you how many requests came from an IP address; it cannot tell you:</p>
<ul>
<li><strong>&quot;How many verification codes has this phone number received today?&quot;</strong> The moment the user switches networks, the counter resets — completely useless</li>
<li><strong>&quot;How many reports has this user exported today?&quot;</strong> Switching from mobile to desktop gives them a fresh counter</li>
<li><strong>&quot;How many times has someone tried to log in as <code>alice</code>?&quot;</strong> An attacker rotating through dozens of IPs will never hit the per-IP limit</li>
</ul>
<p>There's another gap: some rate-limiting logic has no corresponding HTTP endpoint at all — it lives inside an application service method called by multiple endpoints, or triggered by a background job. HTTP middleware has no place to hook in.</p>
<p>Real-world requirements tend to look like this:</p>
<ul>
<li>The same phone number can receive at most 3 verification codes per hour, regardless of which device or IP the request comes from</li>
<li>Each user can generate at most 2 monthly sales reports per day, because a single report query scans millions of records</li>
<li>Login attempts are limited to 5 failures per username per 5 minutes, <em>and</em> 20 failures per IP per hour — two independent counters, both enforced simultaneously</li>
<li>Free-tier users get 50 AI calls per month, paid users get 500 — this is a product-defined quota, not a security measure</li>
<li>Your system integrates with an LLM provider (OpenAI, Azure OpenAI, etc.) where every call has a real dollar cost. Without per-user or per-tenant limits, a single user can exhaust your monthly budget overnight</li>
</ul>
<p>The pattern is clear: the identity being throttled is a <strong>business identity</strong> — a user, a phone number, a resource ID — not an IP address. And the action being throttled is a <strong>business operation</strong>, not an HTTP request.</p>
<p>ABP's <strong>Operation Rate Limiting</strong> module is built for exactly this. It lets you enforce limits directly in your application or domain layer, with full awareness of who is doing what.</p>
<p>This module is used by the Account (Pro) modules internally and comes pre-installed in the latest startup templates. You must have an <a href="https://abp.io/pricing">ABP Team or a higher license</a> to use this module.</p>
<h2>Defining a Policy</h2>
<p>The model is straightforward: define a named policy in <code>ConfigureServices</code>, then call <code>CheckAsync</code> wherever you need to enforce it.</p>
<p>Name your policies after the business action they protect — <code>&quot;SendSmsCode&quot;</code>, <code>&quot;GenerateReport&quot;</code>, <code>&quot;CallAI&quot;</code>. A clear name makes the intent obvious at the call site, and avoids the mystery of something like <code>&quot;policy1&quot;</code>.</p>
<pre><code class="language-csharp">Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
{
    options.AddPolicy(&quot;SendSmsCode&quot;, policy =&gt;
    {
        policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1)
              .PartitionByParameter();
    });
});
</code></pre>
<ul>
<li><code>WithFixedWindow</code> sets the time window and maximum count — here, at most 1 call per minute</li>
<li><code>PartitionByParameter</code> means each distinct value you pass at call time (such as a phone number) gets its own independent counter</li>
</ul>
<p>Then inject <code>IOperationRateLimitingChecker</code> and call <code>CheckAsync</code> at the top of the method you want to protect:</p>
<pre><code class="language-csharp">public class SmsAppService : ApplicationService
{
    private readonly IOperationRateLimitingChecker _rateLimitChecker;

    public SmsAppService(IOperationRateLimitingChecker rateLimitChecker)
    {
        _rateLimitChecker = rateLimitChecker;
    }

    public virtual async Task SendCodeAsync(string phoneNumber)
    {
        await _rateLimitChecker.CheckAsync(&quot;SendSmsCode&quot;, phoneNumber);

        // Limit not exceeded — proceed with sending the SMS
    }
}
</code></pre>
<p><code>CheckAsync</code> checks the current usage against the limit and throws <code>AbpOperationRateLimitingException</code> (HTTP 429) if the limit is already exceeded. If the check passes, it then increments the counter and proceeds. ABP's exception pipeline catches this automatically and returns a standard error response. Put <code>CheckAsync</code> first — the rate limit check is the gate, and everything else only runs if it passes.</p>
<h2>Declarative Usage with <code>[OperationRateLimiting]</code></h2>
<p>The explicit <code>CheckAsync</code> approach is useful when you need fine-grained control — for example, when you want to check the limit conditionally, or when the parameter value comes from somewhere other than a method argument. But for the common case where you simply want to enforce a policy on every invocation of a specific method, there's a cleaner way: the <code>[OperationRateLimiting]</code> attribute.</p>
<pre><code class="language-csharp">public class SmsAppService : ApplicationService
{
    [OperationRateLimiting(&quot;SendSmsCode&quot;)]
    public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber)
    {
        // Rate limit is enforced automatically — no manual CheckAsync needed.
        await _smsSender.SendAsync(phoneNumber, GenerateCode());
    }
}
</code></pre>
<p>The attribute works on both <strong>Application Service methods</strong> (via ABP's interceptor) and <strong>MVC Controller actions</strong> (via an action filter). No manual injection of <code>IOperationRateLimitingChecker</code> required.</p>
<h3>Providing the Partition Key</h3>
<p>When using the attribute, the partition key is resolved from the method's parameters automatically:</p>
<ul>
<li>Mark a parameter with <code>[RateLimitingParameter]</code> to use its <code>ToString()</code> value as the key — this is the most common case when the key is a single primitive like a phone number or email.</li>
<li>Have your input DTO implement <code>IHasOperationRateLimitingParameter</code> and provide a <code>GetPartitionParameter()</code> method — useful when the key is a property buried inside a complex input object.</li>
</ul>
<pre><code class="language-csharp">public class SendSmsCodeInput : IHasOperationRateLimitingParameter
{
    public string PhoneNumber { get; set; }
    public string Language { get; set; }

    public string? GetPartitionParameter() =&gt; PhoneNumber;
}

[OperationRateLimiting(&quot;SendSmsCode&quot;)]
public virtual async Task SendCodeAsync(SendSmsCodeInput input)
{
    // input.GetPartitionParameter() = input.PhoneNumber is used as the partition key.
}
</code></pre>
<p>If neither is provided, <code>Parameter</code> is <code>null</code> — which is perfectly valid for policies that use <code>PartitionByCurrentUser</code>, <code>PartitionByClientIp</code>, or similar partition types that don't rely on an explicit value.</p>
<pre><code class="language-csharp">// Policy uses PartitionByCurrentUser — no partition key needed.
[OperationRateLimiting(&quot;GenerateReport&quot;)]
public virtual async Task&lt;ReportDto&gt; GenerateMonthlyReportAsync()
{
    // Rate limit is checked per current user, automatically.
}
</code></pre>
<blockquote>
<p>The resolution order is: <code>[RateLimitingParameter]</code> first, then <code>IHasOperationRateLimitingParameter</code>, then <code>null</code>. If the method has parameters but none is resolved, a warning is logged to help you catch the misconfiguration early.</p>
</blockquote>
<p>You can also place <code>[OperationRateLimiting]</code> on the class itself to apply the policy to all public methods:</p>
<pre><code class="language-csharp">[OperationRateLimiting(&quot;MyServiceLimit&quot;)]
public class MyAppService : ApplicationService
{
    public virtual async Task MethodAAsync([RateLimitingParameter] string key) { ... }

    public virtual async Task MethodBAsync([RateLimitingParameter] string key) { ... }
}
</code></pre>
<p>A method-level attribute always takes precedence over the class-level one.</p>
<h2>Choosing a Partition Type</h2>
<p>The partition type controls <strong>how counters are isolated from each other</strong> — it's the most important decision when setting up a policy, because it determines <em>what dimension you're counting across</em>.</p>
<p>Getting this wrong can make your rate limiting completely ineffective. Using <code>PartitionByClientIp</code> for SMS verification? An attacker just needs to switch networks. Using <code>PartitionByCurrentUser</code> for a login endpoint? There's no current user before login, so the counter has nowhere to land.</p>
<ul>
<li><strong><code>PartitionByParameter</code></strong> — uses the value you explicitly pass as the partition key. This is the most flexible option. Pass a phone number, an email address, a resource ID, or any business identifier you have at hand. It's the right choice whenever you know exactly what the &quot;who&quot; is.</li>
<li><strong><code>PartitionByCurrentUser</code></strong> — uses the authenticated user's ID, with no value to pass. Perfect for &quot;each user gets N per day&quot; scenarios where user identity is all you need.</li>
<li><strong><code>PartitionByClientIp</code></strong> — uses the client's IP address. Don't rely on this alone — it's too easy to rotate. Use it as a secondary layer alongside another partition type, as in the login example below.</li>
<li><strong><code>PartitionByEmail</code></strong> and <strong><code>PartitionByPhoneNumber</code></strong> — designed for pre-authentication flows where the user isn't logged in yet. They prefer the <code>Parameter</code> value you explicitly pass, and fall back to the current user's email or phone number if none is provided.</li>
<li><strong><code>PartitionBy</code></strong> — a named custom resolver that can produce any partition key you need. Register a resolver function under a unique name via <code>options.AddPartitionKeyResolver(&quot;MyResolver&quot;, ctx =&gt; ...)</code>, then reference it by name: <code>.PartitionBy(&quot;MyResolver&quot;)</code>. You can also register and reference in one step: <code>.PartitionBy(&quot;MyResolver&quot;, ctx =&gt; ...)</code>. When the built-in options don't fit, you're free to implement whatever logic makes sense: look up a resource's owner in the database, derive a key from the user's subscription tier, partition by tenant — anything that returns a string. Because the resolver is stored by name (not as an anonymous delegate), it can be serialized and managed from a UI or database.</li>
</ul>
<blockquote>
<p>The rule of thumb: partition by the identity of whoever's behavior you're trying to limit.</p>
</blockquote>
<h2>Combining Rules in One Policy</h2>
<p>A single rule covers most cases, but sometimes you need to enforce limits across multiple dimensions simultaneously. Login protection is the textbook example: throttling by username alone doesn't stop an attacker from targeting many accounts; throttling by IP alone doesn't stop an attacker with a botnet. You need both, at the same time.</p>
<pre><code class="language-csharp">options.AddPolicy(&quot;Login&quot;, policy =&gt;
{
    // Rule 1: at most 5 attempts per username per 5-minute window
    policy.AddRule(rule =&gt; rule
        .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 5)
        .PartitionByParameter());

    // Rule 2: at most 20 attempts per IP per hour, counted independently
    policy.AddRule(rule =&gt; rule
        .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20)
        .PartitionByClientIp());
});
</code></pre>
<p>The two counters are completely independent. If <code>alice</code> fails 5 times, her account is locked — but other accounts from the same IP are unaffected. If an IP accumulates 20 failures, it's blocked — but <code>alice</code> can still be targeted from other IPs until their own counters fill up.</p>
<p>When multiple rules are present, the module uses a two-phase approach: it checks all rules first, and only increments counters if every rule passes. This prevents a rule from consuming quota on a request that would have been rejected by another rule anyway.</p>
<h2>Customizing Policies from Reusable Modules</h2>
<p>ABP modules (including your own) can ship with built-in rate limiting policies. For example, an Account module might define a <code>&quot;Account.SendPasswordResetCode&quot;</code> policy with conservative defaults that make sense for most applications. When you need different rules in your specific application, you have two options.</p>
<p><strong>Complete replacement with <code>AddPolicy</code>:</strong> call <code>AddPolicy</code> with the same name and the second registration wins, replacing all rules from the module:</p>
<pre><code class="language-csharp">Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
{
    options.AddPolicy(&quot;Account.SendPasswordResetCode&quot;, policy =&gt;
    {
        policy.AddRule(rule =&gt; rule
            .WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3)
            .PartitionByEmail());
    });
});
</code></pre>
<p><strong>Partial modification with <code>ConfigurePolicy</code>:</strong> when you only want to tweak part of a policy — change the error code, add a secondary rule, or tighten the window — use <code>ConfigurePolicy</code>. The builder starts pre-populated with the module's existing rules, so you only express what changes.</p>
<p>For example, keep the module's default rules but assign your own localized error code:</p>
<pre><code class="language-csharp">Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
{
    options.ConfigurePolicy(&quot;Account.SendPasswordResetCode&quot;, policy =&gt;
    {
        policy.WithErrorCode(&quot;MyApp:PasswordResetLimit&quot;);
    });
});
</code></pre>
<p>Or add a secondary IP-based rule on top of what the module already defined, without touching it:</p>
<pre><code class="language-csharp">Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
{
    options.ConfigurePolicy(&quot;Account.SendPasswordResetCode&quot;, policy =&gt;
    {
        policy.AddRule(rule =&gt; rule
            .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20)
            .PartitionByClientIp());
    });
});
</code></pre>
<p>If you want a clean slate, call <code>ClearRules()</code> first and then define entirely new rules — this gives you the same result as <code>AddPolicy</code> but makes the intent explicit:</p>
<pre><code class="language-csharp">Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
{
    options.ConfigurePolicy(&quot;Account.SendPasswordResetCode&quot;, policy =&gt;
    {
        policy.ClearRules()
              .WithFixedWindow(TimeSpan.FromMinutes(10), maxCount: 5)
              .PartitionByEmail();
    });
});
</code></pre>
<p><code>ConfigurePolicy</code> throws if the policy name doesn't exist — which catches typos at startup rather than silently doing nothing.</p>
<p>The general rule: use <code>AddPolicy</code> for full replacements, <code>ConfigurePolicy</code> for surgical modifications.</p>
<h2>Beyond Just Checking</h2>
<p>Not every scenario calls for throwing an exception. <code>IOperationRateLimitingChecker</code> provides three additional methods for more nuanced control.</p>
<p><strong><code>IsAllowedAsync</code></strong> performs a read-only check — it returns <code>true</code> or <code>false</code> without touching any counter. The most common use case is UI pre-checking: when a user opens the &quot;send verification code&quot; page, check the limit first. If they've already hit it, disable the button and show a countdown immediately, rather than making them click and get an error. That's a meaningfully better experience.</p>
<pre><code class="language-csharp">var isAllowed = await _rateLimitChecker.IsAllowedAsync(&quot;SendSmsCode&quot;, phoneNumber);
</code></pre>
<p><strong><code>GetStatusAsync</code></strong> also reads without incrementing, but returns richer data: <code>RemainingCount</code>, <code>RetryAfter</code>, and <code>CurrentCount</code>. This is what you need to build quota displays — &quot;You have 2 exports remaining today&quot; or &quot;Please try again in 47 seconds&quot; — which are far friendlier than a raw 429.</p>
<pre><code class="language-csharp">var status = await _rateLimitChecker.GetStatusAsync(&quot;SendSmsCode&quot;, phoneNumber);
// status.RemainingCount, status.RetryAfter, status.IsAllowed ...
</code></pre>
<p><strong><code>ResetAsync</code></strong> clears the counter for a given policy and context. Useful in admin panels where support staff can manually unblock a user, or in test environments where you need to reset state between runs.</p>
<pre><code class="language-csharp">await _rateLimitChecker.ResetAsync(&quot;SendSmsCode&quot;, phoneNumber);
</code></pre>
<h2>When the Limit Is Hit</h2>
<p>When <code>CheckAsync</code> triggers, it throws <code>AbpOperationRateLimitingException</code>, which:</p>
<ul>
<li>Inherits from <code>BusinessException</code> and maps to HTTP <strong>429 Too Many Requests</strong></li>
<li>Is handled automatically by ABP's exception pipeline</li>
<li>Carries useful metadata: <code>RetryAfterSeconds</code>, <code>RemainingCount</code>, <code>MaxCount</code>, <code>CurrentCount</code></li>
</ul>
<p>By default, the error code sent to the client is a generic one from the module. If you want each operation to produce its own localized message — &quot;Too many verification code requests, please wait before trying again&quot; instead of a generic error — assign a custom error code to the policy:</p>
<pre><code class="language-csharp">options.AddPolicy(&quot;SendSmsCode&quot;, policy =&gt;
{
    policy.WithFixedWindow(TimeSpan.FromMinutes(1), maxCount: 1)
          .PartitionByParameter()
          .WithErrorCode(&quot;App:SmsCodeLimit&quot;);
});
</code></pre>
<blockquote>
<p>For details on mapping error codes to localized messages, see <a href="https://abp.io/docs/latest/framework/fundamentals/exception-handling">Exception Handling</a> in the ABP docs.</p>
</blockquote>
<h2>Turning It Off in Development</h2>
<p>Rate limiting and local development don't mix well. When you're iterating quickly and calling the same endpoint a dozen times to test something, getting blocked by a 429 every few seconds is genuinely painful. Disable the module in your development environment:</p>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    var hostEnvironment = context.Services.GetHostingEnvironment();

    Configure&lt;AbpOperationRateLimitingOptions&gt;(options =&gt;
    {
        if (hostEnvironment.IsDevelopment())
        {
            options.IsEnabled = false;
        }
    });
}
</code></pre>
<h2>Summary</h2>
<p>ABP's Operation Rate Limiting fills the gap that ASP.NET Core's HTTP middleware can't: rate limiting with real awareness of <em>who</em> is doing <em>what</em>. Define a named policy, pick a time window, a max count, and a partition type. Then either call <code>CheckAsync</code> explicitly, or just add <code>[OperationRateLimiting]</code> to your method and let the framework handle the rest. Counter storage, distributed locking, and exception handling are all taken care of.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/modules/operation-rate-limiting">Operation Rate Limiting (Pro)</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit">ASP.NET Core Rate Limiting Middleware</a></li>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/exception-handling">Exception Handling</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1fe8a4-530c-f970-dbc4-fdfa961bd58b" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1fe8a4-530c-f970-dbc4-fdfa961bd58b" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/resourcebased-authorization-in-abp-framework-choku1sn</guid>
      <link>https://abp.io/community/posts/resourcebased-authorization-in-abp-framework-choku1sn</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>authorization</category>
      <category>abp-framework</category>
      <category>abp</category>
      <category>permission-management</category>
      <title>Resource-Based Authorization in ABP Framework</title>
      <description>ABP has a built-in permission system that supports role-based access control (RBAC). You define permissions, assign them to roles, and assign roles to users — once a user logs in, they automatically have the corresponding access. This covers the vast majority of real-world scenarios and is simple, straightforward, and easy to maintain.

However, there is one class of requirements it cannot handle: different access rights for different instances of the same resource type.</description>
      <pubDate>Mon, 09 Mar 2026 09:01:24 Z</pubDate>
      <a10:updated>2026-09-26T00:28:02Z</a10:updated>
      <content:encoded><![CDATA[<h1>Resource-Based Authorization in ABP Framework</h1>
<p>ABP has a built-in permission system that supports role-based access control (RBAC). You define permissions, assign them to roles, and assign roles to users — once a user logs in, they automatically have the corresponding access. This covers the vast majority of real-world scenarios and is simple, straightforward, and easy to maintain.</p>
<p>However, there is one class of requirements it cannot handle: <strong>different access rights for different instances of the same resource type</strong>.</p>
<p>Take a bookstore application as an example. You define a <code>Books.Edit</code> permission and assign it to an editor role, so every editor can modify every book. But reality is often more nuanced:</p>
<ul>
<li>A specific book should only be editable by its assigned editor</li>
<li>Certain books are only visible to specific users</li>
<li>Different users have different levels of access to the same book</li>
</ul>
<p>Standard permissions cannot address this, because their granularity is the <em>permission type</em>, not a <em>specific record</em>. The traditional approach requires designing your own database tables, writing query logic, and building a management UI from scratch — all of which is costly.</p>
<p>ABP Framework now ships with <strong>Resource-Based Authorization</strong> to solve exactly this problem. The core idea is to bind permissions to specific resource instances rather than just resource types. For example, you can grant a user permission to edit the price of <em>1984</em> specifically, while they have no access to any other book.</p>
<p>More importantly, the entire permission management workflow is handled through a built-in UI dialog — <strong>no custom code needed for the management side</strong>.</p>
<h2>How It Works</h2>
<p>Each resource instance (e.g. a book) can have its own permission management dialog. Users who hold the <code>ManagePermissions</code> permission can open it and grant or revoke access for users, roles, or OAuth clients — all from the UI.</p>
<p>A <strong>Permissions</strong> action appears in each book's action menu:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/book-list.png" alt="book-list" /></p>
<p>Clicking it opens the resource permission management dialog for that specific book. You can see who currently has access and click <strong>Add permission</strong> to grant more:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/resource-permission-dialog.png" alt="resource-permission-dialog" /></p>
<p>The <strong>Add permission</strong> dialog lets you select a user, role, or OAuth client, then choose which permissions to grant:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/add-permission-dialog.png" alt="add-permission-dialog" /></p>
<p>After saving, the new entry appears in the list immediately.</p>
<p>Each entry in the list also supports <strong>Edit</strong> and <strong>Delete</strong> actions. Clicking <strong>Edit</strong> opens the update dialog where you can adjust the granted permissions:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/update-permission-dialog.png" alt="update-permission-dialog" /></p>
<p>Clicking <strong>Delete</strong> shows a confirmation prompt — confirming removes all permissions for that user, role, or OAuth client on this book:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-03-09-Resource-Based-Authorization-in-ABP-Framework/delete-permission-confirm.png" alt="delete-permission-confirm" /></p>
<h2>Setting It Up</h2>
<p>To get this working, you need to define your resource permissions and wire up the dialog.</p>
<h3>Defining Resource Permissions</h3>
<pre><code class="language-csharp">public static class BookStorePermissions
{
    public const string GroupName = &quot;BookStore&quot;;

    public static class Books
    {
        public const string Default = GroupName + &quot;.Books&quot;;
        public const string ManagePermissions = Default + &quot;.ManagePermissions&quot;;

        public static class Resources
        {
            public const string Name = &quot;Acme.BookStore.Books.Book&quot;;
            public const string View = Name + &quot;.View&quot;;
            public const string Edit = Name + &quot;.Edit&quot;;
            public const string Delete = Name + &quot;.Delete&quot;;
        }
    }
}
</code></pre>
<pre><code class="language-csharp">public override void Define(IPermissionDefinitionContext context)
{
    var group = context.AddGroup(BookStorePermissions.GroupName);

    var bookPermission = group.AddPermission(BookStorePermissions.Books.Default);

    // Users with this permission can open the resource permission dialog
    bookPermission.AddChild(BookStorePermissions.Books.ManagePermissions);

    context.AddResourcePermission(
        name: BookStorePermissions.Books.Resources.View,
        resourceName: BookStorePermissions.Books.Resources.Name,
        managementPermissionName: BookStorePermissions.Books.ManagePermissions
    );

    context.AddResourcePermission(
        name: BookStorePermissions.Books.Resources.Edit,
        resourceName: BookStorePermissions.Books.Resources.Name,
        managementPermissionName: BookStorePermissions.Books.ManagePermissions
    );

    context.AddResourcePermission(
        name: BookStorePermissions.Books.Resources.Delete,
        resourceName: BookStorePermissions.Books.Resources.Name,
        managementPermissionName: BookStorePermissions.Books.ManagePermissions
    );
}
</code></pre>
<p>The <code>managementPermissionName</code> acts as a gate: only users who hold <code>ManagePermissions</code> will see the resource permission dialog for a book.</p>
<h3>Wiring Up the Dialog (MVC)</h3>
<p>Add the required script to your page and open the dialog using <code>abp.ModalManager</code>:</p>
<pre><code class="language-html">@section scripts
{
    &lt;abp-script src=&quot;/Pages/Books/Index.js&quot;/&gt;
    &lt;abp-script src=&quot;/Pages/AbpPermissionManagement/resource-permission-management-modal.js&quot; /&gt;
}
</code></pre>
<pre><code class="language-javascript">var _permissionsModal = new abp.ModalManager({
    viewUrl: abp.appPath + 'AbpPermissionManagement/ResourcePermissionManagementModal',
    modalClass: 'ResourcePermissionManagement'
});

function openPermissionsModal(bookId, bookName) {
    _permissionsModal.open({
        resourceName: 'Acme.BookStore.Books.Book',
        resourceKey: bookId,
        resourceDisplayName: bookName
    });
}
</code></pre>
<blockquote>
<p>For Blazor and Angular applications, ABP provides the equivalent <code>ResourcePermissionManagementModal</code> component and <code>ResourcePermissionManagementComponent</code>. See the <a href="https://abp.io/docs/latest/modules/permission-management">Permission Management Module</a> documentation for details.</p>
</blockquote>
<h2>Checking Permissions in Code</h2>
<p>The UI manages the permission assignments; the code enforces them at runtime. In your application service, use <code>AuthorizationService.CheckAsync</code> to verify that the current user holds a specific permission on a given resource instance.</p>
<p>All ABP entities implement <code>IKeyedObject</code>, which the framework uses to extract the resource key automatically — so you can pass the entity object directly without building the key manually:</p>
<pre><code class="language-csharp">public virtual async Task&lt;BookDto&gt; GetAsync(Guid id)
{
    var book = await _bookRepository.GetAsync(id);

    // Throws AbpAuthorizationException if the current user has no View permission on this book
    await AuthorizationService.CheckAsync(book, BookStorePermissions.Books.Resources.View);

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

public virtual async Task&lt;BookDto&gt; UpdateAsync(Guid id, UpdateBookDto input)
{
    var book = await _bookRepository.GetAsync(id);

    await AuthorizationService.CheckAsync(book, BookStorePermissions.Books.Resources.Edit);

    book.Name = input.Name;
    await _bookRepository.UpdateAsync(book);

    return ObjectMapper.Map&lt;Book, BookDto&gt;(book);
}
</code></pre>
<p>If you want to check a permission without throwing an exception — for example, to conditionally show or hide a button — use <code>IsGrantedAsync</code> instead, which returns a <code>bool</code>:</p>
<pre><code class="language-csharp">var canEdit = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.Edit);
</code></pre>
<h2>Don't Forget to Clean Up</h2>
<p>Every resource permission grant is stored as a record in the database. When a book is deleted, those records are not removed automatically — orphaned permission data accumulates over time.</p>
<p>Make sure to clean up resource permissions whenever a resource is deleted:</p>
<pre><code class="language-csharp">public virtual async Task DeleteAsync(Guid id)
{
    await _bookRepository.DeleteAsync(id);

    // Clean up all resource permissions for this book
    await _resourcePermissionManager.DeleteAsync(
        resourceName: BookStorePermissions.Books.Resources.Name,
        resourceKey: id.ToString()
    );
}
</code></pre>
<h2>Summary</h2>
<p>Resource-Based Authorization fills the gap between &quot;everyone can do this&quot; and &quot;only specific users can do this on specific resources.&quot; In practice, most of the work comes down to two things:</p>
<ul>
<li>Define resource permissions and wire up the built-in UI dialog so administrators can assign access through the interface</li>
<li>Call <code>AuthorizationService.CheckAsync</code> in your application services to enforce those permissions at runtime</li>
</ul>
<p>Storing permission grants, rendering the dialog, searching for users, roles, and OAuth clients — ABP handles all of that for you.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/authorization/resource-based-authorization">Resource-Based Authorization</a></li>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/authorization">Authorization</a></li>
<li><a href="https://abp.io/docs/latest/modules/permission-management">Permission Management Module</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1fe401-420b-4451-f360-972d75406786" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1fe401-420b-4451-f360-972d75406786" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-ai-is-changing-developers-e8y4a85f</guid>
      <link>https://abp.io/community/posts/how-ai-is-changing-developers-e8y4a85f</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>ai</category>
      <title>How AI Is Changing Developers</title>
      <description>In the last few years, AI has moved from “nice to have” to “hard to live without” for developers. At first it was just code completion and smart hints. Now it’s getting deep into how we build software: the methods, the toolchain, and even the job itself.

Here are some structured thoughts on how AI is affecting developers, based on trends and personal experience.</description>
      <pubDate>Mon, 26 Jan 2026 07:45:39 Z</pubDate>
      <a10:updated>2026-09-26T01:38:36Z</a10:updated>
      <content:encoded><![CDATA[<h1>How AI Is Changing Developers</h1>
<p>In the last few years, AI has moved from “nice to have” to “hard to live without” for developers. At first it was just code completion and smart hints. Now it’s getting deep into how we build software: the methods, the toolchain, and even the job itself.</p>
<p>Here are some structured thoughts on how AI is affecting developers, based on trends and personal experience.</p>
<h2>Every library will have AI-first docs</h2>
<p>Future libraries and frameworks won’t just have docs for humans. They’ll also have a manual for AI:</p>
<ul>
<li>How to use</li>
<li>Why it is designed this way</li>
<li>What NOT to do</li>
<li>Conventions &amp; Best Practices</li>
</ul>
<p>Once these rules are written in a structured way, AI can onboard to a library faster and more consistently than a junior developer.</p>
<p>Docs won’t just be knowledge anymore. They’ll be instructions AI can execute.</p>
<h2>AI will be a must-have for developers</h2>
<p>Soon, “writing code without AI” will feel as strange as “writing code without an IDE.”</p>
<ul>
<li>It won’t be about whether you use AI</li>
<li>It’ll be about how well you use it and where</li>
</ul>
<p>AI will become:</p>
<ul>
<li>A standard productivity tool</li>
<li>An extension of a developer’s thinking</li>
<li>A second brain</li>
</ul>
<p>Developers who don’t use AI will fall behind in both speed and understanding.</p>
<h2>As AI gets smarter, it replaces “time”</h2>
<p>AI isn’t replacing developers right away. It’s replacing:</p>
<ul>
<li>Lots of repetitive time</li>
<li>Basic development costs</li>
<li>Higher output per hour</li>
</ul>
<p>Boilerplate, CRUD, basic validation, simple logic — all of that will get swallowed fast.</p>
<p>It’s not people being replaced. It’s waste.</p>
<h2>Orchestrating multiple AIs becomes real</h2>
<p>The future isn’t “one AI does everything.” It’s more like:</p>
<ul>
<li>Claude writes core code</li>
<li>Copilot generates and maintains unit tests</li>
<li>Codex and similar tools write docs and examples</li>
<li>Other AIs handle refactoring, performance analysis, security checks</li>
</ul>
<p>The dev process itself becomes an AI orchestration system.</p>
<p>The developer’s role looks more like:</p>
<p>Architect + conductor + quality gatekeeper</p>
<h2>Only great infrastructure gets amplified by AI</h2>
<p>Even if AI can teach you “how to use it correctly,” it still can’t invent mature infrastructure for you.</p>
<p>We still rely on:</p>
<ul>
<li>Stable base frameworks (like <a href="https://abp.io">ABP</a>)</li>
<li>Engineering capability proven by many projects</li>
<li>Long-term maintenance and evolution</li>
</ul>
<p>AI is an accelerator, not the foundation.</p>
<p>For open source, AI is actually a better companion:</p>
<ul>
<li>Helps understand the source code</li>
<li>Helps learn design thinking</li>
<li>Helps ship faster</li>
</ul>
<p>The stronger the infrastructure, the more value AI can amplify.</p>
<h2>Frontend feels mature; backend still evolving</h2>
<p>From personal experience:</p>
<ul>
<li>AI is already very strong in frontend work (Bootstrap / UI components, layout, styling, interaction)</li>
<li>Backend is still learning and improving (business boundaries, architecture trade-offs, implicit constraints)</li>
</ul>
<p>This shows: the clearer the rules and the faster the feedback, the faster AI improves.</p>
<h2>Writing rules for AI is productivity itself</h2>
<p>In the ABP libraries, we’ve already written lots of rules for AI:</p>
<ul>
<li>Conventions</li>
<li>Usage limits</li>
<li>Recommended patterns</li>
</ul>
<p>As rules grow:</p>
<ul>
<li>AI becomes more stable</li>
<li>More predictable</li>
<li>Base development work can be largely automated</li>
</ul>
<p>Future engineering skill will be, in large part: how to design a rules system for AI.</p>
<h2>The real advantage is better feedback loops</h2>
<p>AI gets much stronger when there’s clear feedback:</p>
<ul>
<li>Tests that run fast and fail loudly</li>
<li>Logs and metrics that explain behavior</li>
<li>Code review that checks for edge cases and security</li>
</ul>
<p>The teams that win are the ones who can quickly verify, correct, and learn.</p>
<h2>About a developer’s career</h2>
<p>Sometimes I think: I’m glad I didn’t enter the software industry just in the last few years.</p>
<p>If you’re just starting out, you really feel:</p>
<ul>
<li>The barrier is lower</li>
<li>The competition is tougher</li>
</ul>
<p>But whenever I see AI generate confident but wrong code, I’m reminded:</p>
<ul>
<li>The industry still has a future</li>
<li>It still needs judgment, taste, and experience</li>
</ul>
<p>There will always be people who love coding. If AI does it and we watch, that’s fine too.</p>
<h2>Chaos everywhere, but the experience is moving fast</h2>
<p>Big companies, platforms, tools:</p>
<ul>
<li>GitHub</li>
<li>OpenAI</li>
<li>Claude</li>
<li>All kinds of IDEs / agents</li>
</ul>
<p>New AI tools, apps, and platforms keep popping up. New concepts show up almost every week. It’s noisy, but the big picture is clear: AI keeps getting better, and the overall developer experience is improving fast.</p>
<h2>Get ready for the AI revolution</h2>
<p>Looking back at personal experience:</p>
<ul>
<li>Before: Google</li>
<li>Now: ChatGPT</li>
<li>Before: manual translation</li>
<li>Now: fully automatic</li>
<li>Before: writing unit tests by hand</li>
<li>Now: AI does it all</li>
<li>Before: human replies to customers</li>
<li>Now: AI-assisted or even AI-led</li>
</ul>
<p>From code completion to agents running tasks, and now deep IDE integration — the pace is shocking.</p>
<h2>Closing</h2>
<p>AI is not the end of software engineering. It is:</p>
<ul>
<li>A leap in cognition</li>
<li>A restructure of how work gets done</li>
<li>An upgrade of roles</li>
</ul>
<p>What matters most isn’t how much code AI can write, but how we redefine the value of “developers” in the AI era.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1f0b70-ce20-a44e-1f86-c940c9e8ae28" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1f0b70-ce20-a44e-1f86-c940c9e8ae28" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/.net-conf-china-2025-changing-the-world-changing-ourselves-see-you-again-in-shanghai-fz03gfge</guid>
      <link>https://abp.io/community/posts/.net-conf-china-2025-changing-the-world-changing-ourselves-see-you-again-in-shanghai-fz03gfge</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>conference</category>
      <category>dotnetconf</category>
      <category>.net</category>
      <category>events</category>
      <category>net10</category>
      <title>.NET Conf China 2025: Changing the World, Changing Ourselves - See You Again in Shanghai</title>
      <description>.NET Conf China 2025 is an annual community event for developers, celebrating the release of .NET 10 (LTS) and the achievements of the past year in China.</description>
      <pubDate>Wed, 03 Dec 2025 08:32:26 Z</pubDate>
      <a10:updated>2026-09-26T00:03:54Z</a10:updated>
      <content:encoded><![CDATA[<h1>.NET Conf China 2025: Changing the World, Changing Ourselves - See You Again in Shanghai</h1>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/1.png" alt="" /></p>
<p>.NET Conf China 2025 is an annual community event for developers, celebrating the release of .NET 10 (LTS) and the achievements of the past year in China. As an extension of .NET Conf 2025, this event brings together local tech communities, well-known companies, and open-source organizations. It has become the largest .NET online and offline conference in China, dedicated to spreading .NET technology in Chinese and fostering collaboration and exchange.</p>
<h2>Event Highlights: Key Topics and Takeaways</h2>
<p>This year’s conference focused on three main themes: performance improvements, AI integration, and cross-platform development. Topics covered how to achieve performance gains while maintaining engineering quality, balancing between multi-platform consistency and native capabilities, and taking generative AI from “demo-level” to “production-ready.” On the community and ecosystem side, the event showcased the .NET Foundation’s and domestic and international companies’ progress in supporting architectures like ARM, LoongArch, and RISC-V. It also highlighted best practices in DevOps, observability, and engineering toolchains, creating a complete path from ideas to implementation.</p>
<h3>Opening Keynote</h3>
<p>Scott Hanselman kicked off .NET Conf China 2025 with a video keynote, announcing that .NET 10 is now available on the official website. He framed the release around four pillars—AI, cloud-native, cross-platform, and performance—including integration with the Microsoft Agent Framework for building and orchestrating multi-agent systems in .NET/C#, industry-leading container and Kubernetes support with Aspire simplifying local containerized development, a richer cross-platform desktop ecosystem (.NET MAUI, Avalonia, Uno Platform), and major performance gains such as Native AOT and single-file publishing for faster startup and easier distribution across platforms.</p>
<p>He underscored China’s importance as .NET’s second-largest market, with roughly 13% of users, and noted that generative AI usage in China has doubled in 2025. The local community is seeing strong momentum around ML.NET, Aspire, and the C# Dev Kit in VS Code. Reflecting on his Baby Smash game written 20 years ago, which now runs cross-platform on .NET 10, he called on developers to modernize: move existing Web, WinForms, and WPF apps to the cloud, improve performance, ship as a single executable, and weave in AI capabilities.</p>
<p>On AI, he emphasized a human-centered stance: AI and agents should augment, not replace, developers. In the future, developers will orchestrate and govern agents, and human judgment will matter more than ever. He closed by thanking the open-source community for its many proposals and pull requests, stressing that .NET is an open-source platform built together by Microsoft and the community, and wishing everyone an inspiring conference and a joyful journey with .NET 10.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/2.png" alt="2" /></p>
<h3>Roundtable Discussion</h3>
<p>The roundtable discussion, titled “Empowering with AI, Breaking Through Cross-Platform Barriers, and Ecosystem Innovation,” focused on practical implementation. It explored typical paths for large models and intelligent agents in enterprises, key considerations for choosing cross-platform UI frameworks, and the evolution of these frameworks. Panelists discussed questions like: How can AI capabilities be integrated into existing business processes instead of creating an “experimental” pipeline? How should cross-platform solutions be evaluated in terms of performance, ecosystem, and team skillsets? What are the unique opportunities for domestic ecosystems in the global tech landscape? And how can community collaboration help developers quickly adopt best practices? A shared consensus emerged: in the short term, focus on running scenarios; in the long term, return to engineering fundamentals. Both toolchains and methodologies are equally important.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/21.png" alt="" /></p>
<h3>In-Depth Sessions</h3>
<p>The afternoon featured four breakout sessions, covering a wide range of topics with deep dives into both foundational technologies and real-world project reviews:</p>
<ul>
<li><strong>Frontend and Cross-Platform:</strong> Focused on the progress of Avalonia, Blazor, and WebAssembly, as well as the integrated experience of Aspire in multi-service applications. Speakers shared insights on reusing core logic between desktop and web, shortening cold start times with incremental compilation and resource trimming, and performance profiling and optimization in WASM scenarios.</li>
<li><strong>AI Agents and Enterprise Adoption:</strong> Discussed multi-agent orchestration, the MCP plugin ecosystem, and enterprise data compliance. From common pitfalls of “demo-level” AI to the “five-step method” for moving from POC to production, the session covered use cases like knowledge retrieval, process automation, intelligent customer service, and developer assistants, emphasizing evaluation metrics, prompt engineering, and monitoring governance.</li>
<li><strong>.NET Practices and Engineering:</strong> Focused on the latest capabilities and performance practices of EF Core, the boundaries of NativeAOT, automated testing strategies, and observability implementation. Discussions included database migration strategies, caching and concurrency control for hot paths, end-to-end tracing, and structured logging.</li>
<li><strong>Solutions and Case Studies:</strong> From Clean Architecture/DDD to AI-powered business evolution, topics included application modernization, SaaS transformation, and edge-cloud collaboration in AIoT. Speakers broke down modular governance, team collaboration, and release strategies for complex systems, putting “delivering value continuously” at the center stage.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/3.png" alt="" /></p>
<h2>ABP Booth Highlights: Showcases, Conversations, and Fun</h2>
<p>The story of ABP began with a promise to create a better starting point. From the frustration of “copy-pasting boilerplate code,” we crafted a modular, opinionated framework. We chose open source and community collaboration. We founded Volosoft to turn our vision into reality with professional tools. Today, tens of thousands of developers explore the ABP framework, and thousands of teams rely on the ABP platform to deliver production-grade .NET applications faster and more securely.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/4.png" alt="" /></p>
<p>At .NET Conf China 2025, we brought our “developer platform built for developers” to every visitor. Our booth demonstrations started with “a production-ready skeleton from the start”: modular layered architecture, built-in authentication and authorization systems, multi-tenancy support, audit logging, and localization—all out of the box. On the frontend and backend, ABP offers diverse options like MVC, Blazor, and Angular, enabling teams to quickly implement solutions on familiar stacks while maintaining flexibility for future evolution. We also showcased how ABP integrates with containerization, CI/CD, and observability, emphasizing “engineering built into the framework, not reinvented by every team.”</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/42.png" alt="" /></p>
<p><strong>Interaction and Prizes:</strong> Sharing technology should also be warm and engaging. We hosted a QR code raffle at the booth, with prizes including ABP stickers, the book <em>Mastering ABP Framework</em>, and Bluetooth headphones. Multiple rounds of raffles and group photos made the interactions more memorable. Many developers shared their ABP experiences and plans for improvement right at the booth, and a few impromptu “code walkthroughs” naturally happened. The love and joy for technology were captured in every handshake and discussion.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/41.png" alt="" /></p>
<h2>Looking Ahead: Building the Ecosystem Together</h2>
<p>From an open-source journey to a complete development platform for the future, we’ve always believed that developers deserve a better starting point. Around performance, intelligence, and cross-platform capabilities, we will continue investing in engineering, ecosystem collaboration, and best practice sharing. We also welcome more partners to contribute through documentation and examples, share your experiences, and submit your ideas. Together, let’s make “useful infrastructure” more stable, efficient, and business-friendly.</p>
<p>We look forward to exchanging ideas, sharing practices, and building the ecosystem together at the next gathering. Technology meets creativity, and the possibilities are endless. We’re on the road and waiting for you at the next event.</p>
<p>See you next year at .NET Conf China 2026!</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-11-30-NET-Conf-China-2025/images/5.png" alt="" /></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1df584-3c49-1c2c-0e23-c1faa45ca0a9" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1df584-3c49-1c2c-0e23-c1faa45ca0a9" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/keep-track-of-your-users-in-an-asp.net-core-application-jlt1fxvb</guid>
      <link>https://abp.io/community/posts/keep-track-of-your-users-in-an-asp.net-core-application-jlt1fxvb</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Keep Track of Your Users in an ASP.NET Core Application</title>
      <description>Tracking what users do in your app matters for security, debugging, and business insights. Doing it by hand usually means lots of boilerplate: managing request context, logging operations, tracking entity changes, and more. It adds complexity and makes mistakes more likely.

</description>
      <pubDate>Wed, 03 Sep 2025 07:46:36 Z</pubDate>
      <a10:updated>2026-09-25T22:54:11Z</a10:updated>
      <content:encoded><![CDATA[<h1>Keep Track of Your Users in an ASP.NET Core Application</h1>
<p>Tracking what users do in your app matters for security, debugging, and business insights. Doing it by hand usually means lots of boilerplate: managing request context, logging operations, tracking entity changes, and more. It adds complexity and makes mistakes more likely.</p>
<h2>Why Applications Need Audit Logs</h2>
<p>Audit logs are time-ordered records that show what happened in your app.</p>
<p>A good audit log should capture details for every web request, including:</p>
<h3>1. Request and Response Details</h3>
<ul>
<li>Basic info like <strong>URL, HTTP method, browser</strong>, and <strong>HTTP status code</strong></li>
<li>Network info like <strong>client IP address</strong> and <strong>user agent</strong></li>
<li><strong>Request parameters</strong> and <strong>response content</strong> when needed</li>
</ul>
<h3>2. Operations Performed</h3>
<ul>
<li><strong>Controller actions</strong> and <strong>application service method calls</strong> with parameters</li>
<li><strong>Execution time</strong> and <strong>duration</strong> for performance tracking</li>
<li><strong>Call chains</strong> and <strong>dependencies</strong> where helpful</li>
</ul>
<h3>3. Entity Changes</h3>
<ul>
<li><strong>Entity changes</strong> that happen during requests</li>
<li><strong>Property-level changes</strong>, with old and new values</li>
<li><strong>Change types</strong> (create, update, delete) and timestamps</li>
</ul>
<h3>4. Exception Information</h3>
<ul>
<li><strong>Errors and exceptions</strong> during request execution</li>
<li><strong>Exception stack traces</strong> and <strong>error context</strong></li>
<li>Clear records of failed operations</li>
</ul>
<h3>5. Request Duration</h3>
<ul>
<li>Key metrics for <strong>measuring performance</strong></li>
<li><strong>Finding bottlenecks</strong> and optimization opportunities</li>
<li>Useful data for <strong>monitoring system health</strong></li>
</ul>
<h2>The Challenge with Doing It by Hand</h2>
<p>In ASP.NET Core, developers often use middleware or MVC filters for tracking. Here’s what that looks like and the common problems you’ll hit.</p>
<h3>Using Middleware</h3>
<p>Middleware are components in the ASP.NET Core pipeline that run during request processing.</p>
<p>Manual tracking typically requires:</p>
<ul>
<li>Writing custom middleware to intercept HTTP requests</li>
<li>Extracting user info (user ID, username, IP address, and so on)</li>
<li>Recording request start time and execution duration</li>
<li>Handling both success and failure cases</li>
<li>Saving audit data to logs or a database</li>
</ul>
<h3>Tracking Inside Business Methods</h3>
<p>In your business code, you also need to:</p>
<ul>
<li>Log the start and end of important operations</li>
<li>Capture errors and related context</li>
<li>Link business operations to the request-level audit data</li>
<li>Make sure you track all critical actions</li>
</ul>
<h3>Problems with Manual Tracking</h3>
<p>Manual tracking has some big downsides:</p>
<p><strong>Code duplication and maintenance pain</strong>: Each controller ends up repeating similar tracking logic. Changing the rules means touching many places, and it’s easy to miss some.</p>
<p><strong>Consistency and reliability issues</strong>: Different people implement tracking differently. Exception paths are easy to forget. It’s hard to ensure complete coverage.</p>
<p><strong>Performance and scalability concerns</strong>: Homegrown tracking can slow the app if not designed well. Tuning and extending it takes effort.</p>
<p><strong>Entity change tracking is especially hard</strong>. It often requires:</p>
<ul>
<li>Recording original values before updates</li>
<li>Comparing old and new values for each property</li>
<li>Handling complex types, collections, and navigation properties</li>
<li>Designing and saving change records</li>
<li>Capturing data even when exceptions happen</li>
</ul>
<p>This usually leads to:</p>
<ul>
<li><strong>A lot of code</strong> in every update method</li>
<li><strong>Easy-to-miss edge cases</strong> and subtle bugs</li>
<li><strong>High maintenance</strong> when entity models change</li>
<li><strong>Extra queries and comparisons</strong> that can hurt performance</li>
<li><strong>Incomplete coverage</strong> for complex scenarios</li>
</ul>
<h2>ABP Framework’s Built-in Solution</h2>
<p>ABP Framework includes a built-in audit logging system. It solves the problems above and adds useful features on top.</p>
<h3>Simple Setup vs. Manual Tracking</h3>
<p>Instead of writing lots of code, you configure it once:</p>
<pre><code class="language-csharp">// Configure audit log options in the module's ConfigureServices method
Configure&lt;AbpAuditingOptions&gt;(options =&gt;
{
    options.IsEnabled = true; // Enable audit log system (default value)
    options.IsEnabledForAnonymousUsers = true; // Track anonymous users (default value)
    options.IsEnabledForGetRequests = false; // Skip GET requests (default value)
    options.AlwaysLogOnException = true; // Always log on errors (default value)
    options.HideErrors = true; // Hide audit log errors (default value)
    options.EntityHistorySelectors.AddAllEntities(); // Track all entity changes
});
</code></pre>
<pre><code class="language-csharp">// Add middleware in the module's OnApplicationInitialization method
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    var app = context.GetApplicationBuilder();
    
    // Add audit log middleware - one line of code solves all problems!
    app.UseAuditing();
}
</code></pre>
<p>By contrast, manual tracking needs middleware, controller logic, exception handling, and often hundreds of lines. With ABP, a couple of lines enable it and it just works.</p>
<h2>What You Get with ABP</h2>
<p>Here’s how ABP removes tracking code from your application and still captures what you need.</p>
<h3>1. Application Services: No Tracking Code</h3>
<p>Manual approach: You’d log inside each method and still risk missing cases.</p>
<p>ABP approach: Tracking is automatic—no tracking code in your methods.</p>
<pre><code class="language-csharp">public class BookAppService : ApplicationService
{
    private readonly IRepository&lt;Book, Guid&gt; _bookRepository;
    private readonly IRepository&lt;Author, Guid&gt; _authorRepository;
    
    [Authorize(BookPermissions.Create)]
    public virtual async Task&lt;BookDto&gt; CreateAsync(CreateBookDto input)
    {
        // No need to write any tracking code!
        // ABP automatically tracks:
        // - Method calls and parameters
        // - Calling user
        // - Execution duration
        // - Any exceptions thrown
        
        var author = await _authorRepository.GetAsync(input.AuthorId);
        var book = new Book(input.Title, author, input.Price);
        
        await _bookRepository.InsertAsync(book);
        
        return ObjectMapper.Map&lt;Book, BookDto&gt;(book);
    }
    
    [Authorize(BookPermissions.Update)]
    public virtual async Task&lt;BookDto&gt; UpdateAsync(Guid id, UpdateBookDto input)
    {
        var book = await _bookRepository.GetAsync(id);
        
        // No need to write any entity change tracking code!
        // ABP automatically tracks entity changes:
        // - Which properties changed
        // - Old and new values
        // - When the change happened
        
        book.ChangeTitle(input.Title);
        book.ChangePrice(input.Price);
        
        await _bookRepository.UpdateAsync(book);
        
        return ObjectMapper.Map&lt;Book, BookDto&gt;(book);
    }
}
</code></pre>
<p>With manual code, each method might need 20–30 lines for tracking. With ABP, it’s zero—and you still get richer data.</p>
<p>For entity changes, ABP also saves you from writing comparison code. It handles:</p>
<ul>
<li>Property change detection</li>
<li>Recording old and new values</li>
<li>Complex types and collections</li>
<li>Navigation property changes</li>
<li>All with no extra code to maintain</li>
</ul>
<h3>2. Entity Change Tracking: One Line to Turn It On</h3>
<p>Manual approach: You’d compare properties, serialize complex types, track collection changes, and write to storage.</p>
<p>ABP approach: Mark the entity or select entities globally.</p>
<pre><code class="language-csharp">// Enable audit log for specific entity - one line of code solves all problems!
[Audited]
public class MyEntity : Entity&lt;Guid&gt;
{
    public string Name { get; set; }
    public string Description { get; set; }
    
    [DisableAuditing] // Exclude sensitive data - security control
    public string InternalNotes { get; set; }
}
</code></pre>
<pre><code class="language-csharp">// Or global configuration - batch processing
Configure&lt;AbpAuditingOptions&gt;(options =&gt;
{
    // Track all entities - one line of code tracks all entity changes
    options.EntityHistorySelectors.AddAllEntities();
    
    // Or use custom selector - precise control
    options.EntityHistorySelectors.Add(
        new NamedTypeSelector(
            &quot;MySelectorName&quot;,
            type =&gt; typeof(IEntity).IsAssignableFrom(type)
        )
    );
});
</code></pre>
<h3>3. Extension Features</h3>
<p>Manual approach: Adding custom tracking usually spreads across many places and is hard to test.</p>
<p>ABP approach: Use a contributor for clean, centralized extensions.</p>
<pre><code class="language-csharp">public class MyAuditLogContributor : AuditLogContributor
{
    public override void PreContribute(AuditLogContributionContext context)
    {
        var currentUser = context.ServiceProvider.GetRequiredService&lt;ICurrentUser&gt;();
        
        // Easily add custom properties - manual implementation needs lots of work
        context.AuditInfo.SetProperty(
            &quot;MyCustomClaimValue&quot;,
            currentUser.FindClaimValue(&quot;MyCustomClaim&quot;)
        );
    }
    
    public override void PostContribute(AuditLogContributionContext context)
    {
        // Add custom comments - business logic integration
        context.AuditInfo.Comments.Add(&quot;Some comment...&quot;);
    }
}

// Register contributor - one line of code enables extension features
Configure&lt;AbpAuditingOptions&gt;(options =&gt;
{
    options.Contributors.Add(new MyAuditLogContributor());
});
</code></pre>
<h3>4. Precise Control</h3>
<p>Manual approach: You end up with complex conditional logic.</p>
<p>ABP approach: Use attributes for simple, precise control.</p>
<pre><code class="language-csharp">// Disable audit log for specific controller - precise control
[DisableAuditing]
public class HomeController : AbpController
{
    // Health check endpoints won't be audited - avoid meaningless logs
}

// Disable for specific action - method-level control
public class HomeController : AbpController
{
    [DisableAuditing]
    public async Task&lt;ActionResult&gt; Home()
    {
        // This action won't be audited - public data access
    }
    
    public async Task&lt;ActionResult&gt; OtherActionLogged()
    {
        // This action will be audited - important business operation
    }
}
</code></pre>
<h3>5. Visual Management of Audit Logs</h3>
<p>ABP also provides a UI to browse and inspect audit logs:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/1.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/2.png" alt="" /></p>
<h2>Manual vs. ABP: A Quick Comparison</h2>
<p>The benefits of ABP’s audit log system compared to doing it by hand:</p>
<p>| Aspect | Manual Implementation | ABP Audit Logs |
|--------|----------------------|----------------|
| <strong>Setup Complexity</strong> | High — Write middleware, services, repository code | Low — A few lines of config, works out of the box |
| <strong>Code Maintenance</strong> | High — Tracking code spread across the app | Low — Centralized, convention-based |
| <strong>Consistency</strong> | Variable — Depends on discipline | Consistent — Automated and standardized |
| <strong>Performance</strong> | Risky without careful tuning | Built-in optimizations and scope control |
| <strong>Functionality Completeness</strong> | Basic tracking only | Comprehensive by default |
| <strong>Error Handling</strong> | Easy to miss edge cases | Automatic and reliable |
| <strong>Data Integrity</strong> | Manual effort required | Handled by the framework |
| <strong>Extensibility</strong> | Custom work is costly | Rich extension points |
| <strong>Development Efficiency</strong> | Weeks to build | Minutes to enable |
| <strong>Learning Cost</strong> | Understand many details | Convention-based, low effort |</p>
<h2>Why ABP Audit Logs Matter</h2>
<p>ABP’s audit logging removes the boilerplate from user tracking in ASP.NET Core apps.</p>
<h3>Core Idea</h3>
<p>Manual tracking is error-prone and hard to maintain. ABP gives you a convention-based, automated system that works with minimal setup.</p>
<h3>Key Benefits</h3>
<p>ABP runs by convention, so you don’t need repetitive code. You can control behavior at the request, entity, and method levels. It automatically captures request details, operations, entity changes, and exceptions, and you can extend it with contributors when needed.</p>
<h3>Results in Practice</h3>
<p>| Metric | Manual Implementation | ABP Implementation | Improvement |
|--------|----------------------|-------------------|-------------|
| Development Time | Weeks | Minutes | <strong>99%+</strong> |
| Lines of Code | Hundreds of lines | 2 lines of config | <strong>99%+</strong> |
| Maintenance Cost | High | Low | <strong>Significant</strong> |
| Functionality Completeness | Basic | Comprehensive | <strong>Significant</strong> |
| Error Rate | Higher risk | Lower risk | <strong>Improved</strong> |</p>
<h3>Recommendation</h3>
<p>If you need audit logs, start with ABP’s built-in system. It reduces effort, improves consistency, and stays flexible as your app grows. You can focus on your business logic and let the framework handle the infrastructure.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/audit-logging">ABP Audit Logging</a></li>
<li><a href="https://abp.io/modules/Volo.AuditLogging.Ui">ABP Audit Logging UI</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c20b7-8f9c-a26b-f5df-63b39e6b99bd" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c20b7-8f9c-a26b-f5df-63b39e6b99bd" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/app-services-vs-domain-services-deep-dive-into-two-core-service-types-in-abp-framework-4dvau41u</guid>
      <link>https://abp.io/community/posts/app-services-vs-domain-services-deep-dive-into-two-core-service-types-in-abp-framework-4dvau41u</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>App Services vs Domain Services: Deep Dive into Two Core Service Types in ABP Framework</title>
      <description>In ABP's layered architecture, we frequently encounter two types of services that appear similar but serve distinctly different purposes: Application Services and Domain Services. Understanding the differences between them is crucial for building clear and maintainable enterprise applications.</description>
      <pubDate>Mon, 25 Aug 2025 11:49:04 Z</pubDate>
      <a10:updated>2026-09-26T00:13:07Z</a10:updated>
      <content:encoded><![CDATA[<h1>App Services vs Domain Services: Deep Dive into Two Core Service Types in ABP Framework</h1>
<p>In ABP's layered architecture, we frequently encounter two types of services that appear similar but serve distinctly different purposes: Application Services and Domain Services. Understanding the differences between them is crucial for building clear and maintainable enterprise applications.</p>
<h2>Architectural Positioning</h2>
<p>In ABP's layered architecture:</p>
<ul>
<li><strong>Application Services</strong> reside in the application layer and are responsible for coordinating use case execution</li>
<li><strong>Domain Services</strong> reside in the domain layer and are responsible for implementing core business logic</li>
</ul>
<p>This layered design follows Domain-Driven Design (DDD) principles, ensuring clear separation of business logic and system maintainability.</p>
<h2>Application Services: Use Case Orchestrators</h2>
<h3>Core Responsibilities</h3>
<p>Application Services are stateless services primarily used to implement application use cases. They act as a bridge between the presentation layer and domain layer, responsible for:</p>
<ul>
<li><strong>Parameter Validation</strong>: Input validation is automatically handled by ABP using data annotations</li>
<li><strong>Authorization</strong>: Checking user permissions and access control using <code>[Authorize]</code> attribute or manual authorization checks via <code>IAuthorizationService</code></li>
<li><strong>Transaction Management</strong>: Methods automatically run as Unit of Work (transactional by default)</li>
<li><strong>Use Case Orchestration</strong>: Organizing and coordinating multiple domain objects to complete specific business use cases</li>
<li><strong>Data Transformation</strong>: Handling conversion between DTOs and domain objects using ObjectMapper</li>
</ul>
<h3>Design Principles</h3>
<ol>
<li><strong>DTO Boundaries</strong>: Application service methods should only accept and return DTOs, never directly expose domain entities</li>
<li><strong>Use Case Oriented</strong>: Each method should correspond to a clear user use case</li>
<li><strong>Thin Layer Design</strong>: Avoid implementing complex business logic in application services</li>
</ol>
<h3>Typical Execution Flow</h3>
<p>A standard application service method typically follows this pattern:</p>
<pre><code class="language-csharp">[Authorize(BookPermissions.Create)] // Declarative authorization
public virtual async Task&lt;BookDto&gt; CreateBookAsync(CreateBookDto input) // input is automatically validated
{
    // Get related data
    var author = await _authorRepository.GetAsync(input.AuthorId);
    
    // Call domain service to execute business logic (if needed)
    // You can also use the entity constructor directly if no complex business logic is required
    var book = await _bookManager.CreateAsync(input.Title, author, input.Price);
    
    // Persist changes
    await _bookRepository.InsertAsync(book);
    
    // Return DTO
    return ObjectMapper.Map&lt;Book, BookDto&gt;(book);
}
</code></pre>
<h3>Integration Services: Special kind of Application Service</h3>
<p>It's worth mentioning that ABP also provides a special type of application service—Integration Services. They are application services marked with the <code>[IntegrationService]</code> attribute, designed for inter-module or inter-microservice communication.</p>
<p>We have a community article dedicated to integration services: <a href="https://abp.io/community/articles/integration-services-explained-what-they-are-when-to-use-lienmsy8">Integration Services Explained — What they are, when to use them, and how they behave</a></p>
<h2>Domain Services: Guardians of Business Logic</h2>
<h3>Core Responsibilities</h3>
<p>Domain Services implement core business logic and are particularly needed when:</p>
<ul>
<li><strong>Core domain logic depends on services</strong>: You need to implement logic that requires repositories or other external services</li>
<li><strong>Logic spans multiple aggregates</strong>: The business logic is related to more than one aggregate/entity and doesn't properly fit in any single aggregate</li>
<li><strong>Complex business rules</strong>: Complex domain rules that don't naturally belong in a single entity</li>
</ul>
<h3>Design Principles</h3>
<ol>
<li><strong>Domain Object Interaction</strong>: Method parameters and return values should be domain objects (entities, value objects), never DTOs</li>
<li><strong>Business Logic Focus</strong>: Focus on implementing pure business rules</li>
<li><strong>Stateless Design</strong>: Maintain the stateless nature of services</li>
<li><strong>State-Changing Operations Only</strong>: Domain services should only define methods that mutate data, not query methods</li>
<li><strong>No Authorization Logic</strong>: Domain services should not perform authorization checks or depend on current user context</li>
<li><strong>Specific Method Names</strong>: Use descriptive, business-meaningful method names (e.g., <code>AssignToAsync</code>) instead of generic names (e.g., <code>UpdateAsync</code>)</li>
</ol>
<h3>Implementation Example</h3>
<pre><code class="language-csharp">public class IssueManager : DomainService
{
    private readonly IRepository&lt;Issue, Guid&gt; _issueRepository;
    
    public virtual async Task AssignToAsync(Issue issue, Guid userId)
    {
        // Business rule: Check user's unfinished task count
        var openIssueCount = await _issueRepository.GetCountAsync(i =&gt; i.AssignedUserId == userId &amp;&amp; !i.IsClosed);
            
        if (openIssueCount &gt;= 3)
        {
            throw new BusinessException(&quot;IssueTracking:ConcurrentOpenIssueLimit&quot;);
        }
        
        // Execute assignment logic
        issue.AssignedUserId = userId;
        issue.AssignedDate = Clock.Now;
    }
}
</code></pre>
<h2>Key Differences Comparison</h2>
<p>| Dimension | Application Services | Domain Services |
|-----------|---------------------|-----------------|
| <strong>Layer Position</strong> | Application Layer | Domain Layer |
| <strong>Primary Responsibility</strong> | Use Case Orchestration | Business Logic Implementation |
| <strong>Data Interaction</strong> | DTOs | Domain Objects |
| <strong>Callers</strong> | Presentation Layer/Client Applications | Application Services/Other Domain Services |
| <strong>Authorization</strong> | Responsible for permission checks | No authorization logic |
| <strong>Transaction Management</strong> | Manages transaction boundaries (Unit of Work) | Participates in transactions but doesn't manage |
| <strong>Current User Context</strong> | Can access current user information | Should not depend on current user context |
| <strong>Return Types</strong> | Returns DTOs | Returns domain objects only |
| <strong>Query Operations</strong> | Can perform query operations | Should not define GET/query methods |
| <strong>Naming Convention</strong> | <code>*AppService</code> | <code>*Manager</code> or <code>*Service</code> |</p>
<h2>Collaboration Patterns in Practice</h2>
<p>In real-world development, these two types of services typically work together:</p>
<pre><code class="language-csharp">// Application Service
public class BookAppService : ApplicationService
{
    private readonly BookManager _bookManager;
    private readonly IRepository&lt;Book&gt; _bookRepository;
    
    [Authorize(BookPermissions.Update)]
    public virtual async Task&lt;BookDto&gt; UpdatePriceAsync(Guid id, decimal newPrice)
    {
        var book = await _bookRepository.GetAsync(id);

        await _bookManager.ChangePriceAsync(book, newPrice);
        
        await _bookRepository.UpdateAsync(book);
        
        return ObjectMapper.Map&lt;Book, BookDto&gt;(book);
    }
}

// Domain Service
public class BookManager : DomainService
{
    public virtual async Task ChangePriceAsync(Book book, decimal newPrice)
    {
        // Domain service focuses on business rules
        if (newPrice &lt;= 0)
        {
            throw new BusinessException(&quot;Book:InvalidPrice&quot;);
        }
        
        if (book.IsDiscounted &amp;&amp; newPrice &gt; book.OriginalPrice)
        {
            throw new BusinessException(&quot;Book:DiscountedPriceCannotExceedOriginal&quot;);
        }

        if (book.Price == newPrice)
        {
            return;
        }

        // Additional business logic: Check if price change requires approval
        if (await RequiresApprovalAsync(book, newPrice))
        {
            throw new BusinessException(&quot;Book:PriceChangeRequiresApproval&quot;);
        }

        book.ChangePrice(newPrice);
    }
    
    private Task&lt;bool&gt; RequiresApprovalAsync(Book book, decimal newPrice)
    {
        // Example business rule: Large price increases require approval
        var increasePercentage = ((newPrice - book.Price) / book.Price) * 100;
        return Task.FromResult(increasePercentage &gt; 50); // 50% increase threshold
    }
}
</code></pre>
<h2>Best Practice Recommendations</h2>
<h3>Application Services</h3>
<ul>
<li>Create a corresponding application service for each aggregate root</li>
<li>Use clear naming conventions (e.g., <code>IBookAppService</code>)</li>
<li>Implement standard CRUD operation methods (<code>GetAsync</code>, <code>CreateAsync</code>, <code>UpdateAsync</code>, <code>DeleteAsync</code>)</li>
<li>Avoid inter-application service calls within the same module/application</li>
<li>Always return DTOs, never expose domain entities directly</li>
<li>Use the <code>[Authorize]</code> attribute for declarative authorization or manual checks via <code>IAuthorizationService</code></li>
<li>Methods automatically run as Unit of Work (transactional)</li>
<li>Input validation is handled automatically by ABP</li>
</ul>
<h3>Domain Services</h3>
<ul>
<li>Use the <code>Manager</code> suffix for naming (e.g., <code>BookManager</code>)</li>
<li>Only define state-changing methods, avoid query methods (use repositories directly in Application Services for queries)</li>
<li>Throw <code>BusinessException</code> with clear, unique error codes for domain validation failures</li>
<li>Keep methods pure, avoid involving user context or authorization logic</li>
<li>Accept and return domain objects only, never DTOs</li>
<li>Use descriptive, business-meaningful method names (e.g., <code>AssignToAsync</code>, <code>ChangePriceAsync</code>)</li>
<li>Do not implement interfaces unless there's a specific need for multiple implementations</li>
</ul>
<h2>Summary</h2>
<p>Application Services and Domain Services each have their distinct roles in the ABP framework: Application Services serve as use case orchestrators, handling authorization, validation, transaction management, and DTO transformations; Domain Services focus purely on business logic implementation without any infrastructure concerns. Integration Services are a special type of Application Service designed for inter-service communication.</p>
<p>Correctly understanding and applying these service patterns is key to building high-quality ABP applications. Through clear separation of responsibilities, we can not only build more maintainable code but also flexibly switch between monolithic and microservice architectures—this is precisely the elegance of ABP framework design.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services">Application Services</a></li>
<li><a href="https://abp.io/docs/latest/framework/api-development/integration-services">Integration Services</a></li>
<li><a href="https://abp.io/docs/latest/framework/architecture/domain-driven-design/domain-services">Domain Services</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1bf33c-52fe-e4d6-e564-cfec296d3f65" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1bf33c-52fe-e4d6-e564-cfec296d3f65" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-hangfire-dashboard-in-abp-api-website--r32ox497</guid>
      <link>https://abp.io/community/posts/using-hangfire-dashboard-in-abp-api-website--r32ox497</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>hangfire</category>
      <category>background-jobs</category>
      <title>Using Hangfire Dashboard in ABP API Website 🚀</title>
      <description>In this article, I'll show you how to integrate and use the Hangfire Dashboard in an ABP API website.

</description>
      <pubDate>Fri, 20 Jun 2025 08:12:31 Z</pubDate>
      <a10:updated>2026-09-26T01:06:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using Hangfire Dashboard in ABP API Website 🚀</h1>
<h2>Introduction</h2>
<p>In this article, I'll show you how to integrate and use the Hangfire Dashboard in an ABP API website.</p>
<p>Typically, API websites use <code>JWT Bearer</code> authentication, but the Hangfire Dashboard isn't compatible with <code>JWT Bearer</code> authentication. Therefore, we need to implement <code>Cookies</code> and <code>OpenIdConnect</code> authentication for the Hangfire Dashboard access.</p>
<h2>Creating a New ABP Demo Project 🛠️</h2>
<p>We'll create a new ABP Demo <code>Tiered</code> project that includes <code>AuthServer</code>, <code>API</code>, and <code>Web</code> projects.</p>
<pre><code class="language-bash">abp new AbpHangfireDemoApp -t app --tiered
</code></pre>
<p>Now let's add the Hangfire Dashboard to the <code>API</code> project and configure it to use <code>Cookies</code> and <code>OpenIdConnect</code> authentication for accessing the dashboard.</p>
<h2>Adding a New Hangfire Application 🔧</h2>
<p>We need to add a new Hangfire application to the <code>appsettings.json</code> file in the <code>DbMigrator</code> project:</p>
<blockquote>
<p><strong>Note:</strong> Replace <code>44371</code> with your <code>API</code> project's port.</p>
</blockquote>
<pre><code class="language-json">&quot;OpenIddict&quot;: {
    &quot;Applications&quot;: {
        //...
        &quot;AbpHangfireDemoApp_Hangfire&quot;: {
            &quot;ClientId&quot;: &quot;AbpHangfireDemoApp_Hangfire&quot;,
            &quot;RootUrl&quot;: &quot;https://localhost:44371/&quot;
        }
        //...
    }
}
</code></pre>
<ol start="2">
<li>Update the <code>OpenIddictDataSeedContributor</code>'s <code>CreateApplicationsAsync</code> method in the <code>Domain</code> project to seed the new Hangfire application.</li>
</ol>
<pre><code class="language-csharp"> //Hangfire Client
var hangfireClientId = configurationSection[&quot;AbpHangfireDemoApp_Hangfire:ClientId&quot;];
if (!hangfireClientId.IsNullOrWhiteSpace())
{
    var hangfireClientRootUrl = configurationSection[&quot;AbpHangfireDemoApp_Hangfire:RootUrl&quot;]!.EnsureEndsWith('/');

    await CreateApplicationAsync(
        applicationType: OpenIddictConstants.ApplicationTypes.Web,
        name: hangfireClientId!,
        type: OpenIddictConstants.ClientTypes.Confidential,
        consentType: OpenIddictConstants.ConsentTypes.Implicit,
        displayName: &quot;Hangfire Application&quot;,
        secret: configurationSection[&quot;AbpHangfireDemoApp_Hangfire:ClientSecret&quot;] ?? &quot;1q2w3e*&quot;,
        grantTypes: new List&lt;string&gt; //Hybrid flow
        {
            OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit
        },
        scopes: commonScopes,
        redirectUris: new List&lt;string&gt; { $&quot;{hangfireClientRootUrl}signin-oidc&quot; },
        postLogoutRedirectUris: new List&lt;string&gt; { $&quot;{hangfireClientRootUrl}signout-callback-oidc&quot; },
        clientUri: hangfireClientRootUrl,
        logoUri: &quot;/images/clients/aspnetcore.svg&quot;
    );
}
</code></pre>
<ol start="3">
<li>Run the <code>DbMigrator</code> project to seed the new Hangfire application.</li>
</ol>
<h3>Adding Hangfire Dashboard to the <code>API</code> Project 📦</h3>
<ol>
<li>Add the following packages and modules dependencies to the <code>API</code> project:</li>
</ol>
<pre><code class="language-bash">&lt;PackageReference Include=&quot;Volo.Abp.BackgroundJobs.HangFire&quot; Version=&quot;9.2.0&quot; /&gt;
&lt;PackageReference Include=&quot;Volo.Abp.AspNetCore.Authentication.OpenIdConnect&quot; Version=&quot;9.2.0&quot; /&gt;
&lt;PackageReference Include=&quot;Hangfire.SqlServer&quot; Version=&quot;1.8.20&quot; /&gt;
</code></pre>
<pre><code class="language-cs">typeof(AbpBackgroundJobsHangfireModule),
typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule)
</code></pre>
<ol start="2">
<li>Add the <code>HangfireClientId</code> and <code>HangfireClientSecret</code> to the <code>appsettings.json</code> file in the <code>API</code> project:</li>
</ol>
<pre><code class="language-csharp">&quot;AuthServer&quot;: {
    &quot;Authority&quot;: &quot;https://localhost:44358&quot;,
    &quot;RequireHttpsMetadata&quot;: true,
    &quot;MetaAddress&quot;: &quot;https://localhost:44358&quot;,
    &quot;SwaggerClientId&quot;: &quot;AbpHangfireDemoApp_Swagger&quot;,
    &quot;HangfireClientId&quot;: &quot;AbpHangfireDemoApp_Hangfire&quot;,
    &quot;HangfireClientSecret&quot;: &quot;1q2w3e*&quot;
}
</code></pre>
<ol start="3">
<li>Add the <code>ConfigureHangfire</code> method to the <code>API</code> project to configure Hangfire:</li>
</ol>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();
    var hostingEnvironment = context.Services.GetHostingEnvironment();

    //...

    //Add Hangfire
    ConfigureHangfire(context, configuration);
    //...
}

private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddHangfire(config =&gt;
    {
        config.UseSqlServerStorage(configuration.GetConnectionString(&quot;Default&quot;));
    });
}
</code></pre>
<ol start="4">
<li>Modify the <code>ConfigureAuthentication</code> method to add new <code>Cookies</code> and <code>OpenIdConnect</code> authentication schemes:</li>
</ol>
<pre><code class="language-csharp">private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddAbpJwtBearer(options =&gt;
        {
            options.Authority = configuration[&quot;AuthServer:Authority&quot;];
            options.RequireHttpsMetadata = configuration.GetValue&lt;bool&gt;(&quot;AuthServer:RequireHttpsMetadata&quot;);
            options.Audience = &quot;AbpHangfireDemoApp&quot;;

            options.ForwardDefaultSelector = httpContext =&gt; httpContext.Request.Path.StartsWithSegments(&quot;/hangfire&quot;, StringComparison.OrdinalIgnoreCase)
                ? CookieAuthenticationDefaults.AuthenticationScheme
                : null;
        })
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =&gt;
        {
            options.Authority = configuration[&quot;AuthServer:Authority&quot;];
            options.RequireHttpsMetadata = Convert.ToBoolean(configuration[&quot;AuthServer:RequireHttpsMetadata&quot;]);
            options.ResponseType = OpenIdConnectResponseType.Code;

            options.ClientId = configuration[&quot;AuthServer:HangfireClientId&quot;];
            options.ClientSecret = configuration[&quot;AuthServer:HangfireClientSecret&quot;];

            options.UsePkce = true;
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;

            options.Scope.Add(&quot;roles&quot;);
            options.Scope.Add(&quot;email&quot;);
            options.Scope.Add(&quot;phone&quot;);
            options.Scope.Add(&quot;AbpHangfireDemoApp&quot;);

            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        });

    //...
}
</code></pre>
<ol start="5">
<li>Add a custom middleware and <code>UseAbpHangfireDashboard</code> after <code>UseAuthorization</code> in the <code>OnApplicationInitialization</code> method:</li>
</ol>
<pre><code class="language-csharp">//...
app.UseAuthorization();

app.Use(async (httpContext, next) =&gt;
{
    if (httpContext.Request.Path.StartsWithSegments(&quot;/hangfire&quot;, StringComparison.OrdinalIgnoreCase))
    {
        var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            await httpContext.ChallengeAsync(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties
                {
                    RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString
                });
            return;
        }
    }
    await next.Invoke();
});
app.UseAbpHangfireDashboard(&quot;/hangfire&quot;, options =&gt;
{
    options.AsyncAuthorization = new[]
    {
        new AbpHangfireAuthorizationFilter()
    };
});

//...
</code></pre>
<p>Perfect! 🎉 Now you can run the <code>AuthServer</code> and <code>API</code> projects and access the Hangfire Dashboard at <code>https://localhost:44371/hangfire</code>.</p>
<blockquote>
<p><strong>Note:</strong> Replace <code>44371</code> with your <code>API</code> project's port.</p>
</blockquote>
<p>The first time you access the Hangfire Dashboard, you'll be redirected to the login page of the <code>AuthServer</code> project. After you log in, you'll be redirected back to the Hangfire Dashboard.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif" alt="Hangfire Dashboard" /></p>
<h2>Key Points 🔑</h2>
<h3>1. Authentication Scheme Selection</h3>
<p>The default authentication scheme in API websites is <code>JWT Bearer</code>. We've implemented <code>Cookies</code> and <code>OpenIdConnect</code> specifically for the Hangfire Dashboard.</p>
<p>We've configured the <code>JwtBearerOptions</code>'s <code>ForwardDefaultSelector</code> to use <code>CookieAuthenticationDefaults.AuthenticationScheme</code> for Hangfire Dashboard requests.</p>
<p>This means that if the request path starts with <code>/hangfire</code>, the request will be authenticated using the <code>Cookies</code> authentication scheme; otherwise, it will use the <code>JwtBearer</code> authentication scheme.</p>
<pre><code class="language-csharp">options.ForwardDefaultSelector = httpContext =&gt; httpContext.Request.Path.StartsWithSegments(&quot;/hangfire&quot;, StringComparison.OrdinalIgnoreCase)
    ? CookieAuthenticationDefaults.AuthenticationScheme
    : null;
</code></pre>
<h3>2. Custom Middleware for Authentication</h3>
<p>We've also implemented a custom middleware to handle <code>Cookies</code> authentication for the Hangfire Dashboard. If the current request isn't authenticated with the <code>Cookies</code> authentication scheme, it will be redirected to the login page.</p>
<pre><code class="language-csharp">app.Use(async (httpContext, next) =&gt;
{
    if (httpContext.Request.Path.StartsWithSegments(&quot;/hangfire&quot;, StringComparison.OrdinalIgnoreCase))
    {
        var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        if (!authenticateResult.Succeeded)
        {
            await httpContext.ChallengeAsync(
                OpenIdConnectDefaults.AuthenticationScheme,
                new AuthenticationProperties
                {
                    RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString
                });
            return;
        }
    }
    await next.Invoke();
});
</code></pre>
<h2>References 📚</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire">ABP Hangfire Background Job Manager</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-9.0">Use cookie authentication in ASP.NET Core</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1a9e92-58a8-f2e0-022d-59d68c675e8c" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1a9e92-58a8-f2e0-022d-59d68c675e8c" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/integrating-.net-ai-chat-template-with-abp-framework-qavb5p2j</guid>
      <link>https://abp.io/community/posts/integrating-.net-ai-chat-template-with-abp-framework-qavb5p2j</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Integrating .NET AI Chat Template with ABP Framework</title>
      <description>This article demonstrates how to integrate the .NET AI Chat Template into an ABP Framework application, enabling powerful AI chat capabilities in your ABP-based solution.</description>
      <pubDate>Fri, 30 May 2025 11:27:59 Z</pubDate>
      <a10:updated>2026-09-26T01:23:12Z</a10:updated>
      <content:encoded><![CDATA[<h1>Integrating .NET AI Chat Template with ABP Framework</h1>
<p>This article demonstrates how to integrate the <a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-ai-template-preview2/">.NET AI Chat Template</a> into an ABP Framework application, enabling powerful AI chat capabilities in your ABP-based solution.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-05-11-AI-Chat/cover.png" alt="cover" /></p>
<h2>Step 1: Create a New ABP Project</h2>
<p>First, let's create a new <a href="https://abp.io/docs/latest/solution-templates/single-layer-web-application/overview">single-layer Blazor Server project</a> named <code>AbpAiChat</code> using ABP Studio, You can also use the following ABP CLI command to create the project:</p>
<pre><code class="language-bash">abp new AbpAiChat -t app-nolayers --ui-framework blazor-server --use-open-source-template
</code></pre>
<h2>Step 2: Integrate AI Chat Template</h2>
<p>The integration process involves copying and adapting the .NET AI Chat Template code into our ABP project. The template code is already included in our sample project, so you don't need to install it separately.</p>
<h3>2.1 Project Structure Changes</h3>
<ol>
<li>Copy Blazor components to the <code>Components</code> folder</li>
<li>Copy AI service classes to the <code>Services</code> folder</li>
<li>Add required entities(<code>IngestedDocument</code>, <code>IngestedRecord</code>) to the <code>AbpAiChatDbContext</code> and add new migration</li>
<li>Copy frontend resources to the <code>wwwroot</code> folder</li>
<li>Adjust some styles to capatible with the ABP theme</li>
</ol>
<h3>2.2 Required NuGet Packages</h3>
<p>Add the following packages to <code>AbpAiChat.csproj</code>:</p>
<pre><code class="language-xml">&lt;PackageReference Include=&quot;Microsoft.Extensions.AI.OpenAI&quot; Version=&quot;9.4.3-preview.1.25230.7&quot; /&gt;
&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.Sqlite&quot; Version=&quot;9.0.4&quot; /&gt;
&lt;PackageReference Include=&quot;Microsoft.Extensions.AI&quot; Version=&quot;9.4.3-preview.1.25230.7&quot; /&gt;
&lt;PackageReference Include=&quot;Microsoft.SemanticKernel.Core&quot; Version=&quot;1.47.0&quot; /&gt;
&lt;PackageReference Include=&quot;PdfPig&quot; Version=&quot;0.1.9&quot; /&gt;
&lt;PackageReference Include=&quot;System.Linq.Async&quot; Version=&quot;6.0.1&quot; /&gt;
</code></pre>
<h3>2.3 Configure AI Services</h3>
<p>Add the following configuration to <code>AbpAiChatModule.cs</code>:</p>
<pre><code class="language-csharp">private void ConfigureAi(ServiceConfigurationContext context)
{
    var credential = new ApiKeyCredential(context.Services.GetConfiguration()[&quot;GitHubToken&quot;] ?? throw new InvalidOperationException(&quot;Missing configuration: GitHubToken. See the README for details.&quot;));
    var openAiOptions = new OpenAIClientOptions()
    {
        Endpoint = new Uri(&quot;https://models.inference.ai.azure.com&quot;)
    };

    var ghModelsClient = new OpenAIClient(credential, openAiOptions);
    var chatClient = ghModelsClient.GetChatClient(&quot;gpt-4o-mini&quot;).AsIChatClient();
    var embeddingGenerator = ghModelsClient.GetEmbeddingClient(&quot;text-embedding-3-small&quot;).AsIEmbeddingGenerator();

    var vectorStore = new JsonVectorStore(Path.Combine(AppContext.BaseDirectory, &quot;vector-store&quot;));

    context.Services.AddSingleton&lt;IVectorStore&gt;(vectorStore);
    context.Services.AddScoped&lt;DataIngestor&gt;();
    context.Services.AddSingleton&lt;SemanticSearch&gt;();
    context.Services.AddChatClient(chatClient).UseFunctionInvocation().UseLogging();
    context.Services.AddEmbeddingGenerator(embeddingGenerator);

    context.Services.Configure&lt;AbpAspNetCoreContentOptions&gt;(options =&gt;
    {
        options.ContentTypeMaps.Add(&quot;.mjs&quot;, &quot;application/javascript&quot;);
    });
}
</code></pre>
<p>The <code>ConfigureAi</code> method is called in the <code>ConfigureServices</code> method of <code>AbpAiChatModule</code>. It sets up the AI services, including the OpenAI client, chat client, embedding generator, and vector store.</p>
<h3>2.4 Configure GitHub Token</h3>
<p>Add your GitHub Personal Access Token to <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
    &quot;GitHubToken&quot;: &quot;your-github-token&quot;
}
</code></pre>
<p>You can obtain your token from <a href="https://github.com/settings/personal-access-tokens">GitHub Personal Access Tokens</a>.</p>
<h2>Step 3: Add Custom AI Functionality</h2>
<p>Let's add a custom AI function to retrieve the current user's information. Update the <code>Chat.razor</code> component:</p>
<pre><code class="language-csharp">chatOptions.Tools =
[
    AIFunctionFactory.Create(SearchAsync),
    AIFunctionFactory.Create(GetWeather),
    AIFunctionFactory.Create(GetCurrentUserInfo)
];

[Description(&quot;Get current user information&quot;)]
private Task&lt;string&gt; GetCurrentUserInfo()
{
    return Task.FromResult(CurrentUser.IsAuthenticated ?
        $&quot;UserId: {CurrentUser.Id}, Name: {CurrentUser.UserName}, Email: {CurrentUser.Email}, Roles: {string.Join(&quot;, &quot;, CurrentUser.Roles)}&quot; :
        &quot;No user information available.&quot;);
}
</code></pre>
<h2>Step 4: Add Navigation</h2>
<p>Add a <code>Chat</code> menu item in <code>AbpAiChatMenuContributor</code> to navigate to the AI Chat component.</p>
<h2>Running the Application</h2>
<p>After completing the integration, you can run the application and access the AI chat functionality. The chat interface allows you to:</p>
<ul>
<li>Get weather information</li>
<li>Ask questions about PDF content</li>
<li>Retrieve current user information</li>
<li>And more!</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-05-11-AI-Chat/ai-chat.png" alt="AI Chat Interface" /></p>
<h2>Conclusion</h2>
<p>This integration demonstrates how to leverage the power of AI in your ABP Framework applications. The .NET AI Chat Template provides a solid foundation for building intelligent chat interfaces, and ABP Framework makes it more powerful.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/solution-templates/single-layer-web-application/overview">ABP Single Layer Solution</a></li>
<li><a href="https://devblogs.microsoft.com/dotnet/announcing-dotnet-ai-template-preview1/">.NET AI Template Documentation</a></li>
<li><a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens">GitHub Personal Access Tokens Guide</a></li>
</ul>
<h2>Source Code</h2>
<ul>
<li><a href="https://github.com/abpframework/abp-samples/tree/master/AIChat">AbpAiChat Source Code</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1a331f-bff5-6fb4-5954-8b2b7f88e6c8" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1a331f-bff5-6fb4-5954-8b2b7f88e6c8" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/resolving-tenant-from-route-in-abp-framework-ah7oru97</guid>
      <link>https://abp.io/community/posts/resolving-tenant-from-route-in-abp-framework-ah7oru97</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>multi-tenancy</category>
      <title>Resolving Tenant from Route in ABP Framework</title>
      <description>The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more.

This article will demonstrate how to resolve tenant information from the route.</description>
      <pubDate>Thu, 22 May 2025 08:59:40 Z</pubDate>
      <a10:updated>2026-09-26T01:15:50Z</a10:updated>
      <content:encoded><![CDATA[<h1>Resolving Tenant from Route in ABP Framework</h1>
<p>The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more.</p>
<p>This article will demonstrate how to resolve tenant information from the route.</p>
<h2>Tenant Information in Routes</h2>
<p>In the ABP Framework, tenant information in routes is handled by the <code>RouteTenantResolveContributor</code>.</p>
<p>Let's say your application is hosted at <code>https://abp.io</code> and you have a tenant named <code>acme</code>. You can add the <code>{__tenant}</code> variable to your controller or page routes like this:</p>
<pre><code class="language-csharp">[Route(&quot;{__tenant}/[Controller]&quot;)]
public class MyController : MyProjectNameController
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(&quot;Hello My Page&quot;);
    }
}
</code></pre>
<pre><code class="language-cshtml">@page &quot;{__tenant?}/mypage&quot;
@model MyPageModel

&lt;html&gt;
&lt;body&gt;
    &lt;h1&gt;My Page&lt;/h1&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>When you access <code>https://abp.io/acme/my</code> or <code>https://abp.io/acme/mypage</code>, ABP will automatically resolve the tenant information from the route.</p>
<h2>Adding __tenant to Global Routes</h2>
<p>While we've shown how to add <code>{__tenant}</code> to individual controllers or pages, you might want to add it globally to your entire application. Here's how to implement this:</p>
<pre><code class="language-cs">using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace MyCompanyName;

public class AddTenantRouteToPages : IPageRouteModelConvention, IApplicationModelConvention
{
    public void Apply(PageRouteModel model)
    {
        var selectorCount = model.Selectors.Count;
        var selectorModels = new List&lt;SelectorModel&gt;();
        for (var i = 0; i &lt; selectorCount; i++)
        {
            var selector = model.Selectors[i];
            selectorModels.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Template = AttributeRouteModel.CombineTemplates(&quot;{__tenant:regex(^[a-zA-Z0-9]+$)}&quot;, selector.AttributeRouteModel!.Template!.RemovePreFix(&quot;/&quot;))
                }
            });
        }
        foreach (var selectorModel in selectorModels)
        {
            model.Selectors.Add(selectorModel);
        }
    }
}

public class AddTenantRouteToControllers :IApplicationModelConvention
{
    public void Apply(ApplicationModel application)
    {
        var controllers = application.Controllers;
        foreach (var controller in controllers)
        {
            var selector = controller.Selectors.FirstOrDefault();
            if (selector == null || selector.AttributeRouteModel == null)
            {
                controller.Selectors.Add(new SelectorModel
                {
                    AttributeRouteModel = new AttributeRouteModel
                    {
                        Template = AttributeRouteModel.CombineTemplates(&quot;{__tenant:regex(^[[a-zA-Z0-9]]+$)}&quot;, controller.ControllerName)
                    }
                });
                controller.Selectors.Add(new SelectorModel
                {
                    AttributeRouteModel = new AttributeRouteModel
                    {
                        Template = controller.ControllerName
                    }
                });
            }
            else
            {
                var template = selector.AttributeRouteModel?.Template;
                template = template.IsNullOrWhiteSpace() ? &quot;{__tenant:regex(^[[a-zA-Z0-9]]+$)}&quot; : AttributeRouteModel.CombineTemplates(&quot;{__tenant:regex(^[[a-zA-Z0-9]]+$)}&quot;, template.RemovePreFix(&quot;/&quot;));
                controller.Selectors.Add(new SelectorModel
                {
                    AttributeRouteModel = new AttributeRouteModel
                    {
                        Template = template
                    }
                });
            }
        }
    }
}
</code></pre>
<p>Register the services:</p>
<pre><code class="language-cs">public override void ConfigureServices(ServiceConfigurationContext context)
{
    //...
    
    PostConfigure&lt;RazorPagesOptions&gt;(options =&gt;
    {
        options.Conventions.Add(new AddTenantRouteToPages());
    });

    PostConfigure&lt;MvcOptions&gt;(options =&gt;
    {
        options.Conventions.Add(new AddTenantRouteToControllers());
    });
    
    // Configure cookie path to prevent authentication cookie loss
    context.Services.ConfigureApplicationCookie(x =&gt;
    {
        x.Cookie.Path = &quot;/&quot;;
    });
    //...
}
</code></pre>
<p>After implementing this, you'll notice that all controllers in your Swagger UI will have the <code>{__tenant}</code> route added:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/swagger-ui.png" alt="Swagger UI" /></p>
<h2>Handling Navigation Links</h2>
<p>To ensure navigation links automatically include tenant information, we need to add middleware that dynamically adds the tenant to the PathBase:</p>
<pre><code class="language-cs">public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    //...
    app.Use(async (httpContext, next) =&gt;
    {
        var tenantMatch = Regex.Match(httpContext.Request.Path, &quot;^/([^/.]+)(?:/.*)?$&quot;);
        if (tenantMatch.Groups.Count &gt; 1 &amp;&amp; !string.IsNullOrEmpty(tenantMatch.Groups[1].Value))
        {
            var tenantName = tenantMatch.Groups[1].Value;
            if (!tenantName.IsNullOrWhiteSpace())
            {
                var tenantStore = httpContext.RequestServices.GetRequiredService&lt;ITenantStore&gt;();
                var tenantNormalizer = httpContext.RequestServices.GetRequiredService&lt;ITenantNormalizer&gt;();
                var tenantInfo = await tenantStore.FindAsync(tenantNormalizer.NormalizeName(tenantName)!);
                if (tenantInfo != null)
                {
                    if (httpContext.Request.Path.StartsWithSegments(new PathString(tenantName.EnsureStartsWith('/')), out var matchedPath, out var remainingPath))
                    {
                        var originalPath = httpContext.Request.Path;
                        var originalPathBase = httpContext.Request.PathBase;
                        httpContext.Request.Path = remainingPath;
                        httpContext.Request.PathBase = originalPathBase.Add(matchedPath);
                        try
                        {
                            await next(httpContext);
                        }
                        finally
                        {
                            httpContext.Request.Path = originalPath;
                            httpContext.Request.PathBase = originalPathBase;
                        }
                        return;
                    }
                }
            }
        }

        await next(httpContext);
    });
    app.UseRouting();
    app.MapAbpStaticAssets();
    //...
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/ui.png" alt="ui" /></p>
<p>After setting the PathBase, we need to add a custom tenant resolver to extract tenant information from the <code>PathBase</code>:</p>
<pre><code class="language-cs">public class MyRouteTenantResolveContributor : RouteTenantResolveContributor
{
    public const string ContributorName = &quot;MyRoute&quot;;

    public override string Name =&gt; ContributorName;

    protected override Task&lt;string?&gt; GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext)
    {
        var tenantId = httpContext.GetRouteValue(context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey) ?? httpContext.Request.PathBase.ToString();
        var tenantIdStr = tenantId?.ToString()?.RemovePreFix(&quot;/&quot;);
        return Task.FromResult(!tenantIdStr.IsNullOrWhiteSpace() ? Convert.ToString(tenantIdStr) : null);
    }
}
</code></pre>
<p>Register the MyRouteTenantResolveContributor with the ABP Framework:</p>
<pre><code class="language-cs">public override void ConfigureServices(ServiceConfigurationContext context)
{
    //...
    Configure&lt;AbpTenantResolveOptions&gt;(options =&gt;
    {
        options.TenantResolvers.Add(new MyRouteTenantResolveContributor());
    });
    //...
}
</code></pre>
<h3>Modifying abp.appPath</h3>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    //...
    context.Services.AddOptions&lt;AbpThemingOptions&gt;().Configure&lt;IServiceProvider&gt;((options, rootServiceProvider) =&gt;
    {
        var currentTenant = rootServiceProvider.GetRequiredService&lt;ICurrentTenant&gt;();
        if (!currentTenant.Name.IsNullOrWhiteSpace())
        {
            options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/');
        }
    });

    context.Services.RemoveAll(x =&gt; x.ServiceType == typeof(IOptions&lt;AbpThemingOptions&gt;));
    context.Services.Add(ServiceDescriptor.Scoped(typeof(IOptions&lt;&gt;), typeof(OptionsManager&lt;&gt;)));
    //...
}
</code></pre>
<p>Browser console output:</p>
<pre><code class="language-cs">&gt; https://localhost:44303/acme/
&gt; abp.appPath
&gt; '/acme/'
</code></pre>
<h2>Summary</h2>
<p>By following these steps, you can implement tenant resolution from routes in the ABP Framework and handle navigation links appropriately. This approach provides a clean and maintainable way to manage multi-tenancy in your application.</p>
<h2>References</h2>
<ul>
<li><a href="https://docs.abp.io/en/abp/latest/Multi-Tenancy">ABP Multi-Tenancy</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing">Routing in ASP.NET Core</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base">HTML base tag</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1a0965-14c5-05d8-6d97-6e0fffc75457" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1a0965-14c5-05d8-6d97-6e0fffc75457" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/common-errors-in-jwt-bearer-authentication-4u3wrbs5</guid>
      <link>https://abp.io/community/posts/common-errors-in-jwt-bearer-authentication-4u3wrbs5</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>authentication</category>
      <title>Common Errors in JWT Bearer Authentication</title>
      <description>When implementing JWT Bearer authentication in an ABP(tiered) application, you might occasionally encounter errors starting with IDX. These errors are related to JWT Bearer Token validation and this article will help you understand and resolve them.</description>
      <pubDate>Sun, 20 Apr 2025 10:28:16 Z</pubDate>
      <a10:updated>2026-09-25T20:39:45Z</a10:updated>
      <content:encoded><![CDATA[<h1>Common Errors in JWT Bearer Authentication</h1>
<p>When implementing JWT Bearer authentication in an ABP(tiered) application, you might occasionally encounter errors starting with <code>IDX</code>. These errors are related to JWT Bearer Token validation and this article will help you understand and resolve them.</p>
<h2>Enable JWT Bearer authentication</h2>
<p>Your API project usually contains the following code, which enables JWT Bearer authentication and makes it as the default authentication scheme.</p>
<p>We simply configure the JWT's <code>Authority</code> and <code>Audience</code> properties, and it will work fine.</p>
<pre><code class="language-csharp">context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
	.AddJwtBearer(options =&gt;
	{
		options.Authority = &quot;https://localhost:44301/&quot;; //configuration[&quot;AuthServer:Authority&quot;];
		options.Audience = &quot;MyProjectName&quot;;
	});
</code></pre>
<blockquote>
<p><code>AddJwtBearer</code> and <code>AddAbpJwtBearer</code> will do the same thing, but <code>AddAbpJwtBearer</code> is recommended.</p>
</blockquote>
<h2>JWT authentication process</h2>
<p>Let's take a look at how the above code works.</p>
<p>A JWT Token usually consists of three parts: <code>Header</code>, <code>Payload</code>, and <code>Signature</code>.</p>
<ul>
<li><code>Header</code>: Contains the type and signing algorithm of the token</li>
<li><code>Payload</code>: Contains the claims of the token, including <code>sub</code>, <code>aud</code>, <code>exp</code>, <code>iat</code>, <code>iss</code>, <code>jti</code>, <code>preferred_username</code>, <code>given_name</code>, <code>role</code>, <code>email</code>, etc.</li>
<li><code>Signature</code>: The cryptographic signature of the token used to verify its authenticity</li>
</ul>
<p>Here is an example of a JWT Token issued by <code>AuthServer(OpenIddict)</code>:</p>
<p>The <code>Header</code> part:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-04-20-IDX10204/header.png" alt="JWT Header" /></p>
<p>The <code>Payload</code> part:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-04-20-IDX10204/payload.png" alt="JWT Payload" /></p>
<h3>TokenValidationParameters</h3>
<p>In the <code>JwtBearerOptions</code>, there is a <code>TokenValidationParameters</code> property, which is used to validate the JWT Token.</p>
<p>The default implementation for JWT Token validation is <code>JsonWebTokenHandler</code>, which comes from the <a href="https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/">Microsoft.IdentityModel.JsonWebTokens</a> package.</p>
<p>We didn't set the <code>TokenValidationParameters</code> property in the code above, so the default values below will be used:</p>
<pre><code class="language-csharp">//...
TokenValidationParameters.ValidateAudience = true
TokenValidationParameters.ValidAudience = &quot;MyProjectName&quot;
TokenValidationParameters.ValidAudiences = null

TokenValidationParameters.ValidateIssuer = true
TokenValidationParameters.ValidIssuer = null
TokenValidationParameters.ValidIssuers = null
//...
</code></pre>
<h3>JWT Bearer Token Validation Process</h3>
<p>During JWT Bearer authentication, API website will get the token from the HTTP request and validate it.</p>
<p>The <code>JsonWebTokenHandler</code> will get the <code>OpenID Connect</code> metadata from the <code>AuthServer</code>, it will be used in the validation process, the current metadata request address is: https://localhost:44301/.well-known/openid-configuration , it is a fixed address calculated from the <code>Authority</code> property.</p>
<p>First, the token's Signature is verified using the public key obtained from <code>OpenID Connect</code> metadata(https://localhost:44301/.well-known/jwks).</p>
<p>Then, the payload is validated. The payload is a JSON object containing essential information such as the <code>token type</code>, <code>expiration time</code>, <code>issuer</code>, and <code>audience</code> etc.</p>
<p>Most of the validation problems we may encounter are payload validation failures, for example:</p>
<h4>Lifetime</h4>
<p>If the token in your request has expired, the validation will fail. You will see the exception information like <code>IDX10230</code> in the log.</p>
<h4>Audience</h4>
<p>The <code>ValidAudience</code> of <code>TokenValidationParameters</code> is <code>MyProjectName</code>, the <code>aud</code> in the payload of the token is also <code>MyProjectName</code>, if the token does not contain <code>aud</code> or the <code>aud</code> does not match, the validation will fail. You may see the exception information like <code>IDX10206</code>, <code>IDX10277</code> or <code>IDX10208</code>.</p>
<blockquote>
<p>If the <code>ValidateAudience</code> of <code>TokenValidationParameters</code> is <code>false</code>, then the <code>aud</code> will not be validated.</p>
</blockquote>
<h4>Issuer</h4>
<p>The default value of <code>TokenValidationParameters.ValidateIssuer</code> is <code>true</code>, it requires the token's payload to contain the <code>issuer</code> field, and it must match one of <code>TokenValidationParameters.ValidIssuer</code> or <code>TokenValidationParameters.ValidIssuers</code>.</p>
<blockquote>
<p>The default value of <code>ValidIssuer</code> or <code>ValidIssuers</code> is <code>null</code>, it will use the <code>issuer</code> from the <code>OpenID Connect</code> metadata as the default value.</p>
</blockquote>
<ol>
<li>If the token's payload does not contain the <code>issuer</code> field, you may see the error <code>IDX10211</code>.</li>
<li>If the API website cannot get the <code>OpenID Connect</code> metadata from AuthServer website, the validation will fail. You may see the error <code>IDX10204</code>, the full exception message is: <code>IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.</code></li>
<li>If the <code>issuer</code> does not match, the validation will fail. You may see the error <code>IDX10205</code> in the log.</li>
</ol>
<blockquote>
<p>If the <code>ValidateIssuer</code> of <code>TokenValidationParameters</code> is <code>false</code>, then the <code>issuer</code> will not be validated.</p>
</blockquote>
<blockquote>
<p>Please note that <code>OpenIddict</code> will use the current HTTP request information as the value of <code>issuer</code>. If the AuthServer website is deployed behind a reverse proxy or similar deployment configurations, the <code>issuer</code> in the token may not be the value you expect. In this case, please specify it manually.</p>
</blockquote>
<pre><code class="language-csharp">PreConfigure&lt;OpenIddictServerBuilder&gt;(serverBuilder =&gt;
{
	serverBuilder.SetIssuer(&quot;https://localhost:44301/&quot;);
});
</code></pre>
<h2>Troubleshooting</h2>
<p>To troubleshoot any <code>IDX</code> errors during JWT authentication, you can enable detailed logging by configuring the <code>identitymodel</code> logs as follows:</p>
<pre><code class="language-csharp">using System.Diagnostics.Tracing;
using Microsoft.IdentityModel.Logging;

public class Program
{
    public async static Task&lt;int&gt; Main(string[] args)
    {
        IdentityModelEventSource.ShowPII = true;
        IdentityModelEventSource.Logger.LogLevel = EventLevel.Verbose;
        var wilsonTextLogger = newTextWriterEventListener(&quot;Logs/identitymodel.txt&quot;);
        wilsonTextLogger.EnableEvents(IdentityModelEventSource.Logger, EventLevel.Verbose);

		//...
    }
}
</code></pre>
<p>Additionally, you can enable <code>OpenIddict</code>'s <code>Verbose</code> logs for more detailed debugging information:</p>
<pre><code class="language-csharp">var loggerConfiguration = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override(&quot;Microsoft.EntityFrameworkCore&quot;, LogEventLevel.Warning)
    .MinimumLevel.Override(&quot;OpenIddict&quot;, LogEventLevel.Verbose)
    .Enrich.FromLogContext()
    .WriteTo.Async(c =&gt; c.File(&quot;Logs/logs.txt&quot;))
</code></pre>
<h2>Summary</h2>
<p>For JWT authentication, you need to pay attention to the following key points:</p>
<ol>
<li>Ensure your API website can communicate with the AuthServer properly</li>
<li>Verify that the <code>aud</code> claim in your token matches the expected audience</li>
<li>Confirm that the <code>issuer</code> claim in your token is valid and matches the configuration</li>
</ol>
<p>You can customize the <code>JwtBearerOptions</code>'s <code>TokenValidationParameters</code> to modify the validation rules to meet your actual needs.</p>
<p>For example, if your <code>issuer</code> needs to support multiple subdomains, you can use the <a href="https://github.com/maliming/Owl.TokenWildcardIssuerValidator">Owl.TokenWildcardIssuerValidator</a> library to customize the validation.</p>
<pre><code class="language-csharp">services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =&gt;
    {
        options.Authority = &quot;https://abp.io&quot;;
        options.Audience = &quot;abp_io&quot;;

        options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
        options.TokenValidationParameters.ValidIssuers = new[]
        {
            &quot;https://{0}.abp.io&quot;
        };
    });
</code></pre>
<h2>References</h2>
<ul>
<li><a href="https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2025-04-20-IDX10204/%5Bhttps%3A//learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-auth?view=aspnetcore-8.0%5D(https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication)">Configure JWT bearer authentication in ASP.NET Core</a></li>
<li><a href="https://github.com/openiddict/openiddict-core">OpenIddict</a></li>
<li><a href="https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet">IdentityModel</a></li>
<li><a href="https://github.com/maliming/Owl.TokenWildcardIssuerValidator">Owl.TokenWildcardIssuerValidator</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1964ea-b27c-ceac-64f9-989945797a44" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1964ea-b27c-ceac-64f9-989945797a44" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/developing-a-multitimezone-application-using-the-abp-framework-zk7fnrdq</guid>
      <link>https://abp.io/community/posts/developing-a-multitimezone-application-using-the-abp-framework-zk7fnrdq</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>timezone</category>
      <title>Developing a Multi-Timezone Application Using the ABP Framework</title>
      <description>When developing multi-timezone applications, we need to handle users from different time zones and make sure they see the correct time. The system also needs to support users changing their timezone (like when traveling or moving) and make sure all time displays update correctly to show accurate time information.</description>
      <pubDate>Fri, 11 Apr 2025 13:56:32 Z</pubDate>
      <a10:updated>2026-09-25T22:24:54Z</a10:updated>
      <content:encoded><![CDATA[<h1>Developing a Multi-Timezone Application Using the ABP Framework</h1>
<p>When developing multi-timezone applications, we need to handle users from different time zones and make sure they see the correct time. The system also needs to support users changing their timezone (like when traveling or moving) and make sure all time displays update correctly to show accurate time information.</p>
<p>All these scenarios require us to handle timezone conversions correctly in our application. The ABP framework provides a complete solution for these challenges.</p>
<p>In this article, we'll show you step by step how to handle multi-timezone in the ABP framework.</p>
<blockquote>
<p>The content mentioned in this article will be available after the ABP 9.2 version</p>
</blockquote>
<h2>Timezone Settings</h2>
<p>The ABP framework provides a setting called <code>Abp.Timing.TimeZone</code> for setting and getting the timezone of users, tenants, or applications. The default value is empty, which means the application will use the server's time zone. Check out the <a href="https://abp.io/docs/latest/framework/infrastructure/timing">Timing documentation</a> for more information.</p>
<h2>ISO 8601 Date Time Format</h2>
<p>Different countries and regions may use different time formats:</p>
<ul>
<li>Year-Month-Day (YYYY-MM-DD): Mainly used in China, Japan, Korea, Canada (official standard), Germany (ISO standard), ISO 8601 international standard, etc. Example: 2025-03-11</li>
<li>Day-Month-Year (DD-MM-YYYY): Mainly used in UK, India, Australia, New Zealand, most European countries (like France, Germany, Italy, Spain), some South American countries, etc. Example: 11-03-2025 or 11/03/2025</li>
<li>Month-Day-Year (MM-DD-YYYY): Mainly used in USA, Philippines, some parts of Canada, etc. Example: 03-11-2025 or 03/11/2025</li>
<li>Day.Month.Year (DD.MM.YYYY): Mainly used in Germany, Russia, Switzerland, Hungary, Czech Republic, etc. Example: 11.03.2025</li>
</ul>
<p>Also, different countries/regions might use different separators (like slash /, hyphen -, dot .), and some countries use different month abbreviations or full names (like March 11, 2025).</p>
<p>ISO 8601 uses a standard format to avoid confusion between different date formats and ensure global compatibility.</p>
<p>It has 4 parts:</p>
<ul>
<li>Date part: <code>YYYY-MM-DD</code></li>
<li><code>T</code> as a separator</li>
<li>Time part: <code>HH:MM:SS</code></li>
<li>Timezone part: <code>Z</code> or <code>+/-HH:MM</code></li>
</ul>
<p>You'll usually see formats like: <code>YYYY-MM-DDTHH:MM:SSZ</code> or <code>YYYY-MM-DDTHH:MM:SS+/-HH:MM</code>, for example: <code>2025-03-11T10:30:00Z</code> or <code>2025-03-11T22:30:00+03:00</code></p>
<p>When our application needs to handle multiple timezones, we usually use ISO 8601 to represent time.</p>
<h2>Enabling Multi-Timezone Support</h2>
<p>When we set the <code>Kind</code> of <code>AbpClockOptions</code> to <code>DateTimeKind.Utc</code>, the ABP framework will normalize all times. Times written to the database and returned to the frontend will be in <code>UTC</code>. the <code>SupportsMultipleTimezone</code> property will be <code>true</code> in the <code>IClock</code> service.</p>
<pre><code class="language-csharp">Configure&lt;AbpClockOptions&gt;(options =&gt;
{
    options.Kind = DateTimeKind.Utc;
});
</code></pre>
<h3>Using DateTime to Store Time</h3>
<p>Assuming the <code>DateTime</code> stored in the database is <code>2025-03-01 10:30:00</code>, then the time returned to the front end will be <code>2025-03-01T10:30:00Z</code>. This is a time in ISO 8601 format. Because <code>DateTime</code> does not have timezone information, the framework will assume it is <code>UTC</code> time.</p>
<h3>Using DateTimeOffset to Store Time</h3>
<p>If you use <code>DateTimeOffset</code> to store time, the ABP framework will not normalize <code>DateTimeOffset</code>, but will return it directly to the front end.</p>
<p>Assuming the <code>DateTimeOffset</code> stored in the database is <code>2025-03-01 13:30:00 +03:00</code>, then the time returned to the front end will be <code>2025-03-01T13:30:00+03:00</code>. This is also a time in ISO 8601 format.</p>
<p>We recommend using <code>DateTimeOffset</code> to store time because it has timezone information.</p>
<h2>Timezone Conversion</h2>
<h3>Converting UTC Time to User Time</h3>
<p>The <code>IClock</code> service has 2 methods to convert a given <code>UTC</code> time to the user time:</p>
<pre><code class="language-csharp">DateTime ConvertToUserTime(utcDateTime dateTime)
DateTimeOffset ConvertToUserTime(DateTimeOffset dateTimeOffset)
</code></pre>
<blockquote>
<p>If <code>SupportsMultipleTimezone</code> is <code>false</code> or <code>dateTime.Kind</code> is not <code>Utc</code> or no timezone is set, it will return the given <code>DateTime</code> or <code>DateTimeOffset</code> without any changes.</p>
</blockquote>
<p><strong>Example:</strong></p>
<p>If the user's timezone is <code>Europe/Istanbul</code></p>
<pre><code class="language-csharp">// 2025-03-01T05:30:00Z
var utcTime = new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Utc);

var userTime = Clock.ConvertToUserTime(utcTime);

// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours later.
userTime.Kind.ShouldBe(DateTimeKind.Unspecified);
userTime.ToString(&quot;O&quot;).ShouldBe(&quot;2025-03-01T08:30:00&quot;);
</code></pre>
<pre><code class="language-csharp">// 2025-03-01T05:30:00Z
var utcTime = new DateTimeOffset(new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Utc), TimeSpan.Zero);

var userTime = Clock.ConvertToUserTime(utcTime);

// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours later.
userTime.Offset.ShouldBe(TimeSpan.FromHours(3));
userTime.ToString(&quot;O&quot;).ShouldBe(&quot;2025-03-01T08:30:00.0000000+03:00&quot;);
</code></pre>
<h3>Converting User Time to UTC</h3>
<p>The <code>IClock</code> service has 1 method to convert a given user time to UTC.</p>
<pre><code class="language-csharp">DateTime ConvertToUtc(DateTime dateTime)
</code></pre>
<blockquote>
<p>If <code>SupportsMultipleTimezone</code> is <code>false</code> or <code>dateTime.Kind</code> is <code>Utc</code> or no timezone is set, it will return the given <code>DateTime</code> without any changes.</p>
</blockquote>
<p><strong>Example:</strong></p>
<p>If the user's timezone is <code>Europe/Istanbul</code></p>
<pre><code class="language-csharp">// 2025-03-01T05:30:00
var userTime = new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Unspecified); //Same as Local

var utcTime = Clock.ConvertToUtc(userTime);

// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours earlier.
utcTime.Kind.ShouldBe(DateTimeKind.Utc);
utcTime.ToString(&quot;O&quot;).ShouldBe(&quot;2025-03-01T02:30:00.0000000Z&quot;);
</code></pre>
<h2>Handling Timezone in Different UIs</h2>
<p>We'll use the <code>TimeZoneApp</code> project to demonstrate handling timezone in different UIs. It has a <code>Meeting</code> entity, with several time properties.</p>
<pre><code class="language-csharp">public class Meeting : AggregateRoot&lt;Guid&gt;
{
    public string Subject { get; set; }

    public DateTime StartTime { get; set; }

    public DateTime EndTime { get; set; }

    public DateTime ActualStartTime { get; set; }

    public DateTime? CanceledTime { get; set; }

    public DateTimeOffset ReminderTime { get; set; }

    public DateTimeOffset? FollowUpTime { get; set; }

    public string Description { get; set; }
}
</code></pre>
<p><code>TimeZoneApp</code> project is an ABP layered architecture project, it sets a global <code>Europe/Istanbul</code> timezone, it contains 4 websites</p>
<ul>
<li><code>API.Host</code>: API website, it does not have UI, it returns data in JSON format</li>
<li><code>AuthServer</code>: Authentication server, it uses Razor Pages as UI</li>
<li><code>Web</code>: Razor Pages website, it uses JavaScript to manage Meeting creation and editing and display</li>
<li><code>Blazor</code>: Blazor Server website, it uses Blazor to manage Meeting creation and editing and display</li>
</ul>
<p>All 4 applications are enabled for multi-timezone support, and use the <code>UseAbpTimeZone</code> middleware.</p>
<blockquote>
<p>Blazor WASM and Angular do not need to use the <code>UseAbpTimeZone</code> middleware</p>
</blockquote>
<h3>DateTime in API Response</h3>
<p>In the API response, we usually use the ISO 8601 format time, as you can see, after enabling multi-timezone support, the API returns time to the front end as UTC time.</p>
<p><code>2025-03-01T09:30:00Z</code> and <code>2025-03-01T12:30:00+00:00</code> are ISO 8601 format time.</p>
<pre><code class="language-json">[
  {
    &quot;subject&quot;: &quot;ABP Developer Guide&quot;,
    &quot;startTime&quot;: &quot;2025-03-01T09:30:00Z&quot;,
    &quot;endTime&quot;: &quot;2025-03-01T10:30:00Z&quot;,
    &quot;actualStartTime&quot;: &quot;2025-03-01T11:30:00Z&quot;,
    &quot;canceledTime&quot;: null,
    &quot;reminderTime&quot;: &quot;2025-03-01T12:30:00+00:00&quot;,
    &quot;followUpTime&quot;: &quot;2025-03-01T13:30:00+00:00&quot;,
    &quot;description&quot;: &quot;We will discuss the ABP developer guide.&quot;,
    &quot;id&quot;: &quot;2af0abd3-be06-ecff-5d4c-3a1895ac7950&quot;
  },
  {
    &quot;subject&quot;: &quot;ABP Training&quot;,
    &quot;startTime&quot;: &quot;2025-03-01T09:30:00Z&quot;,
    &quot;endTime&quot;: &quot;2025-03-01T10:30:00Z&quot;,
    &quot;actualStartTime&quot;: &quot;2025-03-01T11:30:00Z&quot;,
    &quot;canceledTime&quot;: &quot;2025-03-01T12:00:00Z&quot;,
    &quot;reminderTime&quot;: &quot;2025-03-01T12:30:00+00:00&quot;,
    &quot;followUpTime&quot;: &quot;2025-03-01T13:30:00+00:00&quot;,
    &quot;description&quot;: &quot;ABP training for the new developers.&quot;,
    &quot;id&quot;: &quot;290b0cb6-3e50-6324-1e79-3a1895ac795f&quot;
  }
]
</code></pre>
<h3>Handling Timezone in MVC/Razor Pages</h3>
<p>In the <code>AuthServer</code> project, we handle time conversion in a simple way:</p>
<ol>
<li>First, we get the <code>Meeting</code> entities from the database using <code>IRepository&lt;Meeting, Guid&gt;</code>. At this point, all <code>DateTime</code> values are in UTC.</li>
<li>Then, when displaying the times in the view, we use <code>Clock.ConvertToUserTime</code> to show them in the user's timezone.</li>
</ol>
<blockquote>
<p>Note: The <code>ConvertToUserTime</code> method will only convert times if multi-timezone support is enabled in the application.</p>
</blockquote>
<pre><code class="language-csharp">public class IndexModel : AbpPageModel
{
    public List&lt;Meeting&gt;? Meetings { get; set; }

    protected IRepository&lt;Meeting, Guid&gt; MeetingRepository { get; }

    public IndexModel(IRepository&lt;Meeting, Guid&gt; meetingRepository)
    {
        MeetingRepository = meetingRepository;
    }

    public async Task OnGetAsync()
    {
        Meetings = await MeetingRepository.GetListAsync();
    }
}
</code></pre>
<pre><code class="language-html">&lt;div class=&quot;container&quot;&gt;
	&lt;abp-row&gt;
		&lt;div class=&quot;table-responsive&quot;&gt;
			&lt;table class=&quot;table table-striped table-hover mt-3&quot;&gt;
				&lt;thead&gt;
				&lt;tr&gt;
					&lt;th&gt;@L[&quot;Subject&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;StartTime&quot;] / @L[&quot;EndTime&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;ActualStartTime&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;CanceledTime&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;ReminderTime&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;FollowUpTime&quot;]&lt;/th&gt;
					&lt;th&gt;@L[&quot;Description&quot;]&lt;/th&gt;
				&lt;/tr&gt;
				&lt;/thead&gt;
				&lt;tbody&gt;
				@foreach (var meeting in Model.Meetings)
				{
					&lt;tr&gt;
						&lt;td&gt;@meeting.Subject&lt;/td&gt;
						&lt;td&gt;@Clock.ConvertToUserTime(meeting.StartTime) ➡️ @Clock.ConvertToUserTime(meeting.EndTime)&lt;/td&gt;
						&lt;td&gt;@Clock.ConvertToUserTime(meeting.ActualStartTime)&lt;/td&gt;
						&lt;td&gt;@(meeting.CanceledTime.HasValue ? Clock.ConvertToUserTime(meeting.CanceledTime.Value) : &quot;N/A&quot;)&lt;/td&gt;
						&lt;td&gt;@Clock.ConvertToUserTime(meeting.ReminderTime).DateTime&lt;/td&gt;
						&lt;td&gt;@(meeting.FollowUpTime.HasValue ? Clock.ConvertToUserTime(meeting.FollowUpTime.Value).DateTime : &quot;N/A&quot;)&lt;/td&gt;
						&lt;td&gt;@meeting.Description&lt;/td&gt;
					&lt;/tr&gt;
				}
				&lt;/tbody&gt;
			&lt;/table&gt;
		&lt;/div&gt;
	&lt;/abp-row&gt;
&lt;/div&gt;
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/auth-list.png" alt="" /></p>
<h3>Handling Timezone in JavaScript</h3>
<p>In the <code>Web</code> project, we use JavaScript to handle timezone.</p>
<h4>Displaying Time in UI</h4>
<ul>
<li><code>timeZoneApp.meetings.meeting.getList</code> gets all <code>Meeting</code> entities and displays them in <code>DataTables</code></li>
<li><code>abp.clock.normalizeToLocaleString()</code> is the ABP JavaScript API, it converts <code>UTC</code> time to the current user's timezone, and then calls its <code>toLocaleString</code> method to format time</li>
<li><code>dataFormat: &quot;datetime&quot;</code> is the ABP DataTable extension method, it calls the <code>abp.clock.normalizeToLocaleString</code> method to convert and format time</li>
</ul>
<blockquote>
<p>If the current application is not enabled for multi-timezone support, then the <code>abp.clock.normalizeToLocaleString</code> method will not convert the time, it will just call the <code>Date</code> object's <code>toLocaleString</code> method.</p>
</blockquote>
<pre><code class="language-js">var dataTable = $('#MeetingsTable').DataTable(
	abp.libs.datatables.normalizeConfiguration({
		serverSide: true,
		paging: true,
		order: [[1, &quot;asc&quot;]],
		searching: false,
		scrollX: true,
		ajax: abp.libs.datatables.createAjax(timeZoneApp.meetings.meeting.getList),
		columnDefs: [
			{
				title: l('Actions'),
				rowAction: {
					items:
						[
							{
								text: l('Edit'),
								visible: abp.auth.isGranted('TimeZoneApp.Meetings.Edit'),
								action: function (data) {
									editModal.open({ id: data.record.id });
								},
							},
							{
								text: l('Delete'),
								visible: abp.auth.isGranted('TimeZoneApp.Meetings.Delete'),
								confirmMessage: function (data) {
									return l('MeetingDeletionConfirmationMessage', data.record.subject);
								},
								action: function (data) {
									timeZoneApp.meetings.meeting
										.delete(data.record.id)
										.then(function() {
											abp.notify.info(l('SuccessfullyDeleted'));
											dataTable.ajax.reload();
										});
								}
							}
						]
				}
			},
			{
				title: l('Subject'),
				data: &quot;subject&quot;
			},
			{
				title: l('StartTime') + ' / ' + l('StartTime'),
				data: &quot;startTime&quot;,
				render: function (data, type, row) {
					return abp.clock.normalizeToLocaleString(row.startTime) + ' ➡️ ' + abp.clock.normalizeToLocaleString(row.endTime);
				}
			},
			{
				title: l('ActualStartTime'),
				data: &quot;actualStartTime&quot;,
				dataFormat: &quot;datetime&quot;
			},
			{
				title: l('CanceledTime'),
				data: &quot;canceledTime&quot;,
				render: function (data, type, row) {
					return data ? abp.clock.normalizeToLocaleString(data) : 'N/A';
				}
			},
			{
				title: l('ReminderTime'),
				data: &quot;reminderTime&quot;,
				dataFormat: &quot;datetime&quot;
			},
			{
				title: l('FollowUpTime'),
				data: &quot;followUpTime&quot;,
				render: function (data, type, row) {
					return data ? abp.clock.normalizeToLocaleString(data) : 'N/A';
				}
			},
			{
				title: l('Description'),
				data: &quot;description&quot;
			}
		]
	})
);
</code></pre>
<p>Below is the screenshot of <code>DataTables</code>:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-list.png" alt="" /></p>
<h4>Creating and Editing Meeting</h4>
<p>We use <code>JavaScript</code> to create and edit <code>Meeting</code>.</p>
<p>ABP's <a href="https://abp.io/docs/latest/framework/ui/mvc-razor-pages/tag-helpers">TagHelper</a> can automatically create forms based on the model, it will generate corresponding HTML tags based on the attributes in the model. For <code>DateTime</code> and <code>DateTimeOffset</code> attributes, it will generate and initialize a <a href="https://www.daterangepicker.com/">DateTimePicker</a> component.</p>
<p><strong>CreateModal</strong> and <strong>EditModal</strong> :</p>
<pre><code class="language-html">&lt;abp-dynamic-form abp-model=&quot;Meeting&quot; asp-page=&quot;/Meetings/CreateModal&quot;&gt; 
    &lt;abp-modal&gt;
        &lt;abp-modal-header title=&quot;@L[&quot;NewMeeting&quot;].Value&quot;&gt;&lt;/abp-modal-header&gt;
        &lt;abp-modal-body&gt;
            &lt;abp-form-content /&gt;
        &lt;/abp-modal-body&gt;
        &lt;abp-modal-footer buttons=&quot;@(AbpModalButtons.Cancel|AbpModalButtons.Save)&quot;&gt;&lt;/abp-modal-footer&gt;
    &lt;/abp-modal&gt;
&lt;/abp-dynamic-form&gt;
</code></pre>
<pre><code class="language-html">&lt;abp-dynamic-form abp-model=&quot;Meeting&quot; asp-page=&quot;/Meetings/EditModal&quot;&gt;
    &lt;abp-modal&gt;
        &lt;abp-modal-header title=&quot;@L[&quot;Update&quot;].Value&quot;&gt;&lt;/abp-modal-header&gt;
        &lt;abp-modal-body&gt;
            &lt;abp-input asp-for=&quot;Id&quot; /&gt;
            &lt;abp-form-content /&gt;
        &lt;/abp-modal-body&gt;
        &lt;abp-modal-footer buttons=&quot;@(AbpModalButtons.Cancel|AbpModalButtons.Save)&quot;&gt;&lt;/abp-modal-footer&gt;
    &lt;/abp-modal&gt;
&lt;/abp-dynamic-form&gt;
</code></pre>
<p>You can see that the time in the control has been converted to the current user's timezone.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-create.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-edit.png" alt="" /></p>
<p>When we submit the form, we need to convert the time to <code>UTC</code>. In the <code>JavaScript</code> of the <code>Create</code> and <code>Edit</code> pages, we use the <code>handleDatepicker</code> this <code>jQuery</code> extension method to handle time in the form, it internally gets the user's local time from the selector <code>input[type=&quot;hidden&quot;][data-hidden-datepicker]</code>, and then uses the <code>abp.clock.normalizeToString</code> method to convert the date field in the form to the <code>ISO 8601</code> format <code>UTC</code> time string.</p>
<blockquote>
<p>If the current application is not enabled for multi-timezone support, then the <code>abp.clock.normalizeToString</code> method will not convert the time, it will just convert to the ISO 8601 format time string without timezone.</p>
</blockquote>
<pre><code class="language-js">var abp = abp || {};
$(function () {
    abp.modals.meetingCreate = function () {
        var initModal = function (publicApi, args) {
            var $form = publicApi.getForm();
            $form.find('button[type=&quot;submit&quot;]').on('click', function (e) {
                $form.handleDatepicker('input[type=&quot;hidden&quot;][data-hidden-datepicker]');
            });
        };

        return {
            initModal: initModal
        }
    };
});
</code></pre>
<p>The requested data is as follows:</p>
<pre><code class="language-csharp">Request URL: Meetings/EditModal
Request Method: POST
Payload:
	Id: 0803780b-3762-2af8-1c75-3a1895d59c89
	Meeting.Subject: ABP Developer Guide
	Meeting.StartTime: 2025-03-01T09:30:00.000Z
	Meeting.EndTime: 2025-03-01T10:30:00.000Z
	Meeting.ActualStartTime: 2025-03-01T11:30:00.000Z
	Meeting.CanceledTime: 
	Meeting.ReminderTime: 2025-03-01T12:30:00.000Z
	Meeting.FollowUpTime: 2025-03-01T13:30:00.000Z
	Meeting.Description: We will discuss the ABP developer guide.
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-post.png" alt="" /></p>
<p>In short, we use the <code>abp.clock.normalizeToLocaleString</code> method to display time, and use the <code>abp.clock.normalizeToString</code> method to modify the time to be submitted. If you submit data via <code>ajax</code>, please remember to use the <code>abp.clock.normalizeToString</code> method to convert time.</p>
<h3>Handling Timezone in Blazor</h3>
<p>We cannot automatically complete some work in <code>Blazor UI</code>, we need to inject <code>IClock</code> and use the <code>ConvertToUserTime</code> and <code>ConvertToUtc</code> methods to display and create/update entities.</p>
<p>Below is a complete <code>Meeting</code> page, please refer to the usage of <code>Clock</code> in it.</p>
<pre><code class="language-csharp">@page &quot;/meetings&quot;
@using Volo.Abp.Application.Dtos
@using Microsoft.Extensions.Localization
@using TimeZoneApp.Meetings
@using TimeZoneApp.Localization
@using TimeZoneApp.Permissions
@using Volo.Abp.AspNetCore.Components.Web
@inject IStringLocalizer&lt;TimeZoneAppResource&gt; L
@inject AbpBlazorMessageLocalizerHelper&lt;TimeZoneAppResource&gt; LH
@inherits AbpCrudPageBase&lt;IMeetingAppService, MeetingDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateMeetingDto&gt;

&lt;Card&gt;
    &lt;CardHeader&gt;
        &lt;Row Class=&quot;justify-content-between&quot;&gt;
            &lt;Column ColumnSize=&quot;ColumnSize.IsAuto&quot;&gt;
                &lt;h2&gt;@L[&quot;Meetings&quot;]&lt;/h2&gt;
            &lt;/Column&gt;
            &lt;Column ColumnSize=&quot;ColumnSize.IsAuto&quot;&gt;
                @if (HasCreatePermission)
                {
                    &lt;Button Color=&quot;Color.Primary&quot; Clicked=&quot;OpenCreateModalAsync&quot;&gt;@L[&quot;NewMeeting&quot;]&lt;/Button&gt;
                }
            &lt;/Column&gt;
        &lt;/Row&gt;
    &lt;/CardHeader&gt;
    &lt;CardBody&gt;
        &lt;DataGrid TItem=&quot;MeetingDto&quot;
                  Data=&quot;Entities&quot;
                  ReadData=&quot;OnDataGridReadAsync&quot;
                  TotalItems=&quot;TotalCount&quot;
                  ShowPager=&quot;true&quot;
                  PageSize=&quot;PageSize&quot;&gt;
            &lt;DataGridColumns&gt;
                &lt;DataGridEntityActionsColumn TItem=&quot;MeetingDto&quot; @ref=&quot;@EntityActionsColumn&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        &lt;EntityActions TItem=&quot;MeetingDto&quot; EntityActionsColumn=&quot;@EntityActionsColumn&quot;&gt;
                            &lt;EntityAction TItem=&quot;MeetingDto&quot;
                                          Text=&quot;@L[&quot;Edit&quot;]&quot;
                                          Visible=HasUpdatePermission
                                          Clicked=&quot;() =&gt; OpenEditModalAsync(context)&quot; /&gt;
                            &lt;EntityAction TItem=&quot;MeetingDto&quot;
                                          Text=&quot;@L[&quot;Delete&quot;]&quot;
                                          Clicked=&quot;() =&gt; DeleteEntityAsync(context)&quot;
                                          Visible=HasDeletePermission
                                          ConfirmationMessage=&quot;()=&gt;GetDeleteConfirmationMessage(context)&quot; /&gt;
                        &lt;/EntityActions&gt;
                    &lt;/DisplayTemplate&gt;
                &lt;/DataGridEntityActionsColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.Subject)&quot;
                                Caption=&quot;@L[&quot;Subject&quot;]&quot;&gt;&lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.StartTime)&quot;
                                Caption=&quot;@(L[&quot;StartTime&quot;] + &quot;/&quot; + L[&quot;EndTime&quot;])&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        @Clock.ConvertToUserTime(context.StartTime).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;) ➡️ @Clock.ConvertToUserTime(context.EndTime).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;)
                    &lt;/DisplayTemplate&gt;
                &lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.ActualStartTime)&quot;
                                Caption=&quot;@L[&quot;ActualStartTime&quot;]&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        @Clock.ConvertToUserTime(context.ActualStartTime).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;)
                    &lt;/DisplayTemplate&gt;
                &lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.CanceledTime)&quot;
                                Caption=&quot;@L[&quot;CanceledTime&quot;]&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        @(context.CanceledTime.HasValue ? Clock.ConvertToUserTime(context.CanceledTime.Value).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;) : &quot;N/A&quot;)
                    &lt;/DisplayTemplate&gt;
                &lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.ReminderTime)&quot;
                                Caption=&quot;@L[&quot;ReminderTime&quot;]&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        @(Clock.ConvertToUserTime(context.ReminderTime).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;) )
                          &lt;/DisplayTemplate&gt;
                &lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.FollowUpTime)&quot;
                                Caption=&quot;@L[&quot;FollowUpTime&quot;]&quot;&gt;
                    &lt;DisplayTemplate&gt;
                        @(context.FollowUpTime.HasValue ? Clock.ConvertToUserTime(context.FollowUpTime.Value).ToString(&quot;yyyy-MM-dd HH:mm:ss&quot;) : &quot;N/A&quot;)
                    &lt;/DisplayTemplate&gt;
                &lt;/DataGridColumn&gt;
                &lt;DataGridColumn TItem=&quot;MeetingDto&quot;
                                Field=&quot;@nameof(MeetingDto.Description)&quot;
                                Caption=&quot;@L[&quot;Description&quot;]&quot;&gt;
                &lt;/DataGridColumn&gt;
            &lt;/DataGridColumns&gt;
        &lt;/DataGrid&gt;
    &lt;/CardBody&gt;
&lt;/Card&gt;

&lt;Modal @ref=&quot;@CreateModal&quot;&gt;
    &lt;ModalContent IsCentered=&quot;true&quot;&gt;
        &lt;Form&gt;
            &lt;ModalHeader&gt;
                &lt;ModalTitle&gt;@L[&quot;NewMeeting&quot;]&lt;/ModalTitle&gt;
                &lt;CloseButton Clicked=&quot;CloseCreateModalAsync&quot;/&gt;
            &lt;/ModalHeader&gt;
            &lt;ModalBody&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;Subject&quot;]&lt;/FieldLabel&gt;
                            &lt;TextEdit @bind-Text=&quot;@NewEntity.Subject&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;StartTime&quot;] / @L[&quot;EndTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DatePicker TValue=&quot;DateTime?&quot; @bind-Dates=&quot;SelectedDates&quot; InputMode=&quot;DateInputMode.DateTime&quot; SelectionMode=&quot;DateInputSelectionMode.Range&quot; /&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;ActualStartTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTime&quot; @bind-Date=&quot;NewEntity.ActualStartTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;CanceledTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTime?&quot; @bind-Date=&quot;NewEntity.CanceledTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;ReminderTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTimeOffset&quot; @bind-Date=&quot;NewEntity.ReminderTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;FollowUpTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTimeOffset?&quot; @bind-Date=&quot;NewEntity.FollowUpTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Validation MessageLocalizer=&quot;@LH.Localize&quot;&gt;
                        &lt;Field&gt;
                            &lt;FieldLabel&gt;@L[&quot;Description&quot;]&lt;/FieldLabel&gt;
                            &lt;TextEdit @bind-Text=&quot;@NewEntity.Description&quot;&gt;
                                &lt;Feedback&gt;
                                    &lt;ValidationError/&gt;
                                &lt;/Feedback&gt;
                            &lt;/TextEdit&gt;
                        &lt;/Field&gt;
                    &lt;/Validation&gt;
                &lt;/Validations&gt;
            &lt;/ModalBody&gt;
            &lt;ModalFooter&gt;
                &lt;Button Color=&quot;Color.Secondary&quot;
                        Clicked=&quot;CloseCreateModalAsync&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;/ModalFooter&gt;
        &lt;/Form&gt;
    &lt;/ModalContent&gt;
&lt;/Modal&gt;

&lt;Modal @ref=&quot;@EditModal&quot;&gt;
    &lt;ModalContent IsCentered=&quot;true&quot;&gt;
        &lt;Form&gt;
            &lt;ModalHeader&gt;
                &lt;ModalTitle&gt;@EditingEntity.Subject&lt;/ModalTitle&gt;
                &lt;CloseButton Clicked=&quot;CloseEditModalAsync&quot;/&gt;
            &lt;/ModalHeader&gt;
            &lt;ModalBody&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;Subject&quot;]&lt;/FieldLabel&gt;
                            &lt;TextEdit @bind-Text=&quot;@EditingEntity.Subject&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;StartTime&quot;] / @L[&quot;EndTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DatePicker TValue=&quot;DateTime?&quot; @bind-Dates=&quot;SelectedDates&quot; InputMode=&quot;DateInputMode.DateTime&quot; SelectionMode=&quot;DateInputSelectionMode.Range&quot; /&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;ActualStartTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTime&quot; @bind-Date=&quot;EditingEntity.ActualStartTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;CanceledTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTime?&quot; @bind-Date=&quot;EditingEntity.CanceledTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;ReminderTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTimeOffset&quot; @bind-Date=&quot;EditingEntity.ReminderTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Field&gt;
                        &lt;FieldLabel&gt;@L[&quot;FollowUpTime&quot;]&lt;/FieldLabel&gt;
                        &lt;DateEdit TValue=&quot;DateTimeOffset?&quot; @bind-Date=&quot;EditingEntity.FollowUpTime&quot; InputMode=&quot;DateInputMode.DateTime&quot;/&gt;
                    &lt;/Field&gt;
                    &lt;Validation MessageLocalizer=&quot;@LH.Localize&quot;&gt;
                        &lt;Field&gt;
                            &lt;FieldLabel&gt;@L[&quot;Description&quot;]&lt;/FieldLabel&gt;
                            &lt;TextEdit @bind-Text=&quot;@EditingEntity.Description&quot;&gt;
                                &lt;Feedback&gt;
                                    &lt;ValidationError/&gt;
                                &lt;/Feedback&gt;
                            &lt;/TextEdit&gt;
                        &lt;/Field&gt;
                    &lt;/Validation&gt;
                &lt;/Validations&gt;
            &lt;/ModalBody&gt;
            &lt;ModalFooter&gt;
                &lt;Button Color=&quot;Color.Secondary&quot;
                        Clicked=&quot;CloseEditModalAsync&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;/ModalFooter&gt;
        &lt;/Form&gt;
    &lt;/ModalContent&gt;
&lt;/Modal&gt;


@code {
    IReadOnlyList&lt;DateTime?&gt; SelectedDates;

    public Meeting()
    {
        CreatePolicyName = TimeZoneAppPermissions.Meetings.Create;
        UpdatePolicyName = TimeZoneAppPermissions.Meetings.Edit;
        DeletePolicyName = TimeZoneAppPermissions.Meetings.Delete;
    }

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

        var now = DateTime.Now;
        SelectedDates = new List&lt;DateTime?&gt; { now.Date.AddHours(10),now.Date.AddDays(7).AddHours(10) };
        NewEntity.ActualStartTime = now.Date.AddHours(11);
        NewEntity.CanceledTime = now.Date.AddHours(12);
        NewEntity.ReminderTime = now.Date.AddHours(13);
        NewEntity.FollowUpTime = now.Date.AddHours(14);
    }

    protected override Task OnCreatingEntityAsync()
    {
        if (SelectedDates.Count == 2 &amp;&amp; SelectedDates[0].HasValue &amp;&amp; SelectedDates[1].HasValue)
        {
            NewEntity.StartTime = Clock.ConvertToUtc(SelectedDates[0]!.Value);
            NewEntity.EndTime = Clock.ConvertToUtc(SelectedDates[1]!.Value);
        }

        NewEntity.ActualStartTime = Clock.ConvertToUtc(NewEntity.ActualStartTime);
        NewEntity.CanceledTime = NewEntity.CanceledTime.HasValue ? Clock.ConvertToUtc(NewEntity.CanceledTime.Value) : null;

        NewEntity.ReminderTime = Clock.ConvertToUtc(NewEntity.ReminderTime.DateTime);
        NewEntity.FollowUpTime = NewEntity.FollowUpTime.HasValue ? Clock.ConvertToUtc(NewEntity.FollowUpTime.Value.DateTime) : null;

        return Task.CompletedTask;
    }

    protected override async Task OpenEditModalAsync(MeetingDto entity)
    {
        await base.OpenEditModalAsync(entity);

        SelectedDates = new List&lt;DateTime?&gt; { Clock.ConvertToUserTime(EditingEntity.StartTime), Clock.ConvertToUserTime(EditingEntity.EndTime) };
        EditingEntity.ActualStartTime = Clock.ConvertToUserTime(EditingEntity.ActualStartTime);
        EditingEntity.CanceledTime = EditingEntity.CanceledTime.HasValue ? Clock.ConvertToUserTime(EditingEntity.CanceledTime.Value) : null;
        EditingEntity.ReminderTime = Clock.ConvertToUserTime(EditingEntity.ReminderTime);
        EditingEntity.FollowUpTime = EditingEntity.FollowUpTime.HasValue ? Clock.ConvertToUserTime(EditingEntity.FollowUpTime.Value) : null;
    }

    protected override Task OnUpdatingEntityAsync()
    {
        if (SelectedDates.Count == 2 &amp;&amp; SelectedDates[0].HasValue &amp;&amp; SelectedDates[1].HasValue)
        {
            EditingEntity.StartTime = Clock.ConvertToUtc(SelectedDates[0]!.Value);
            EditingEntity.EndTime = Clock.ConvertToUtc(SelectedDates[1]!.Value);
        }

        EditingEntity.ActualStartTime = Clock.ConvertToUtc(EditingEntity.ActualStartTime);
        EditingEntity.CanceledTime = EditingEntity.CanceledTime.HasValue ? Clock.ConvertToUtc(EditingEntity.CanceledTime.Value) : null;

        return Task.CompletedTask;
    }
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/blazor-list.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/blazor-create.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/blazor-edit.png" alt="" /></p>
<h2>Timezone Settings Change</h2>
<p>If the timezone settings change, then all times will be converted to the new timezone. For example, if the current timezone changes from <code>Europe/Istanbul</code> to <code>Europe/Berlin</code>, then all times will be converted to the <code>Europe/Berlin</code> timezone.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/berlin.png" alt="" /></p>
<p><code>Europe/Istanbul</code>:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/auth-list-utc3.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-list-utc3.png" alt="" /></p>
<p><code>Europe/Berlin</code>:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/auth-list-utc1.png" alt="" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-11-Developing-A-Multi-Timezone-Application-Using-The-ABP-Framework/mvc-list-utc1.png" alt="" /></p>
<h2>Browser Timezone Detection</h2>
<p>When no timezone setting is configured, ABP's MVC, Blazor, and Angular applications will automatically detect the browser's timezone during initialization. The detected timezone is then stored in either the request's Cookie or Header.</p>
<p>This functionality is implemented by the <code>UseAbpTimeZone</code> middleware, which follows a specific order to determine the appropriate timezone:</p>
<ol>
<li>First, it attempts to retrieve the timezone from the application/tenant/user settings</li>
<li>If no setting is found, it tries to get the timezone from the request information, including Cookie, Header, QueryString, and Form</li>
<li>Finally, if no timezone information is found, it falls back to using the server's timezone as the default</li>
</ol>
<blockquote>
<p>The timezone information is stored using the key <code>__timezone</code></p>
</blockquote>
<h2>TimeZoneApp Source Code</h2>
<p>You can download and view the <a href="https://github.com/maliming/TimeZone">TimeZoneApp source code</a> for detailed implementation.</p>
<h2>Summary</h2>
<p>Through this article, we learned how to handle timezone in different types of UIs. I hope this article is helpful to you. If you have any questions, please contact me at any time.</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/understanding-the-embedded-files-in-abp-framework-nsrp8aa9</guid>
      <link>https://abp.io/community/posts/understanding-the-embedded-files-in-abp-framework-nsrp8aa9</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Understanding the Embedded Files in ABP Framework</title>
      <description>This article provides an in-depth explanation of embedded files and how to use them within the ABP framework Virtual File System.
</description>
      <pubDate>Mon, 24 Mar 2025 00:48:24 Z</pubDate>
      <a10:updated>2026-09-26T01:11:58Z</a10:updated>
      <content:encoded><![CDATA[<h1>Understanding the Embedded Files in ABP Framework</h1>
<p>Embedded Files functionality in .NET applications allows external files (like configuration files, images, etc.) to be directly embedded into assemblies (.exe or .dll). This simplifies deployment, prevents file loss or tampering, improves security and performance, and reduces path and dependency management issues. Through embedded resources, programs can access these files more conveniently without additional file operations.</p>
<h2>Embedding Files in Your Project</h2>
<p>We embed <code>Volo\Abp\MyModule\Localization\*.json</code> files into the assembly in our <code>MyModule.csproj</code>.</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

    &lt;PropertyGroup&gt;
        &lt;TargetFramework&gt;net9.0&lt;/TargetFramework&gt;
        &lt;OutputType&gt;Exe&lt;/OutputType&gt;
        &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;/PropertyGroup&gt;

    &lt;ItemGroup&gt;
        &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;9.0.0&quot; /&gt;
        &lt;PackageReference Include=&quot;Volo.Abp.VirtualFileSystem&quot; Version=&quot;9.0.0&quot;  /&gt;
    &lt;/ItemGroup&gt;

    &lt;ItemGroup&gt;
        &lt;None Remove=&quot;Volo\Abp\MyModule\Localization\*.json&quot; /&gt;
        &lt;EmbeddedResource Include=&quot;Volo\Abp\MyModule\Localization\*.json&quot; /&gt;
    &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>
<p>If we check the <code>en.json</code> file in our IDE, we'll see it's embedded in the assembly.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-23-Understanding-the-Embedded-Files-in-ABP-Framework/1.png" alt="image" /></p>
<p>When we decompile the built <code>MyModule.dll</code> file, we can also see the <code>en.json</code> file.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-23-Understanding-the-Embedded-Files-in-ABP-Framework/2.png" alt="image" /></p>
<h2>Accessing Embedded Files in Code</h2>
<pre><code class="language-csharp">public class Program
{
    public static async Task&lt;int&gt; Main(string[] args)
    {
        var embeddedFiles = typeof(Program).Assembly.GetManifestResourceNames();
        foreach (var embeddedFile in embeddedFiles)
        {
            Console.WriteLine(embeddedFile);
            var fileStream = typeof(Program).Assembly.GetManifestResourceStream(embeddedFile);
            if (fileStream != null)
            {
                using var reader = new System.IO.StreamReader(fileStream);
                var content = await reader.ReadToEndAsync();
                Console.WriteLine(content);
            }
        }
    }
}
</code></pre>
<p>This code will output the embedded file names and their contents.</p>
<pre><code>MyModule.Volo.Abp.MyModule.Localization.en.json

{
  &quot;key&quot;:&quot;value&quot;
}
</code></pre>
<h2>Integrating with ABP Virtual File System</h2>
<p>The ABP Virtual File System makes it possible to manage files that don't physically exist on the file system (disk). It's mainly used to embed (js, css, image..) files into assemblies and use them like physical files at runtime.</p>
<p>The following code shows how to add embedded files from the current application assembly to the ABP virtual file system:</p>
<pre><code class="language-csharp">[DependsOn(typeof(AbpVirtualFileSystemModule))]
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure&lt;AbpVirtualFileSystemOptions&gt;(options =&gt;
        {
            options.FileSets.AddEmbedded&lt;MyModule&gt;();
        });
    }
}
</code></pre>
<p>ABP creates an <code>AbpEmbeddedFileProvider</code> to access the embedded files.</p>
<p>The full name of <code>en.json</code> is <code>MyModule.Volo.Abp.MyModule.Localization.en.json</code>. Without directory information, ABP uses <code>.</code> to split and assume directory information. This creates the following directory structure in the virtual file system:</p>
<pre><code>[Dir] [/MyModule]
[Dir] [/MyModule/Volo]
[Dir] [/MyModule/Volo/Abp]
[Dir] [/MyModule/Volo/Abp/MyModule]
[Dir] [/MyModule/Volo/Abp/MyModule/Localization]
[File] [/MyModule/Volo/Abp/MyModule/Localization/en.json]
</code></pre>
<p>Now you can inject <code>IVirtualFileProvider</code> to access embedded files using the directory/file structure above.</p>
<h2>Manifest Embedded File Provider</h2>
<p>You might have noticed that using <code>.</code> to split and assume directory information can cause confusion if filenames contain dots.</p>
<p>For example, if your filename is <code>zh.hans.json</code>, ABP will generate the following directory structure, which isn't what we want:</p>
<pre><code>[Dir] [/MyModule]
[Dir] [/MyModule/Volo]
[Dir] [/MyModule/Volo/Abp]
[Dir] [/MyModule/Volo/Abp/MyModule]
[Dir] [/MyModule/Volo/Abp/MyModule/Localization]
[Dir] [/MyModule/Volo/Abp/MyModule/Localization/zh]
[File] [/MyModule/Volo/Abp/MyModule/Localization/zh/hans.json]
</code></pre>
<p>Microsoft provides the <code>Microsoft.Extensions.FileProviders.Embedded</code> library to solve this problem.</p>
<p>We need to add this package dependency and set <code>&lt;GenerateEmbeddedFilesManifest&gt;true&lt;/GenerateEmbeddedFilesManifest&gt;</code> in our project:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

    &lt;PropertyGroup&gt;
        &lt;TargetFramework&gt;net9.0&lt;/TargetFramework&gt;
        &lt;OutputType&gt;Exe&lt;/OutputType&gt;
        &lt;Nullable&gt;enable&lt;/Nullable&gt;
        &lt;GenerateEmbeddedFilesManifest&gt;true&lt;/GenerateEmbeddedFilesManifest&gt;
    &lt;/PropertyGroup&gt;

    &lt;ItemGroup&gt;
        &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;9.0.0&quot; /&gt;
        &lt;PackageReference Include=&quot;Volo.Abp.VirtualFileSystem&quot; Version=&quot;9.0.0&quot;  /&gt;
        &lt;PackageReference Include=&quot;Microsoft.Extensions.FileProviders.Embedded&quot; Version=&quot;9.0.0&quot; /&gt;
    &lt;/ItemGroup&gt;

    &lt;ItemGroup&gt;
        &lt;None Remove=&quot;Volo\Abp\MyModule\Localization\*.json&quot; /&gt;
        &lt;EmbeddedResource Include=&quot;Volo\Abp\MyModule\Localization\*.json&quot; /&gt;
    &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>
<p>After rebuilding the project, when we decompile <code>MyModule.dll</code>, we'll see an additional <code>Microsoft.Extensions.FileProviders.Embedded.Manifest.xml</code> file.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-23-Understanding-the-Embedded-Files-in-ABP-Framework/3.png" alt="image" /></p>
<p>This manifest file stores all the directory and file information of embedded resources. When ABP finds this file, it will use <code>ManifestEmbeddedFileProvider</code> instead of <code>AbpEmbeddedFileProvider</code> to access embedded files:</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=&quot;yes&quot;?&gt;
&lt;Manifest&gt;
    &lt;ManifestVersion&gt;1.0&lt;/ManifestVersion&gt;
    &lt;FileSystem&gt;
        &lt;File Name=&quot;Microsoft.Extensions.FileProviders.Embedded.Manifest.xml&quot;&gt;
            &lt;ResourcePath&gt;Microsoft.Extensions.FileProviders.Embedded.Manifest.xml&lt;/ResourcePath&gt;
        &lt;/File&gt;
        &lt;Directory Name=&quot;Volo&quot;&gt;
            &lt;Directory Name=&quot;Abp&quot;&gt;
                &lt;Directory Name=&quot;MyModule&quot;&gt;
                    &lt;Directory Name=&quot;Localization&quot;&gt;
                        &lt;File Name=&quot;zh.hans.json&quot;&gt;
                            &lt;ResourcePath&gt;MyModule.Volo.Abp.MyModule.Localization.zh.hans.json&lt;/ResourcePath&gt;
                        &lt;/File&gt;
                    &lt;/Directory&gt;
                &lt;/Directory&gt;
            &lt;/Directory&gt;
        &lt;/Directory&gt;
    &lt;/FileSystem&gt;
&lt;/Manifest&gt;
</code></pre>
<h2>Parameters of AddEmbedded Method</h2>
<p>The <code>AddEmbedded</code> method can take two parameters:</p>
<h3>baseNamespace</h3>
<p>This may only be needed if you haven't used the <code>Manifest Embedded File Provider</code> and your project's <code>root namespace</code> isn't empty. In this case, set your root namespace here.</p>
<p>The <code>root namespace</code> is your project's name by default. You can change it or set it to empty in the <code>csproj</code> file.</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
    &lt;PropertyGroup&gt;
        &lt;RootNamespace&gt;MyModule&lt;/RootNamespace&gt;
    &lt;/PropertyGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;
    &lt;PropertyGroup&gt;
        &lt;RootNamespace&gt;&lt;/RootNamespace&gt;
    &lt;/PropertyGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code class="language-csharp">Configure&lt;AbpVirtualFileSystemOptions&gt;(options =&gt;
{
    options.FileSets.AddEmbedded&lt;MyModule&gt;(baseNamespace: &quot;MyModule&quot;);
});
</code></pre>
<pre><code>[Dir] [/Volo]
[Dir] [/Volo/Abp]
[Dir] [/Volo/Abp/MyModule]
[Dir] [/Volo/Abp/MyModule/Localization]
[File] [/Volo/Abp/MyModule/Localization/en.json]
</code></pre>
<h3>baseFolder</h3>
<p>If you don't want to expose all embedded files in the project, but only want to expose a specific folder (and sub folders/files), you can set the base folder relative to your project root folder.</p>
<blockquote>
<p>baseFolder is only effective when using <code>Manifest Embedded File Provider</code>.</p>
</blockquote>
<p>You can set the <code>baseFolder</code> parameter to <code>/Volo/Abp/MyModule</code>, resulting in this directory structure:</p>
<pre><code class="language-csharp">Configure&lt;AbpVirtualFileSystemOptions&gt;(options =&gt;
{
    options.FileSets.AddEmbedded&lt;MyModule&gt;(baseFolder: &quot;/Volo/Abp/MyModule&quot;);
});
</code></pre>
<pre><code>[Dir] [Localization]
[File] [Localization/en.json]
</code></pre>
<h2>Summary</h2>
<p>We recommend using the <code>Manifest Embedded File Provider</code> in your projects and libraries. Hope this article has been helpful.</p>
<h2>References</h2>
<p><a href="https://abp.io/docs/latest/framework/infrastructure/virtual-file-system">ABP Virtual File System</a></p>
<p><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers#manifest-embedded-file-provider">Manifest Embedded File Provider</a></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a18d7cc-1e4d-b2ff-afa0-00148297b2cc" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a18d7cc-1e4d-b2ff-afa0-00148297b2cc" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-outboxinbox-pattern-for-reliable-event-handling-in-a-multimodule-monolithic-application-eurs9own</guid>
      <link>https://abp.io/community/posts/using-outboxinbox-pattern-for-reliable-event-handling-in-a-multimodule-monolithic-application-eurs9own</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Using Outbox/Inbox Pattern for Reliable Event Handling in a Multi-Module Monolithic Application</title>
      <description>This article explains how to implement reliable event handling using the Outbox/Inbox pattern in a modular monolithic application with multiple databases. </description>
      <pubDate>Sun, 09 Mar 2025 01:03:02 Z</pubDate>
      <a10:updated>2026-09-25T21:36:51Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using Outbox/Inbox Pattern for Reliable Event Handling in a Multi-Module Monolithic Application</h1>
<p>This article explains how to implement reliable event handling using the <code>Outbox/Inbox</code> pattern in a modular monolithic application with multiple databases. We'll use the <code>ModularCRM</code> project as an example (how that project was created is explained in <a href="https://abp.io/docs/latest/tutorials/modular-crm">this document</a>).</p>
<h2>Project Background</h2>
<p><code>ModularCRM</code> is a monolithic application that integrates multiple ABP framework open-source modules, including:</p>
<ul>
<li><code>Account</code></li>
<li><code>Identity</code></li>
<li><code>Tenant Management</code></li>
<li><code>Permission Management</code></li>
<li><code>Setting Management</code></li>
<li>And other open-source modules</li>
</ul>
<p>Besides the ABP framework modules, the project contains three business modules:</p>
<ul>
<li>Order module (<code>Ordering</code>), using <code>MongoDB</code> database</li>
<li>Product module (<code>Products</code>), using <code>SQL Server</code> database</li>
<li>Payment module (<code>Payment</code>), using <code>MongoDB</code> database</li>
</ul>
<p>The project configures separate database connection strings for <code>ModularCRM</code> and the three business modules in <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
  &quot;ConnectionStrings&quot;: {
    &quot;Default&quot;: &quot;Server=localhost,1434;Database=ModularCrm;User Id=sa;Password=1q2w3E***;TrustServerCertificate=true&quot;,
    &quot;Products&quot;: &quot;Server=localhost,1434;Database=ModularCrm_Products;User Id=sa;Password=1q2w3E***;TrustServerCertificate=true&quot;,
    &quot;Ordering&quot;: &quot;mongodb://localhost:27017/ModularCrm_Ordering?replicaSet=rs0&quot;,
    &quot;Payment&quot;: &quot;mongodb://localhost:27017/ModularCrm_Payment?replicaSet=rs0&quot;
  }
}
</code></pre>
<h2>Business Scenario</h2>
<p>These modules communicate through the ABP framework's <code>DistributedEventBus</code> to implement the following business flow:</p>
<blockquote>
<p>This is a simple example flow. Real business flows are more complex. The sample code is for demonstration purposes.</p>
</blockquote>
<ol>
<li>Order module: Publishes <code>OrderPlacedEto</code> event when an order is placed</li>
<li>Product module: Subscribes to <code>OrderPlacedEto</code> event and reduce product stock</li>
<li>Payment module: Subscribes to <code>OrderPlacedEto</code> event, processes payment, then publishes <code>PaymentCompletedEto</code> event</li>
<li>Order module: Subscribes to <code>PaymentCompletedEto</code> event and updates order status to <code>Delivered</code></li>
</ol>
<p>When implementing this flow, we need to ensure:</p>
<ul>
<li>Transaction consistency between order creation and event publishing</li>
<li>Transaction consistency when modules process messages</li>
<li>Reliable message delivery (including persistence, confirmation, and retry mechanisms)</li>
</ul>
<p>Using the default implementation of the ABP framework's distributed event bus cannot meet these requirements, so we need to add a new mechanism that is also provided by the ABP Framework.</p>
<h2>Outbox/Inbox Pattern Solution</h2>
<p>To meet these requirements, we use the <code>Outbox/Inbox</code> pattern:</p>
<h3>Outbox Pattern</h3>
<ul>
<li>Saves distributed events with database operations in the same transaction</li>
<li>Sends events to distributed message service through background jobs</li>
<li>Ensures consistency between data updates and event publishing</li>
<li>Prevents message loss during system failures</li>
</ul>
<h3>Inbox Pattern</h3>
<ul>
<li>First saves received distributed events to the database</li>
<li>Processes events in a transactional way</li>
<li>Ensures messages are processed only once by saving processed message records</li>
<li>Maintains processing state for reliable handling</li>
</ul>
<blockquote>
<p>For how to enable and configure <code>Outbox/Inbox</code> in projects and modules, see: https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events</p>
</blockquote>
<h3>Module Configuration</h3>
<p>Each module needs to configure separate <code>Outbox/Inbox</code>. Since it's a monolithic application, all message processing classes are in the same project, so we need to configure <code>Outbox/Inbox</code> for each module with <code>Selector/EventSelector</code> to ensure that the module only sends and receives the messages it cares about, avoiding message duplication processing.</p>
<p><strong>ModularCRM Main Application Configuration</strong></p>
<p>It will send and receive messages from all ABP framework open-source modules.</p>
<pre><code class="language-csharp">// This selector will match all abp built-in modules and the current module.
Func&lt;Type, bool&gt; abpModuleSelector = type =&gt; type.Namespace != null &amp;&amp; (type.Namespace.StartsWith(&quot;Volo.&quot;) || type.Assembly == typeof(ModularCrmModule).Assembly);

Configure&lt;AbpDistributedEventBusOptions&gt;(options =&gt;
{
    options.Inboxes.Configure(&quot;ModularCrm&quot;, config =&gt;
    {
        config.UseDbContext&lt;ModularCrmDbContext&gt;();
        config.EventSelector = abpModuleSelector;
        config.HandlerSelector = abpModuleSelector;
    });

    options.Outboxes.Configure(&quot;ModularCrm&quot;, config =&gt;
    {
        config.UseDbContext&lt;ModularCrmDbContext&gt;();
        config.Selector = abpModuleSelector;
    });
});
</code></pre>
<p><strong>Order Module Configuration</strong></p>
<p>It only sends <code>OrderPlacedEto</code> events and receives <code>PaymentCompletedEto</code> events and executes <code>OrderPaymentCompletedEventHandler</code>.</p>
<pre><code class="language-csharp">Configure&lt;AbpDistributedEventBusOptions&gt;(options =&gt;
{
    options.Inboxes.Configure(OrderingDbProperties.ConnectionStringName, config =&gt;
    {
        config.UseMongoDbContext&lt;IOrderingDbContext&gt;();
        config.EventSelector = type =&gt; type == typeof(PaymentCompletedEto);
        config.HandlerSelector = type =&gt; type == typeof(OrderPaymentCompletedEventHandler);
    });

    options.Outboxes.Configure(OrderingDbProperties.ConnectionStringName, config =&gt;
    {
        config.UseMongoDbContext&lt;IOrderingDbContext&gt;();
        config.Selector = type =&gt; type == typeof(OrderPlacedEto);
    });
});
</code></pre>
<blockquote>
<p>Here, the <code>EventSelector</code> and <code>HandlerSelector</code> checks only a single type. If you have multiple events and event handlers, you can check the given type if it is included in an array of types.</p>
</blockquote>
<p><strong>Product Module Configuration</strong></p>
<p>It only receives <code>EntityCreatedEto&lt;UserEto&gt;</code> and <code>OrderPlacedEto</code> events and executes <code>ProductsOrderPlacedEventHandler</code> and <code>ProductsUserCreatedEventHandler</code>. It does not send any events now.</p>
<pre><code class="language-csharp">Configure&lt;AbpDistributedEventBusOptions&gt;(options =&gt;
{
    options.Inboxes.Configure(ProductsDbProperties.ConnectionStringName, config =&gt;
    {
        config.UseDbContext&lt;IProductsDbContext&gt;();
		config.EventSelector = type =&gt; type == typeof(EntityCreatedEto&lt;UserEto&gt;) || type == typeof(OrderPlacedEto);
        config.HandlerSelector = type =&gt; type == typeof(ProductsOrderPlacedEventHandler) || type == typeof(ProductsUserCreatedEventHandler);
    });

    // Outboxes are not used in this module
	options.Outboxes.Configure(ProductsDbProperties.ConnectionStringName, config =&gt;
	{
		config.UseDbContext&lt;IProductsDbContext&gt;();
		config.Selector = type =&gt; false;
	});
});
</code></pre>
<p><strong>Payment Module Configuration</strong></p>
<p>It only sends <code>PaymentCompletedEto</code> events and receives <code>OrderPlacedEto</code> events and executes <code>PaymentOrderPlacedEventHandler</code>.</p>
<pre><code class="language-csharp">Configure&lt;AbpDistributedEventBusOptions&gt;(options =&gt;
{
    options.Inboxes.Configure(PaymentDbProperties.ConnectionStringName, config =&gt;
    {
        config.UseMongoDbContext&lt;IPaymentMongoDbContext&gt;();
        config.EventSelector = type =&gt; type == typeof(OrderPlacedEto);
        config.HandlerSelector = type =&gt; type == typeof(PaymentOrderPlacedEventHandler);
    });

    options.Outboxes.Configure(PaymentDbProperties.ConnectionStringName, config =&gt;
    {
        config.UseMongoDbContext&lt;IPaymentMongoDbContext&gt;();
        config.Selector = type =&gt; type == typeof(PaymentCompletedEto);
    });
});
</code></pre>
<h2>Running ModularCRM Simulation Business Flow</h2>
<ol>
<li>Run the following command in the <code>ModularCrm</code> directory:</li>
</ol>
<pre><code># Start SQL Server and MongoDB databases in Docker
docker-compose up -d

# Restore and install project npm dependencies
abp install-lib              

# Migrate databases
dotnet run --project ModularCrm --migrate-database 

# Start the application
dotnet run --project ModularCrm
</code></pre>
<ol start="2">
<li>Navigate to <code>https://localhost:44303/</code> to view the application homepage</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-02-20-Using-OutboxInbox-Pattern-for-Reliable-Event-Handling-in-a-Multi-Module-Monolithic-Application/index.png" alt="index" /></p>
<ol start="3">
<li>Enter a customer name and select a product, then submit an order. After a moment, refresh the page to see the order, product, and payment information.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-02-20-Using-OutboxInbox-Pattern-for-Reliable-Event-Handling-in-a-Multi-Module-Monolithic-Application/order.png" alt="order" /></p>
<p>Application logs display the complete processing flow:</p>
<pre><code>[Ordering Module] Order created:  OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9, CustomerName: john

[Products Module] OrderPlacedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, CustomerName: john, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9
[Products Module] Stock count decreased for ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9

[Payment Module] OrderPlacedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, CustomerName: john, ProductId: 0f95689f-4cb6-36f5-68bd-3a18344d32c9
[Payment Module] Payment processing completed for OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88

[Ordering Module] PaymentCompletedEto event received: OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88, PaymentId: d0a41ead-ee0f-714c-e254-3a1834504d65, PaymentMethod: CreditCard, PaymentAmount: ModularCrm.Payment.Payment.PaymentCompletedEto
[Ordering Module] Order state updated to Delivered for OrderId: b7ad3f47-0e77-bb81-082f-3a1834503e88
</code></pre>
<p>In addition, when a new user registers, the product module will also receive the <code>EntityCreatedEto&lt;UserEto&gt;</code> event, and we will send an email to the new user, just to demonstrate the <code>Outbox/Inbox Selector</code> mechanism.</p>
<pre><code>[Products Module] UserCreated event received: UserId: &quot;9a1f2bd0-5b28-210a-9e56-3a18344d310a&quot;, UserName: admin
[Products Module] Sending a popular products email to admin@abp.io...
</code></pre>
<h2>Summary</h2>
<p>By introducing the <code>Outbox/Inbox</code> pattern, we have achieved:</p>
<ol>
<li>Transactional message sending and receiving</li>
<li>Reliable message processing mechanism</li>
<li>Modular event processing in a multi-database environment</li>
</ol>
<p>ModularCRM project not only implements reliable message processing but also demonstrates how to handle multi-database scenarios gracefully in a monolithic application. Project source code: https://github.com/abpframework/abp-samples/tree/master/ModularCrm-OutboxInbox-Pattern</p>
<h2>Reference</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events">Outbox/Inbox for transactional events</a></li>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/connection-strings">ConnectionStrings</a></li>
<li><a href="https://abp.io/docs/latest/solution-templates/single-layer-web-application">ABP Studio: Single Layer Solution Template</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a188a9a-1d8f-aca9-4b2c-ebdd9c43f516" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a188a9a-1d8f-aca9-4b2c-ebdd9c43f516" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/encryption-and-decryption-in-abp-framework-37uqhdwz</guid>
      <link>https://abp.io/community/posts/encryption-and-decryption-in-abp-framework-37uqhdwz</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Encryption and Decryption in ABP Framework</title>
      <description>The ABP Framework provides various implementations of encryption and decryption to protect sensitive data. Here are three main encryption scenarios and their implementations:</description>
      <pubDate>Fri, 21 Feb 2025 06:22:36 Z</pubDate>
      <a10:updated>2026-09-26T00:14:57Z</a10:updated>
      <content:encoded><![CDATA[<h1>Encryption and Decryption in ABP Framework</h1>
<p>The ABP Framework provides various implementations of encryption and decryption to protect sensitive data. Here are three main encryption scenarios and their implementations:</p>
<h2>User Passwords</h2>
<p>ABP's Identity module uses HMAC-SHA512 combined with PBKDF2 algorithm for password hashing. The process is as follows:</p>
<ul>
<li><p>Encryption process:</p>
<ul>
<li>System generates a random 128-bit salt</li>
<li>Combines password and salt, performs 100,000 iterations using HMAC-SHA512 and PBKDF2 algorithms</li>
<li>Stores the final hash value combined with the salt (Note: stored ciphertext cannot be reversed to plaintext)</li>
</ul>
</li>
<li><p>Verification process:</p>
<ul>
<li>System extracts the stored salt</li>
<li>Recalculates the hash value of the provided password using the same algorithm and iterations</li>
<li>Compares the results; verification succeeds if matched, fails if not</li>
</ul>
</li>
</ul>
<h2>String Encryption</h2>
<p>ABP's <code>IStringEncryptionService</code> uses AES algorithm (CBC mode) for string encryption and decryption. It mainly encrypts and decrypts strings like settings and configuration information. The process is as follows:</p>
<ul>
<li><p>Encryption process:</p>
<ul>
<li>Derives encryption key from passphrase and salt using Rfc2898DeriveBytes (PBKDF2) algorithm</li>
<li>Encrypts using AES algorithm with 256-bit key (controlled by Options.Keysize)</li>
<li>Uses initialization vector (Options.InitVectorBytes) to ensure encryption security</li>
</ul>
</li>
<li><p>Decryption process:</p>
<ul>
<li>Uses the same passphrase and salt</li>
<li>Goes through the same key derivation process</li>
<li>Restores the encrypted content to original text</li>
</ul>
</li>
</ul>
<blockquote>
<p>Note: If you modify any encryption parameters like passphrase, salt, key size, etc., ensure all applications using encryption use the same parameters, otherwise decryption will fail. For example, encrypted settings in the database will become undecryptable.</p>
</blockquote>
<h2>OAuth2/AuthServer Signing and Encryption</h2>
<p>ABP uses the OpenIddict library for OAuth2 authentication server implementation, which uses two types of credentials to protect generated tokens:</p>
<ul>
<li><p>Credential types:</p>
<ul>
<li>Signing credentials: Prevent token tampering, can be asymmetric (like RSA or ECDSA keys) or symmetric</li>
<li>Encryption credentials: Ensure token content confidentiality, prevent unauthorized access and reading</li>
</ul>
</li>
<li><p>Environment configuration:</p>
<ul>
<li>Development environment:
<ul>
<li>Automatically creates two separate RSA certificates</li>
<li>One for signing, another for encryption</li>
</ul>
</li>
<li>Production environment:
<ul>
<li>ABP Studio generates a single RSA certificate (<code>openiddict.pfx</code>) when creating project</li>
<li>This certificate is used for both signing and encryption operations</li>
</ul>
</li>
</ul>
</li>
<li><p>Custom options:</p>
<ul>
<li>Can replace default certificate with self-generated RSA certificate</li>
<li>Supports symmetric encryption (like AES), but not recommended for production</li>
</ul>
</li>
</ul>
<h2>Data Protection</h2>
<p>Besides the above encryption and decryption features, ASP.NET Core's built-in components and services may use data protection, such as encrypting private data in cookies or generating links for email confirmation or password recovery. For details, refer to <a href="https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction">ASP.NET Core Data Protection</a></p>
<h2>Summary</h2>
<p>The ABP Framework protects data security through various encryption mechanisms: from HMAC-SHA512 hashing for user passwords, to AES encryption for configuration information, and RSA certificate signing and encryption in OAuth2 authentication, while also integrating ASP.NET Core's data protection features.</p>
<p>For production environments, it's recommended to use strong passphrases and custom salt values, prioritize asymmetric encryption algorithms, and ensure proper management and backup of all encryption credentials.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing">Hash passwords in ASP.NET Core</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/string-encryption">String Encryption</a></li>
<li><a href="https://documentation.openiddict.com/configuration/encryption-and-signing-credentials">Encryption and signing credentials</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction">ASP.NET Core Data Protection</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a183958-f11c-7965-9af0-c69a19574a85" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a183958-f11c-7965-9af0-c69a19574a85" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/understanding-transactions-in-abp-unit-of-work-0r248xsr</guid>
      <link>https://abp.io/community/posts/understanding-transactions-in-abp-unit-of-work-0r248xsr</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>unit-of-work</category>
      <title>Understanding Transactions in ABP Unit of Work</title>
      <description>The Unit of Work is a software design pattern that maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems to ensure that all changes are made within a single transaction.</description>
      <pubDate>Mon, 03 Feb 2025 10:08:05 Z</pubDate>
      <a10:updated>2026-09-25T23:35:25Z</a10:updated>
      <content:encoded><![CDATA[<h1>Understanding Transactions in ABP Unit of Work</h1>
<p><a href="https://en.wikipedia.org/wiki/Unit_of_work">The Unit of Work</a> is a software design pattern that maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems to ensure that all changes are made within a single transaction.</p>
<h2>Transaction Management Overview</h2>
<p>One of the primary responsibilities of the Unit of Work is managing database transactions. It provides the following transaction management features:</p>
<ul>
<li>Automatically manages database connections and transaction scopes, eliminating the need for manual transaction control</li>
<li>Ensures business operation integrity by making all database operations within a unit of work either succeed or roll back completely</li>
<li>Supports configuration of transaction isolation levels and timeout periods</li>
<li>Supports nested transactions and transaction propagation</li>
</ul>
<h2>Transaction Behavior</h2>
<h3>Default Transaction Settings</h3>
<p>You can modify the default behavior through the following configuration:</p>
<pre><code class="language-csharp">Configure&lt;AbpUnitOfWorkDefaultOptions&gt;(options =&gt;
{
    /*
        Modify the default transaction behavior for all unit of work:
        - UnitOfWorkTransactionBehavior.Enabled: Always enable transactions, all requests will start a transaction
        - UnitOfWorkTransactionBehavior.Disabled: Always disable transactions, no requests will start a transaction
        - UnitOfWorkTransactionBehavior.Auto: Automatically decide whether to start a transaction based on HTTP request type
    */
    options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
    
    // Set default timeout
    options.Timeout = TimeSpan.FromSeconds(30);
    
    // Set default isolation level
    options.IsolationLevel = IsolationLevel.ReadCommitted;
});
</code></pre>
<h3>Automatic Transaction Management</h3>
<p>ABP Framework implements automatic management of Unit of Work and transactions through middlewares, MVC global filters, and interceptors. In most cases, you don't need to manage them manually</p>
<h3>Transaction Behavior for HTTP Requests</h3>
<p>By default, the framework adopts an intelligent transaction management strategy for HTTP requests:</p>
<ul>
<li><code>GET</code> requests won't start a transactional unit of work because there is no data modification</li>
<li>Other HTTP requests (<code>POST/PUT/DELETE</code> etc.) will start a transactional unit of work</li>
</ul>
<h3>Manual Transaction Control</h3>
<p>If you need to manually start a new unit of work, you can customize whether to start a transaction and set the transaction isolation level and timeout:</p>
<pre><code class="language-csharp">// Start a transactional unit of work
using (var uow = _unitOfWorkManager.Begin(
    isTransactional: true,
    isolationLevel: IsolationLevel.RepeatableRead,
    timeout: 30
))
{
    // Execute database operations within transaction
    await uow.CompleteAsync();
}
</code></pre>
<pre><code class="language-csharp">// Start a non-transactional unit of work
using (var uow = _unitOfWorkManager.Begin(
    isTransactional: false
))
{
    // Execute database operations without transaction
    await uow.CompleteAsync();
}
</code></pre>
<h3>Configuring Transactions Using <code>[UnitOfWork]</code> Attribute</h3>
<p>You can customize transaction behavior by using the <code>UnitOfWorkAttribute</code> on methods, classes, or interfaces:</p>
<pre><code class="language-csharp">[UnitOfWork(
    IsTransactional = true,
    IsolationLevel = IsolationLevel.RepeatableRead,
    Timeout = 30
)]
public virtual async Task ProcessOrderAsync(int orderId)
{
    // Execute database operations within transaction
}
</code></pre>
<h3>Non-Transactional Unit of Work</h3>
<p>In some scenarios, you might not need transaction support. You can create a non-transactional unit of work by setting <code>IsTransactional = false</code>:</p>
<pre><code class="language-csharp">public virtual async Task ImportDataAsync(List&lt;DataItem&gt; items)
{
    using (var uow = _unitOfWorkManager.Begin(
        isTransactional: false
    ))
    {
        foreach (var item in items)
        {
            await _repository.InsertAsync(item, autoSave: true);
            // Each InsertAsync will save to database immediately
            // If subsequent operations fail, saved data won't be rolled back
        }

        await uow.CompleteAsync();
    }
}
</code></pre>
<p>Applicable scenarios:</p>
<ul>
<li>Batch import data scenarios where partial success is accepted</li>
<li>Read-only operations, such as queries</li>
<li>Scenarios with low data consistency requirements</li>
</ul>
<h3>Methods to Commit Transactions</h3>
<h4>In Transactional Unit of Work</h4>
<p>A Unit of Work provides several methods to commit changes to the database:</p>
<ol>
<li><strong>IUnitOfWork.SaveChangesAsync</strong></li>
</ol>
<pre><code class="language-csharp">await _unitOfWorkManager.Current.SaveChangesAsync();
</code></pre>
<ol start="2">
<li><strong>autoSave parameter in repositories</strong></li>
</ol>
<pre><code class="language-csharp">await _repository.InsertAsync(entity, autoSave: true);
</code></pre>
<p>Both <code>autoSave</code> and <code>SaveChangesAsync</code> commit changes in the current context to the database. However, these are not applied until <code>CompleteAsync</code> is called. If the unit of work throws an exception or <code>CompleteAsync</code> is not called, the transaction will be rolled back. It means all the DB operations will be reverted back. Only after successfully executing <code>CompleteAsync</code> will the transaction be permanently committed to the database.</p>
<ol start="3">
<li><strong>CompleteAsync</strong></li>
</ol>
<pre><code class="language-csharp">using (var uow = _unitOfWorkManager.Begin())
{
    // Execute database operations
    await uow.CompleteAsync();
}
</code></pre>
<p>When you manually control the Unit of Work with <code>UnitOfWorkManager</code>, the <code>CompleteAsync</code> method is crucial for transaction completion. The unit of work maintains a <code>DbTransaction</code> object internally, and the <code>CompleteAsync</code> method invokes <code>DbTransaction.CommitAsync</code> to commit the transaction. The transaction will not be committed if <code>CompleteAsync</code> is either not executed or fails to execute successfully.</p>
<p>This method not only commits all database transactions but also:</p>
<ul>
<li>Executes and processes all pending domain events within the Unit of Work</li>
<li>Executes all registered post-operations and cleanup tasks within the Unit of Work</li>
<li>Releases all DbTransaction resources upon disposal of the Unit of Work object</li>
</ul>
<blockquote>
<p>Note: <code>CompleteAsync</code> method should be called only once. Multiple calls are not supported.</p>
</blockquote>
<h4>In Non-Transactional Unit of Work</h4>
<p>In non-transactional  Unit of Work, these methods behave differently:</p>
<p>Both <code>autoSave</code> and <code>SaveChangesAsync</code> will persist changes to the database immediately, and these changes cannot be rolled back. Even in non-transactional Unit of Work, calling the <code>CompleteAsync</code> method remains necessary as it handles other essential tasks.</p>
<p>Example:</p>
<pre><code class="language-csharp">using (var uow = _unitOfWorkManager.Begin(isTransactional: false))
{
    // Changes are persisted immediately and cannot be rolled back
    await _repository.InsertAsync(entity1, autoSave: true);
    
    // This operation persists independently of the previous operation
    await _repository.InsertAsync(entity2, autoSave: true);
    
    await uow.CompleteAsync();
}
</code></pre>
<h3>Methods to Roll Back Transactions</h3>
<h4>In Transactional Unit of Work</h4>
<p>A unit of work provides multiple approaches to roll back transactions:</p>
<ol>
<li><strong>Automatic Rollback</strong></li>
</ol>
<p>For transactions automatically managed by the ABP Framework, any uncaught exceptions during the request will trigger an automatic rollback.</p>
<ol start="2">
<li><strong>Manual Rollback</strong></li>
</ol>
<p>For manually managed transactions, you can explicitly invoke the <code>RollbackAsync</code> method to immediately roll back the current transaction.</p>
<blockquote>
<p>Important: Once <code>RollbackAsync</code> is called, the entire  Unit of Work transaction will be rolled back immediately, and any subsequent calls to <code>CompleteAsync</code> will have no effect.</p>
</blockquote>
<pre><code class="language-csharp">using (var uow = _unitOfWorkManager.Begin(
    isTransactional: true,
    isolationLevel: IsolationLevel.RepeatableRead,
    timeout: 30
))
{
    await _repository.InsertAsync(entity);
    
    if (someCondition)
    {
        await uow.RollbackAsync();
        return;
    }
    
    await uow.CompleteAsync();
}
</code></pre>
<p>The <code>CompleteAsync</code> method attempts to commit the transaction. If any exceptions occur during this process, the transaction will not be committed.</p>
<p>Here are two common exception scenarios:</p>
<ol>
<li><strong>Exception Handling Within Unit of Work</strong></li>
</ol>
<pre><code class="language-csharp">using (var uow = _unitOfWorkManager.Begin(
    isTransactional: true,
    isolationLevel: IsolationLevel.RepeatableRead,
    timeout: 30
))
{
    try
    {
        await _bookRepository.InsertAsync(book);
        await uow.SaveChangesAsync();
        await _productRepository.UpdateAsync(product);
        await uow.CompleteAsync();
    }
    catch (Exception)
    {
        // Exceptions can occur in InsertAsync, SaveChangesAsync, UpdateAsync, or CompleteAsync
        // Even if some operations succeed, the transaction remains uncommitted to the database
        // While you can explicitly call RollbackAsync to roll back the transaction,
        // the transaction will not be committed anyway if CompleteAsync fails to execute
        throw;
    }
}
</code></pre>
<ol start="2">
<li><strong>Exception Handling Outside Unit of Work</strong></li>
</ol>
<pre><code class="language-csharp">try
{
    using (var uow = _unitOfWorkManager.Begin(
        isTransactional: true,
        isolationLevel: IsolationLevel.RepeatableRead,
        timeout: 30
    ))
    {
        await _bookRepository.InsertAsync(book);
        await uow.SaveChangesAsync();
        await _productRepository.UpdateAsync(product);
        await uow.CompleteAsync();
    }
}
catch (Exception)
{
    // Exceptions can occur in UpdateAsync, SaveChangesAsync, UpdateAsync, or CompleteAsync
    // Even if some operations succeed, the transaction remains uncommitted to the database
    // Since CompleteAsync was not successfully executed, the transaction will not be committed
    throw;
}
</code></pre>
<h4>In Non-Transactional Unit of Work</h4>
<p>In non-transactional units of work, operations are irreversible. Changes saved using <code>autoSave: true</code> or <code>SaveChangesAsync()</code> are persisted immediately, and the <code>RollbackAsync</code> method has no effect.</p>
<h2>Transaction Management Best Practices</h2>
<h3>1. Remember to Commit Transactions</h3>
<p>When manually controlling transactions, remember to call the <code>CompleteAsync</code> method to commit the transaction after operations are complete.</p>
<h3>2. Pay Attention to Context</h3>
<p>If a unit of work already exists in the current context, <code>UnitOfWorkManager.Begin</code> method and<code> UnitOfWorkAttribute</code> will <strong>reuse it</strong>. Specify <code>requiresNew: true</code> to force create a new unit of work.</p>
<pre><code class="language-csharp">[UnitOfWork]
public async Task Method1()
{
    using (var uow = _unitOfWorkManager.Begin(
        requiresNew: true, 
        isTransactional: true,
        isolationLevel: IsolationLevel.RepeatableRead,
        timeout: 30
    ))
    {
        await Method2();
        await uow.CompleteAsync();
    }
}
</code></pre>
<h3>3. Use <code>virtual</code> Methods</h3>
<p>To be able to use Unit of Work attribute, you must use the <code>virtual</code> modifier for methods in dependency injection class services, because ABP Framework uses interceptors, and it cannot intercept non <code>virtual</code> methods, thus unable to implement  Unit of Work functionality.</p>
<h3>4. Avoid Long Transactions</h3>
<p>Enabling long-running transactions can lead to resource locking, excessive transaction log usage, and reduced concurrent performance, while rollback costs are high and may exhaust database connection resources. It's recommended to split into shorter transactions, reduce lock holding time, and optimize performance and reliability.</p>
<h2>Transaction-Related Recommendations</h2>
<ul>
<li>Choose appropriate transaction isolation levels based on business requirements</li>
<li>Avoid overly long transactions, long-running operations should be split into multiple small transactions</li>
<li>Use the <code>requiresNew</code> parameter reasonably to control transaction boundaries</li>
<li>Pay attention to setting appropriate transaction timeout periods</li>
<li>Ensure transactions can properly roll back when exceptions occur</li>
<li>For read-only operations, it's recommended to use non-transactional  Unit of Work to improve performance</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work">ABP Unit of Work</a></li>
<li><a href="https://docs.microsoft.com/en-us/ef/core/saving/transactions">EF Core Transactions</a></li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel">Transaction Isolation Levels</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a17dd74-ea16-2860-9404-268046f6d064" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a17dd74-ea16-2860-9404-268046f6d064" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/fixing-openiddict-certificate-issues-in-iis-or-azure-0znavo8r</guid>
      <link>https://abp.io/community/posts/fixing-openiddict-certificate-issues-in-iis-or-azure-0znavo8r</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>azure</category>
      <category>openiddict-module</category>
      <category>iis</category>
      <title>Fixing OpenIddict Certificate Issues in IIS or Azure</title>
      <description>When deploying an ABP application with OpenIddict to IIS or Azure, you may encounter issues with loading PFX/PKCS12 certificates. This article explains how to properly configure certificate loading to ensure it works correctly in these environments.</description>
      <pubDate>Thu, 23 Jan 2025 09:14:48 Z</pubDate>
      <a10:updated>2026-09-26T01:41:06Z</a10:updated>
      <content:encoded><![CDATA[<h1>Fixing OpenIddict Certificate Issues in IIS or Azure</h1>
<p>When deploying an ABP application with OpenIddict to IIS or Azure, you may encounter issues with loading PFX/PKCS12 certificates. This article explains how to properly configure certificate loading to ensure it works correctly in these environments.</p>
<h2>The Problem</h2>
<p>When running under IIS or Azure, the application pool identity may not have sufficient permissions to access certificate private keys. This commonly results in errors such as:</p>
<ul>
<li><code>System.Security.Cryptography.CryptographicException: Access denied.</code></li>
<li><code>WindowsCryptographicException: Access is denied.</code></li>
<li><code>System.Security.Cryptography.CryptographicException: The system cannot find the file specified.</code></li>
</ul>
<h2>The Solution</h2>
<h3>Using AddDevelopmentEncryptionAndSigningCertificate</h3>
<p>For development environments using <code>DevelopmentEncryptionAndSigningCertificate</code>, you must configure the application pool to load a user profile.</p>
<blockquote>
<p>Note: We strongly recommend using <code>DevelopmentEncryptionAndSigningCertificate</code> only in development environments. For production, always create and use a separate certificate.</p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-01-23-Fixing-OpenIddict-Certificate-Issues/Load-User-Profile.jpg" alt="Application Pool Configuration" /></p>
<h3>Using AddProductionEncryptionAndSigningCertificate</h3>
<p>The ABP OpenIddict module provides an <code>AddProductionEncryptionAndSigningCertificate</code> extension method. By default, the template project attempts to load an <code>openiddict.pfx</code> certificate in production environments.</p>
<p>To ensure proper certificate loading in IIS or Azure, you need to specify appropriate <code>X509KeyStorageFlags</code> when calling this method:</p>
<pre><code class="language-csharp">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    var hostingEnvironment = context.Services.GetHostingEnvironment();

    if (!hostingEnvironment.IsDevelopment())
    {
       PreConfigure&lt;AbpOpenIddictAspNetCoreOptions&gt;(options =&gt;
       {
          options.AddDevelopmentEncryptionAndSigningCertificate = false;
       });

       PreConfigure&lt;OpenIddictServerBuilder&gt;(serverBuilder =&gt;
       {
         var flag = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet;
         serverBuilder.AddProductionEncryptionAndSigningCertificate(&quot;openiddict.pfx&quot;, &quot;YourCertificatePassword&quot;, flag);
       });
    }
}
</code></pre>
<h2>Understanding X509KeyStorageFlags</h2>
<p>The configuration uses two important flags:</p>
<ul>
<li><code>X509KeyStorageFlags.MachineKeySet</code>: Specifies that the key belongs to the local computer key store, binding the key pair's lifecycle to the computer rather than a specific user.</li>
<li><code>X509KeyStorageFlags.EphemeralKeySet</code>: Indicates that the key will be stored only in memory and not persisted to disk or key store, enhancing security for runtime-only certificate requirements.</li>
</ul>
<p>Using these flags in combination helps prevent permission-related issues in IIS and Azure environments.</p>
<h2>Troubleshooting Guide</h2>
<p>If you continue to experience issues, verify the following:</p>
<ul>
<li>Confirm that the certificate password is correct</li>
<li>Verify that the <code>openiddict.pfx</code> file exists in your deployment</li>
<li>Ensure the certificate is valid - you can generate a new one using:
<pre><code class="language-bash">dotnet dev-certs https -v -ep openiddict.pfx -p YourCertificatePassword
</code></pre>
</li>
<li>Check the stdout logs for related errors (See <a href="https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/deployment-iis?UI=Blazor&amp;DB=EF&amp;Tiered=No#how-to-get-stdout-log">how to get stdout-log</a>)</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/openiddict-deployment">ABP OpenIddict Deployment</a></li>
<li><a href="https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/deployment-iis">ABP IIS Deployment</a></li>
<li><a href="https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/azure-deployment/azure-deployment">ABP Azure Deployment</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-dev-certs#how-to-generate-a-new-certificate">How to Generate a New Certificate</a></li>
<li><a href="https://learn.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities#load-user-profile-for-an-application-pool">Load User Profile in IIS</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a17a49e-2b67-64a3-613d-f84f17cc00cd" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a17a49e-2b67-64a3-613d-f84f17cc00cd" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-global-assets-new-way-to-bundle-javascriptcss-files-in-blazor-webassembly-app-i0nu10rs</guid>
      <link>https://abp.io/community/posts/abp-global-assets-new-way-to-bundle-javascriptcss-files-in-blazor-webassembly-app-i0nu10rs</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>bundling</category>
      <title>ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app</title>
      <description>We have introduced a new feature in the ABP framework to bundle the JavaScript/CSS files in the Blazor wasm app. This feature is called Global Assets. With this feature, you don't need to run the abp bundle command to manually create/maintain the global.js and global.css files in your Blazor wasm app.

</description>
      <pubDate>Tue, 21 Jan 2025 10:28:08 Z</pubDate>
      <a10:updated>2026-09-26T00:10:49Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app</h1>
<p>We have introduced a new feature in the ABP framework to bundle the <code>JavaScript/CSS</code> files in the Blazor wasm app. This feature is called <code>Global Assets</code>.
With this feature, you don't need to run the <code>abp bundle</code> command to manually create/maintain the <code>global.js</code> and <code>global.css</code> files in your Blazor wasm app.</p>
<h2>How Global Assets works?</h2>
<p>The new <code>Blazor wasm app</code> has two projects:</p>
<ol>
<li><code>MyProjectName</code> (ASP.NET Core app)</li>
<li><code>MyProjectName.Client</code> (Blazor wasm app)</li>
</ol>
<p>The <code>MyProjectName</code> reference the <code>MyProjectName.Client</code> project, and will be the entry point of the application, which means the <code>MyProjectName</code> project will be the <code>host</code> project of the <code>MyProjectName.Client</code> project.</p>
<p>The static/virtual files of <code>MyProjectName</code> can be accessed by the <code>MyProjectName.Client</code> project, so we can create dynamic global assets in the <code>MyProjectName</code> project and use them in the <code>MyProjectName.Client</code> project.</p>
<h2>How it works in ABP?</h2>
<p>We have created a new package <code>WebAssembly.Theme.Bundling</code> for the theme <code>WebAssembly</code> module and used the <code>Volo.Abp.AspNetCore.Mvc.UI.Bundling.BundleContributor</code> to add <code>JavaScript/CSS</code> files to the bundling system.</p>
<ul>
<li>LeptonXLiteTheme: <code>AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule</code></li>
<li>LeptonXTheme: <code>AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule</code></li>
<li>LeptonTheme: <code>AbpAspNetCoreComponentsWebAssemblyLeptonThemeBundlingModule</code></li>
<li>BasicTheme: <code>AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule</code></li>
</ul>
<p>The new <code>ThemeBundlingModule</code> only depends on <code>AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule(new package)</code>. It's an <code>abstractions module</code>, which only depends on <code>AbpAspNetCoreMvcUiBundlingAbstractionsModule</code>.</p>
<p>We will get all <code>JavaScript/CSS</code> files on <code>OnApplicationInitializationAsync</code> method of <code>AbpAspNetCoreMvcUiBundlingModule</code> from bundling system and add them to <code>IDynamicFileProvider</code> service. After that, we can access the <code>JavaScript/CSS</code> files in the Blazor wasm app.</p>
<h2>Add the Global Assets in the module</h2>
<p>If your module has <code>JavaScript/CSS</code> files that need to the bundling system, You have to create a new project(<code>YourModuleName.Blazor.WebAssembly.Bundling</code>) to your module solution, and reference the new project in the <code>MyProjectName</code> project and module dependencies.</p>
<p>The new project should <strong>only</strong> depend on the <code>AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule</code> and define <code>BundleContributor</code> classes to contribute the <code>JavaScript/CSS</code> files.</p>
<blockquote>
<p>Q: The new project(<code>YourModuleName.Blazor.WebAssembly.Bundling</code>) doesn't have the <code>libs/myscript.js</code> and <code>libs/myscript.css</code> files why the files can be added to the bundling system?</p>
</blockquote>
<blockquote>
<p>A: Because the <code>MyProjectName.Client</code> will depend on the <code>MyBlazorModule(YourModuleName.Blazor)</code> that contains the <code>JavaScript/CSS</code> files, The <code>MyProjectName</code> is referencing the <code>MyProjectName.Client</code> project, so the <code>MyProjectName</code> project can access the <code>JavaScript/CSS</code> files in the <code>MyProjectName.Client</code> project and add them to the bundling system.</p>
</blockquote>
<pre><code class="language-csharp">[DependsOn(
    typeof(AbpAspNetCoreComponentsWebAssemblyThemingBundlingModule)
)]
public class MyBlazorWebAssemblyBundlingModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure&lt;AbpBundlingOptions&gt;(options =&gt;
        {
            // Script Bundles
            options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleScriptContributor));

            // Style Bundles
            options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global).AddContributors(typeof(MyModuleBundleStyleBundleContributor));
        });
    }
}
</code></pre>
<pre><code class="language-csharp">public class MyModuleBundleScriptContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.AddIfNotContains(&quot;_content/MyModule.Blazor/libs/myscript.js&quot;);
    }
}

public class MyModuleBundleStyleBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.AddIfNotContains(&quot;_content/MyModule.Blazor/libs/myscript.css&quot;);
    }
}
</code></pre>
<h2>Use the Global Assets in the Blazor WASM</h2>
<h3>MyCompanyName.MyProjectName.Blazor</h3>
<p>Convert your <code>MyCompanyName.MyProjectName.Blazor</code> project to integrate the <code>ABP module</code> system and depend on the <code>AbpAspNetCoreMvcUiBundlingModule</code> and <code>AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule</code>:</p>
<ul>
<li>The <code>AbpAspNetCoreMvcUiBundlingModule</code> uses to create the <code>JavaScript/CSS</code> files to virtual files.</li>
<li>The <code>AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule</code> uses to add theme <code>JavaScript/CSS</code> to the bundling system.</li>
</ul>
<p>Here is how your project files look like:</p>
<p><strong><code>Program.cs</code>:</strong></p>
<pre><code class="language-csharp">public class Program
{
    public async static Task&lt;int&gt; Main(string[] args)
    {
        //...

        var builder = WebApplication.CreateBuilder(args);
        builder.Host.AddAppSettingsSecretsJson()
            .UseAutofac()
            .UseSerilog();
        await builder.AddApplicationAsync&lt;MyProjectNameBlazorModule&gt;();
        var app = builder.Build();
        await app.InitializeApplicationAsync();
        await app.RunAsync();
        return 0;

	//...
	}
}
</code></pre>
<p><strong><code>MyProjectNameBlazorModule.cs</code>:</strong></p>
<pre><code class="language-csharp">[DependsOn(
    typeof(AbpAutofacModule),
    typeof(AbpAspNetCoreMvcUiBundlingModule),
    typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule) //Should be added!
)]
public class MyProjectNameBlazorModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        //https://github.com/dotnet/aspnetcore/issues/52530
        Configure&lt;RouteOptions&gt;(options =&gt;
        {
            options.SuppressCheckForUnhandledSecurityMetadata = true;
        });

        // Add services to the container.
        context.Services.AddRazorComponents()
            .AddInteractiveWebAssemblyComponents();
    }

    public override void OnApplicationInitialization(ApplicationInitializationContext context)
    {
        var env = context.GetEnvironment();
        var app = context.GetApplicationBuilder();

        // Configure the HTTP request pipeline.
        if (env.IsDevelopment())
        {
            app.UseWebAssemblyDebugging();
        }
        else
        {
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.MapAbpStaticAssets();
        app.UseRouting();
        app.UseAntiforgery();

        app.UseConfiguredEndpoints(builder =&gt;
        {
            builder.MapRazorComponents&lt;App&gt;()
                .AddInteractiveWebAssemblyRenderMode()
                .AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies&lt;MyProjectNameBlazorClientModule&gt;());
        });
    }
}
</code></pre>
<p><strong><code>MyCompanyName.MyProjectName.Blazor.csproj</code>:</strong></p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
	&lt;PackageReference Include=&quot;Microsoft.AspNetCore.Components.WebAssembly.Server&quot; Version=&quot;9.0.0.0&quot; /&gt;
	&lt;PackageReference Include=&quot;Volo.Abp.Autofac&quot; Version=&quot;9.0.0&quot; /&gt;
	&lt;PackageReference Include=&quot;Volo.Abp.AspNetCore.Mvc.UI.Bundling&quot; Version=&quot;9.0.0&quot; /&gt;
	&lt;PackageReference Include=&quot;Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme.Bundling&quot; Version=&quot;9.0.0&quot; /&gt;
	&lt;!-- &lt;PackageReference Include=&quot;Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme.Bundling&quot; Version=&quot;9.0.0&quot; /&gt;  --&gt; if you're using LeptonXTheme
	&lt;ProjectReference Include=&quot;..\MyProjectName.Blazor.Client\MyProjectName.Blazor.Client.csproj&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<h3>BlazorWebAssemblyBundlingModule in the ABP commercial</h3>
<p>Here is the list of <code>Bundling Modules</code> in the ABP commercial. If you're using the pro template, you should add them to the <code>MyCompanyName.MyProjectName.Blazor</code> project.</p>
<p>| BundlingModules                             | Nuget Package                                        |
|---------------------------------------------|-----------------------------------------------------|
| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling   |
| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling    |
| SaasHostBlazorWebAssemblyBundlingModule       | Volo.Saas.Host.Blazor.WebAssembly.Bundling         |
| ChatBlazorWebAssemblyBundlingModule           | Volo.Chat.Blazor.WebAssembly.Bundling              |
| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling  |</p>
<h3>MyCompanyName.MyProjectName.Blazor.Client</h3>
<ol>
<li>Remove the <code>global.JavaScript/CSS</code> files from the <code>MyCompanyName.MyProjectName.Blazor</code>'s <code>wwwroot</code> folder.</li>
<li>Remove the <code>AbpCli:Bundle</code> section from the <code>appsettings.json</code> file.</li>
<li>Remove all BundleContributor classes that inherit from IBundleContributor. Then, create <code>MyProjectNameStyleBundleContributor</code> and <code>MyProjectNameScriptBundleContributor</code> classes to add your style and JavaScript files. Finally, add them to <code>AbpBundlingOptions</code>.</li>
</ol>
<pre><code class="language-cs">public class MyProjectNameStyleBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Add(new BundleFile(&quot;main.css&quot;, true));
    }
}


public class MyProjectNameScriptBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Add(new BundleFile(&quot;main.js&quot;, true));
    }
}
</code></pre>
<pre><code class="language-cs">Configure&lt;AbpBundlingOptions&gt;(options =&gt;
{
	var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global);
	globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor));
	
	var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global);
	globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor));
});
</code></pre>
<h2>Use the Global Assets in the Blazor WebApp</h2>
<h3>MyCompanyName.MyProjectName.Blazor.WebApp</h3>
<p>Depending on the <code>AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule</code> in your <code>MyCompanyName.MyProjectName.Blazor.WebApp</code> project.</p>
<ul>
<li>The <code>AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule/AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule</code> uses to add theme <code>JavaScript/CSS</code> to the bundling system.</li>
</ul>
<h3>BlazorWebAssemblyBundlingModule in the ABP commercial</h3>
<p>Here is the list of <code>Bundling Modules</code> in the ABP commercial. If you're using the pro template, you should add them to the <code>MyCompanyName.MyProjectName.Blazor.WebApp</code> project.</p>
<p>| BundlingModules                             | Nuget Package                                        |
|---------------------------------------------|-----------------------------------------------------|
| AbpAuditLoggingBlazorWebAssemblyBundlingModule | Volo.Abp.AuditLogging.Blazor.WebAssembly.Bundling   |
| FileManagementBlazorWebAssemblyBundlingModule | Volo.FileManagement.Blazor.WebAssembly.Bundling    |
| SaasHostBlazorWebAssemblyBundlingModule       | Volo.Saas.Host.Blazor.WebAssembly.Bundling         |
| ChatBlazorWebAssemblyBundlingModule           | Volo.Chat.Blazor.WebAssembly.Bundling              |
| CmsKitProAdminBlazorWebAssemblyBundlingModule | Volo.CmsKit.Pro.Admin.Blazor.WebAssembly.Bundling  |</p>
<h3>MyCompanyName.MyProjectName.Blazor.WebApp.Client</h3>
<ol>
<li>Remove the <code>global.JavaScript/CSS</code> files from the <code>MyCompanyName.MyProjectName.Blazor.WebApp.Client</code>'s <code>wwwroot</code> folder.</li>
<li>Remove the <code>AbpCli:Bundle</code> section from the <code>appsettings.json</code> file.</li>
<li>Remove all BundleContributor classes that inherit from IBundleContributor. Then, create <code>MyProjectNameStyleBundleContributor</code> and <code>MyProjectNameScriptBundleContributor</code> classes to add your style and JavaScript files. Finally, add them to <code>AbpBundlingOptions</code>.</li>
</ol>
<pre><code class="language-cs">public class MyProjectNameStyleBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Add(new BundleFile(&quot;main.css&quot;, true));
    }
}


public class MyProjectNameScriptBundleContributor : BundleContributor
{
    public override void ConfigureBundle(BundleConfigurationContext context)
    {
        context.Files.Add(new BundleFile(&quot;main.js&quot;, true));
    }
}
</code></pre>
<pre><code class="language-cs">Configure&lt;AbpBundlingOptions&gt;(options =&gt;
{
	var globalStyles = options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global);
	globalStyles.AddContributors(typeof(MyProjectNameStyleBundleContributor));
	
	var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global);
	globalScripts.AddContributors(typeof(MyProjectNameScriptBundleContributor));
});
</code></pre>
<h3>Check the Global Assets</h3>
<p>Run the <code>MyProject</code> project and check the <code>https://localhost/global.js</code> and <code>https://localhost/global.css</code> files. You should be able to see the <code>JavaScript/CSS</code> files content from the Bundling system:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-11-25-Global-Assets/image.png" alt="global" /></p>
<h2>GlobalAssets(AbpBundlingGlobalAssetsOptions)</h2>
<p>You can configure the JavaScript and CSS file names in the <code>GlobalAssets</code> property of the <code>AbpBundlingOptions</code> class.</p>
<p>The default values are <code>global.js</code> and <code>global.css</code>.</p>
<h2>Conclusion</h2>
<p>With the new <code>Global Assets</code> feature, you can easily bundle the <code>JavaScript/CSS</code> files in the Blazor wasm app. This feature is very useful for the Blazor wasm app, and it will save you a lot of time and effort. We hope you will enjoy this feature and use it in your projects.</p>
<h2>References</h2>
<ul>
<li><a href="https://docs.abp.io/en/abp/latest/Virtual-Files">Virtual Files</a></li>
<li><a href="https://abp.io/docs/latest/framework/ui/mvc-razor-pages/bundling-minification#bundle-contributors">Bundle Contributors</a></li>
<li><a href="https://github.com/abpframework/abp/pull/19968">Global Assets Pull Request</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a179a94-99df-9bd3-0e08-f76f1d2b39a9" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a179a94-99df-9bd3-0e08-f76f1d2b39a9" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/.net-conf-china-2024-intelligence-innovation-openness-we-meet-again-5y82hdun</guid>
      <link>https://abp.io/community/posts/.net-conf-china-2024-intelligence-innovation-openness-we-meet-again-5y82hdun</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>.NET Conf China 2024: Intelligence · Innovation · Openness - We Meet Again!</title>
      <description>**.NET Conf China 2024: `Intelligence · Innovation · Openness` - We Meet Again!****🎉 The Annual .NET Developer Conference Was Successfully Held!**On December 14th, **.NET Conf China 2024** wa</description>
      <pubDate>Mon, 16 Dec 2024 06:39:20 Z</pubDate>
      <content:encoded><![CDATA[<p><strong>.NET Conf China 2024:<code>Intelligence · Innovation · Openness</code> - We Meet Again!</strong></p>
<p><strong>🎉 The Annual .NET Developer Conference Was Successfully Held!</strong></p>
<p>On December 14th, <strong>.NET Conf China 2024</strong> was grandly held at the Zhonggu Xiaonanguo Garden Hotel in Shanghai. With the theme of <code>Intelligence, Innovation, Openness</code>, the conference spotlighted the latest trends and practices in .NET, encompassing artificial intelligence, open-source technology, and enterprise-level development. Technology enthusiasts, enterprise tech leaders, and open-source community members from across the country gathered to celebrate the thriving growth of the .NET ecosystem.</p>
<p>As one of the largest .NET technical conferences in China, this event attracted numerous renowned technical experts and community organizations. It showcased the latest achievements of .NET in supporting multi-platform and multi-architecture environments and provided developers with an inspiring sense of the power of technology and the warmth of the community through a series of sessions and practice exchanges.</p>
<p><img src="https://i.ibb.co/mRwY5GN/2.jpg" alt="image" /></p>
<hr />
<h4><strong>Highlights of the Event: Merging Technical Depth and Breadth</strong></h4>
<p>The conference featured a main venue and three breakout sessions (A, B, and C), covering a wide range of topics in substantial depth.</p>
<hr />
<p><strong>Main Venue: Keynotes and Industry Insights</strong></p>
<p>The main venue opened with a keynote by Steve, Development Director of the .NET Platform Team, who provided an in-depth analysis of <strong>the new features of .NET 9 and its future trajectory</strong> , outlining a blueprint for innovation and efficiency in development. Following that, Microsoft CTO Wei Qing delivered a keynote discussing how enterprises can enhance their core competitiveness with .NET technologies through real-world cases.</p>
<p>The roundtable discussion, themed “Openness and Collaboration,” brought together industry experts such as Wei Qing, Su Zhenwei, Xiao Weiyu, Yi Mingzhi, Zhang Shanyou, and Zhang Guangpo. They explored the future of generative AI, open-source collaboration, and the .NET technology ecosystem in a lively and thought-provoking dialogue.</p>
<p><img src="https://i.ibb.co/Zmyy62N/3.jpg" alt="image" /></p>
<p><img src="https://i.ibb.co/5Kx5RBj/4.jpg" alt="image" /></p>
<p><img src="https://i.ibb.co/0mBSTrB/5.jpg" alt="image" /></p>
<hr />
<p><strong>Breakout Sessions: Comprehensive Technical Discussions</strong></p>
<p>The breakout sessions (A, B, and C) covered diverse topics, including <strong>artificial intelligence, enterprise-level development, open-source practices, and cross-platform development</strong> :</p>
<ul>
<li><strong>AI and .NET Integration</strong> : Sessions focused on practical applications of AI technologies, such as building enterprise-level AI platforms, exploring natural language programming, and applying Semantic Kernel in real-world scenarios.</li>
<li><strong>Deep Dive into Enterprise Applications with .NET</strong> : Insights on optimizing .NET applications for localized systems, enhancing the security of enterprise AI development, and advanced debugging techniques in .NET.</li>
<li><strong>Open Source and Community Innovation</strong> : Developers shared experiences from open-source project development to enterprise application, covering technologies such as Blazor, EFCore9, and Avalonia, along with their adaptation and optimization for local ecosystems.</li>
</ul>
<p>These sessions not only demonstrated the diverse applications of .NET across various fields but also provided actionable insights to help developers refine their technical practices.</p>
<p><img src="https://i.ibb.co/RPzs5Hb/6.jpg" alt="image" /></p>
<hr />
<h4><strong>ABP and Developers: Exploring the Future of Application Development</strong></h4>
<p>As a key product in the .NET ecosystem, the <strong>ABP Team</strong> engaged deeply with developers at the conference. They showcased the unique advantages of ABP in rapidly building modern applications and provided detailed product insights, drawing significant interest from developers.</p>
<p>During booth interactions, developers gained a deeper understanding of the core features of the <code>ABP Framework</code>, the latest updates in the <code>ABP 9.0</code> version, and the all-new <code>ABP Studio</code> tool. They also explored best practices in areas such as <code>enterprise-level application development</code>, <code>modular architecture design</code>, and <code>microservices</code> with ABP team members.</p>
<p><img src="https://i.ibb.co/zXMyNG5/7.jpg" alt="image" /></p>
<p>Additionally, ABP hosted a giveaway, offering prizes like <strong>Bluetooth headphones</strong> and copies of <em>Mastering ABP Framework</em> by Halil İbrahim Kalkan, which added fun and left attendees with a strong impression of the ABP platform.</p>
<p><img src="https://i.ibb.co/bLfCwgJ/8.jpg" alt="image" /></p>
<hr />
<h3><strong>Conclusion and Outlook: Advancing Toward the Future of Technology</strong></h3>
<p>.NET Conf China 2024 was not just a celebration of technology but also a forward-looking exploration of AI's future. AI was a recurring theme throughout the event, with particular emphasis on applications of generative AI and natural language processing. These discussions underscored the enormous potential for deep integration between AI and .NET technologies. From building enterprise AI platforms to making AI smarter and more efficient with .NET, AI is becoming a critical innovation tool for developers and enterprises alike.</p>
<p>The rapid development of AI challenges developers to keep pace with the latest technological trends and applications, driving the evolution of tools and fostering more efficient and intelligent development methods. Looking ahead, we believe AI will continue to integrate deeply with .NET technologies, empowering developers to tackle complex challenges, enhance productivity, and drive broader technological innovation.</p>
<p>Through this conference, developers not only expanded their technical horizons but also deepened their understanding of AI, .NET, and the broader ecosystem. In the future, we look forward to even closer integration of AI technologies with development practices in the .NET ecosystem, paving the way for a smarter era.</p>
<hr />
<p><strong>🌐 Technology Unites, Innovation Knows No Boundaries. See you at the next .NET Conf China!</strong></p>
<p><img src="https://i.ibb.co/j4tgT3N/9.jpg" alt="image" /></p>
<p>As we wrap up this journey, let us turn our attention to Shanghai—a city full of vitality and innovation. The dazzling night view of the Bund, with lights reflecting off both sides of the Huangpu River, perfectly embodies a blend of history and modernity. Just like the theme of <code>.NET Conf China 2024 - Intelligence, Innovation, Openness</code>, Shanghai inspires developers to explore the boundaries of technology and co-create a future of infinite possibilities.</p>
<p>Looking forward, we hope to welcome more tech enthusiasts in this vibrant city, striving together toward a brighter technological future!</p>
<p><img src="https://i.ibb.co/stzCw8q/10.jpg" alt="image" /></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/cc3317aa-f90d-ab5e-42e5-3a16e05e2fc5" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/cc3317aa-f90d-ab5e-42e5-3a16e05e2fc5" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/the-new-unit-test-structure-in-abp-application-4vvvp2oy</guid>
      <link>https://abp.io/community/posts/the-new-unit-test-structure-in-abp-application-4vvvp2oy</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp-essentials</category>
      <title>The new Unit Test structure in ABP application</title>
      <description>Using abstract unit test classes involves first writing tests in the Application and Domain layers that are independent of the storage technology, ensuring the correctness of core business logic.</description>
      <pubDate>Tue, 10 Dec 2024 07:48:59 Z</pubDate>
      <a10:updated>2026-09-26T00:08:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>The new Unit Test structure in ABP application</h1>
<p>A typical ABP modular project usually consists of three main projects: <code>Application</code>, <code>Domain</code>, and <code>EntityFrameworkCore/MongoDB</code>. In these projects, we may provide many services that require unit testing.</p>
<p>Using abstract unit test classes involves first writing tests in the <code>Application</code> and <code>Domain</code> layers that are independent of the storage technology, ensuring the correctness of core business logic. These abstract tests are then implemented in <code>EntityFrameworkCore</code> or <code>MongoDB</code>. The benefits of this approach include:</p>
<ol>
<li><strong>Reduced Coupling</strong>: Core logic tests do not depend on specific storage technologies, so switching databases does not require rewriting test code.</li>
<li><strong>Better Isolation</strong>: Focuses on verifying business logic correctness, avoiding interference from database operations.</li>
<li><strong>Increased Reusability</strong>: The same abstract tests can be reused with different storage implementations.</li>
<li><strong>Easier Maintenance and Extensibility</strong>: Different storage implementations can be extended independently without breaking existing tests.</li>
<li><strong>Faster and More Reliable Tests</strong>: Reduces dependency on databases, making tests faster and more stable.</li>
</ol>
<h2>How to migrate old unit tests to the new unit test structure</h2>
<p>Assume our project name is <code>MyCompanyName.MyProjectName</code>.</p>
<h3>Changes to the <code>MyCompanyName.MyProjectName.Application.Tests</code> project:</h3>
<ol>
<li>Remove the <code>MyCompanyName.MyProjectName.Application.Tests</code> project's <code>MyProjectNameApplicationCollection</code> class.</li>
<li>Modify the <code>MyCompanyName.MyProjectName.Application.Tests</code> project's <code>MyProjectNameApplicationTestBase</code> class.</li>
</ol>
<pre><code class="language-csharp">public abstract class MyProjectNameApplicationTestBase&lt;TStartupModule&gt; : MyProjectNameTestBase&lt;TStartupModule&gt;
    where TStartupModule : IAbpModule
{
	//...
}
</code></pre>
<ol start="3">
<li>Modify the <code>MyCompanyName.MyProjectName.Application.Tests</code> project's unit test classes to become abstract unit test classes, such as: <code>SampleAppServiceTests</code>.</li>
</ol>
<pre><code class="language-csharp">public abstract class SampleAppServiceTests&lt;TStartupModule&gt; : MyProjectNameApplicationTestBase&lt;TStartupModule&gt;
    where TStartupModule : IAbpModule
{
    [Fact]
    public async Task Initial_Data_Should_Contain_Admin_User()
    {
        //...
    }
}
</code></pre>
<h3>Changes to the <code>MyCompanyName.MyProjectName.Domain.Tests</code> project:</h3>
<ol>
<li>Remove the <code>MyCompanyName.MyProjectName.Domain.Tests</code> project's <code>MyProjectNameDomainCollection</code> class.</li>
<li>Modify the <code>MyCompanyName.MyProjectName.Domain.Tests</code> project's <code>MyProjectNameDomainTestBase</code> class.</li>
</ol>
<pre><code class="language-csharp">public abstract class MyProjectNameDomainTestBase&lt;TStartupModule&gt; : MyProjectNameTestBase&lt;TStartupModule&gt;
    where TStartupModule : IAbpModule
{
	//...
}
</code></pre>
<ol start="3">
<li>Modify the <code>MyCompanyName.MyProjectName.Domain.Tests</code> project's unit test classes to become abstract unit test classes, such as: <code>SampleDomainTests</code>.</li>
</ol>
<pre><code class="language-csharp">public abstract class SampleDomainTests&lt;TStartupModule&gt; : MyProjectNameDomainTestBase&lt;TStartupModule&gt;
    where TStartupModule : IAbpModule
{
    [Fact]
    public async Task Should_Set_Email_Of_A_User()
    {
		//...
    }
}
</code></pre>
<ol start="4">
<li>Modify the <code>MyCompanyName.MyProjectName.Domain.Tests</code> project's <code>csproj</code> and module class. Remove references to <code>EntityFrameworkCore/MongoDB</code>.</li>
</ol>
<p><code>MyCompanyName.MyProjectName.Domain.Tests.csproj</code>:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  //...

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.Domain\MyCompanyName.MyProjectName.Domain.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.TestBase\MyCompanyName.MyProjectName.TestBase.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;

</code></pre>
<p><code>MyProjectNameDomainTestModule.cs</code>:</p>
<pre><code class="language-csharp">[DependsOn(
    typeof(MyProjectNameDomainModule),
    typeof(MyProjectNameTestBaseModule)
)]
public class MyProjectNameDomainTestModule : AbpModule
{
	//...
}
</code></pre>
<h3>Changes to the <code>MyCompanyName.MyProjectName.EntityFrameworkCore.Tests</code> project:</h3>
<p>Here, we need to create implementation classes for all abstract unit tests.</p>
<pre><code class="language-csharp">[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class EfCoreSampleAppServiceTests : SampleAppServiceTests&lt;MyProjectNameEntityFrameworkCoreTestModule&gt;
{
	//...
}
</code></pre>
<pre><code class="language-csharp">[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class EfCoreSampleDomainTests : SampleDomainTests&lt;MyProjectNameEntityFrameworkCoreTestModule&gt;
{
	//...
}
</code></pre>
<p>We also need to modify the project's dependencies and module class, which should directly or indirectly reference the <code>Application</code> and <code>Domain</code> test projects.</p>
<p><code>MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj</code>:</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

   //...

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.EntityFrameworkCore\MyCompanyName.MyProjectName.EntityFrameworkCore.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\..\..\..\..\framework\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<p><code>MyProjectNameEntityFrameworkCoreTestModule.cs</code>:</p>
<pre><code class="language-csharp">[DependsOn(
    typeof(MyProjectNameApplicationTestModule),
    typeof(MyProjectNameEntityFrameworkCoreModule),
    typeof(AbpEntityFrameworkCoreSqliteModule)
    )]
public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule
{
	//...
}
</code></pre>
<h3>Changes to the <code>MyCompanyName.MyProjectName.MongoDB.Tests</code> project (skip this step if not using MongoDB):</h3>
<p>Like the <code>EntityFrameworkCore</code> project, we need to create implementation classes for all abstract unit tests and modify the project's dependencies and module class.</p>
<pre><code class="language-csharp">[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class MongoDBSampleAppServiceTests : SampleAppServiceTests&lt;MyProjectNameMongoDbTestModule&gt;
{
	//...
}
</code></pre>
<pre><code class="language-csharp">[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]
public class MongoDBSampleDomainTests : SampleDomainTests&lt;MyProjectNameMongoDbTestModule&gt;
{
	//...
}
</code></pre>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  //...

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.MongoDB\MyCompanyName.MyProjectName.MongoDB.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code class="language-csharp">[DependsOn(
    typeof(MyProjectNameApplicationTestModule),
    typeof(MyProjectNameMongoDbModule)
)]
public class MyProjectNameMongoDbTestModule : AbpModule
{
	//...
}
</code></pre>
<h3>Changes to the <code>MyCompanyName.MyProjectName.Web.Tests</code> project:</h3>
<p>We need to reference the <code>EntityFrameworkCore/MongoDB</code> test projects in this test project.</p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  //...

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.Web\MyCompanyName.MyProjectName.Web.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\..\..\..\..\framework\src\Volo.Abp.AspNetCore.TestBase\Volo.Abp.AspNetCore.TestBase.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.EntityFrameworkCore.Tests\MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<pre><code class="language-csharp">[DependsOn(
    typeof(AbpAspNetCoreTestBaseModule),
    typeof(MyProjectNameWebModule),
    typeof(MyProjectNameApplicationTestModule),
    typeof(MyProjectNameEntityFrameworkCoreTestModule)
)]
public class MyProjectNameWebTestModule : AbpModule
{
	//...
}
</code></pre>
<p>We no longer need the <code>MyProjectNameWebCollection</code> class in this project. Please delete it and use <code>[Collection(MyProjectNameTestConsts.CollectionDefinitionName)]</code> instead.</p>
<h2>Conclusion</h2>
<p>This is our new unit test structure. Decoupling unit tests from storage technologies ensures the independence of business logic and allows easy switching between storage implementations. Abstract unit test classes improve test reusability, maintainability, and efficiency, reducing refactoring costs and providing flexibility for future tech updates.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/testing/unit-tests">Unit Test</a></li>
<li><a href="https://github.com/abpframework/abp/pull/17880">Abstract all db-related unit tests</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/optimizing-static-asset-delivery-feature-in-asp.net-core-9.0-gyv140vb</guid>
      <link>https://abp.io/community/posts/optimizing-static-asset-delivery-feature-in-asp.net-core-9.0-gyv140vb</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>dotnet-9.0</category>
      <title>Optimizing Static Asset Delivery feature in ASP.NET Core 9.0</title>
      <description>Delivering static assets efficiently is a key factor in building performant web applications. By optimizing how assets like CSS, JavaScript, and images are served to the browser, you can reduce load times, decrease network traffic, and improve the overall user experience.

</description>
      <pubDate>Wed, 13 Nov 2024 09:51:00 Z</pubDate>
      <a10:updated>2026-09-25T23:36:44Z</a10:updated>
      <content:encoded><![CDATA[<h1>Optimizing Static Asset Delivery feature in ASP.NET Core 9.0</h1>
<p>Delivering static assets efficiently is a key factor in building performant web applications. By optimizing how assets like CSS, JavaScript, and images are served to the browser, you can reduce load times, decrease network traffic, and improve the overall user experience.</p>
<p>One powerful tool to help achieve this is <strong>MapStaticAssets</strong>, a feature in ASP.NET Core that significantly optimizes the delivery of static resources. Whether you're working with Blazor, Razor Pages, MVC, or other UI frameworks, <strong>MapStaticAssets</strong> streamlines asset management and ensures that your web app delivers resources in the most efficient way possible.</p>
<h2>Why Optimizing Static Assets Matters</h2>
<p>Serving static assets without optimization can lead to several performance bottlenecks:</p>
<ul>
<li><strong>Excessive network requests</strong>: The browser may need to request the same resources multiple times, even if they haven’t changed.</li>
<li><strong>Unnecessary data transfer</strong>: Larger files are sent over the network, consuming bandwidth and slowing down page loads.</li>
<li><strong>Outdated assets</strong>: Without proper cache management, users may receive stale versions of files after an app update.</li>
</ul>
<p>Optimizing static assets involves compressing files, managing caching headers, and ensuring that only the necessary resources are sent to the client. <strong>MapStaticAssets</strong> takes care of all these issues in a seamless, automated way.</p>
<h2>What is MapStaticAssets?</h2>
<p><strong>MapStaticAssets</strong> is designed to enhance the default static asset serving mechanism in ASP.NET Core. It can replace <code>UseStaticFiles</code> in most scenarios and comes with several built-in optimizations. These optimizations are executed at both build and publish time, ensuring that static resources are served in the most efficient way possible when your app is running.</p>
<p>Here's how you can implement <strong>MapStaticAssets</strong> in your app:</p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(&quot;/Error&quot;);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

// Replacing UseStaticFiles with MapStaticAssets
app.MapStaticAssets();
app.MapRazorPages();

app.Run();
</code></pre>
<h2>Key Features of MapStaticAssets</h2>
<ol>
<li><p><strong>Build-time Compression</strong>:<br />
<strong>MapStaticAssets</strong> automatically compresses all static assets during the build process. It uses <strong>gzip</strong> compression during development and <strong>gzip + brotli</strong> compression when publishing. This reduces the file size significantly, ensuring faster download times.</p>
<p>For example, in a default Razor Pages template, assets like <code>bootstrap.min.css</code> and <code>jquery.js</code> are compressed by over 80%, resulting in significantly reduced file sizes:</p>
<p>| File                 | Original Size | Compressed Size | Compression Reduction |
|----------------------|---------------|-----------------|-----------------------|
| <code>bootstrap.min.css</code>  | 163 KB        | 17.5 KB         | 89.26%                |
| <code>jquery.js</code>          | 89.6 KB       | 28 KB           | 68.75%                |
| <code>bootstrap.min.js</code>   | 78.5 KB       | 20 KB           | 74.52%                |
| <strong>Total</strong>            | 331.1 KB      | 65.5 KB         | 80.20%                |</p>
</li>
<li><p><strong>Content-based ETags</strong>:<br />
<strong>MapStaticAssets</strong> generates <strong>ETags</strong> based on the SHA-256 hash of the file content, encoded in Base64. This ensures that the browser only re-downloads a resource if its content has changed. This eliminates unnecessary network requests, improving page load speeds.</p>
</li>
<li><p><strong>Smaller File Sizes for Libraries</strong>:<br />
Popular component libraries, such as <strong>Fluent UI Blazor</strong> and <strong>MudBlazor</strong>, benefit from similar compression optimizations. For example, the size of the <strong>MudBlazor</strong> library is reduced by over 90%, from 588 KB to just 46.7 KB after compression.</p>
<p>| File                 | Original Size | Compressed Size | Compression Reduction |
|----------------------|---------------|-----------------|-----------------------|
| <code>MudBlazor.min.css</code>  | 541 KB        | 37.5 KB         | 93.07%                |
| <code>MudBlazor.min.js</code>   | 47.4 KB       | 9.2 KB          | 80.59%                |
| <strong>Total</strong>            | 588.4 KB      | 46.7 KB         | 92.07%                |</p>
</li>
<li><p><strong>Automatic Optimization</strong>:<br />
As libraries or components are added or updated, <strong>MapStaticAssets</strong> automatically optimizes the assets as part of the build process. This includes minimizing the size of JavaScript and CSS files, reducing the impact of mobile or low-bandwidth environments.</p>
</li>
<li><p><strong>Serving Assets with a CDN</strong>:<br />
Although <strong>MapStaticAssets</strong> is focused on server-side optimizations, integrating a <strong>CDN (Content Delivery Network)</strong> can further boost performance by serving static assets from servers geographically closer to the user, reducing latency.</p>
</li>
</ol>
<h2>Comparing MapStaticAssets to IIS Dynamic Compression</h2>
<p><strong>MapStaticAssets</strong> provides several advantages over traditional dynamic compression techniques, such as IIS <strong>gzip</strong> compression:</p>
<ul>
<li><strong>Simplicity</strong>: There is no need for server-specific configuration, making <strong>MapStaticAssets</strong> easy to implement.</li>
<li><strong>Performance</strong>: By compressing assets at build time, the app doesn't need to perform compression during every request, which improves server performance.</li>
<li><strong>Optimization</strong>: Developers can focus on ensuring that assets are compressed to the smallest possible size during the build process.</li>
</ul>
<p>For example, using <strong>MapStaticAssets</strong>, a file like <code>MudBlazor.min.css</code> is compressed down to 37.5 KB, whereas IIS dynamic compression might result in a size of 90 KB. This represents a <strong>59%</strong> reduction in size.</p>
<h2>About MapAbpStaticAssets</h2>
<p>The ABP framework is 100% compatible with this new feature.</p>
<p>However, some JavaScript, CSS, and image files exist in the <a href="https://abp.io/docs/latest/framework/infrastructure/virtual-file-system">Virtual File System</a>, which ASP.NET Core's <strong>MapStaticAssets</strong> can't handle. For these files, additional <strong>StaticFileMiddleware</strong> is needed to serve them, which is where <strong>MapAbpStaticAssets</strong> comes in.</p>
<p><strong>MapAbpStaticAssets</strong> adds the necessary <strong>StaticFileMiddleware</strong> to ensure that virtual files are correctly served. This middleware setup ensures seamless delivery of virtual resources alongside static assets.</p>
<p>You can view the source code of <strong>MapAbpStaticAssets</strong> on <a href="https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs#L129-L198">GitHub</a>.</p>
<h2>Conclusion</h2>
<p>Optimizing static asset delivery is essential for building fast, efficient web applications. <strong>MapStaticAssets</strong> simplifies and automates the optimization of static files by providing build-time compression, caching headers, and content-based ETags. This ensures that your app's static assets are always delivered in the most efficient way, whether users are on fast broadband or slower mobile connections. By using <strong>MapStaticAssets</strong>, you can deliver a faster, more reliable experience for your users with minimal effort.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/static-files?view=aspnetcore-9.0">Static files in ASP.NET Core</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-8.0#optimize-static-web-asset-delivery">What's new in ASP.NET Core 9.0</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a16371b-ce37-8ccd-41b6-b892d75192ce" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a16371b-ce37-8ccd-41b6-b892d75192ce" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/switching-between-organization-units-i5tokpzt</guid>
      <link>https://abp.io/community/posts/switching-between-organization-units-i5tokpzt</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>Switching Between Organization Units</title>
      <description>In most companies, a user belongs to more than one organization. Also, in some applications, we need to filter the data shown depending on the logged-in user's organization. For such scenarios, allowing users to select one of the organizations they belong to is a good practice.</description>
      <pubDate>Thu, 15 Aug 2024 12:11:09 Z</pubDate>
      <a10:updated>2026-09-26T01:54:34Z</a10:updated>
      <content:encoded><![CDATA[<h1>Switching Between Organization Units</h1>
<p>In most companies, a user belongs to more than one organization. Also, in some applications, we need to filter the data shown depending on the logged-in user's organization. For such scenarios, allowing users to select one of the organizations they belong to is a good practice.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2024-08-01-switching-between-organization-units/0.png" alt="0" /></p>
<h2>Creating a Data Filter with Organization Units</h2>
<h3>IHasOrganization</h3>
<p>First, we need to create a data filter that filters data based on the organization unit.</p>
<p>The <code>IHasOrganization</code> interface is used to define the organization unit property in the entity classes, and used to filter the data based on the organization unit.</p>
<pre><code class="language-csharp">public interface IHasOrganization
{
    public Guid? OrganizationId { get; set; }
}
</code></pre>
<pre><code class="language-csharp">public class Book : AggregateRoot&lt;Guid&gt;, IHasOrganization
{
    public string Name { get; set; }

    public string Isbn { get; set; }

    public Guid? OrganizationId { get; set; }
}
</code></pre>
<h3>Entity Framework Core DbContext Implementation</h3>
<p>We will override the <code>ShouldFilterEntity</code> and <code>CreateFilterExpression</code> methods in the <code>BookStoreDbContext</code> class to configure the data filter for the entity that implements the <code>IHasOrganization</code> interface.</p>
<pre><code class="language-csharp">public class BookStoreDbContext : AbpDbContext&lt;BookStoreDbContext&gt;
{
	// Your others DbSet properties...

    public DbSet&lt;Book&gt; Books { get; set; }

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

        // Your configure code...

        builder.Entity&lt;Book&gt;(b =&gt;
        {
            b.ToTable(BookStoreConsts.DbTablePrefix + &quot;Book&quot;, BookStoreConsts.DbSchema);
            b.ConfigureByConvention();
        });
    }

    public CurrentOrganizationIdProvider CurrentOrganizationIdProvider =&gt; LazyServiceProvider.LazyGetRequiredService&lt;CurrentOrganizationIdProvider&gt;();

    protected override bool ShouldFilterEntity&lt;TEntity&gt;(IMutableEntityType entityType)
    {
        if (typeof(IHasOrganization).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)
    {
        var expression = base.CreateFilterExpression&lt;TEntity&gt;(modelBuilder);

        if (typeof(IHasOrganization).IsAssignableFrom(typeof(TEntity)))
        {
            Expression&lt;Func&lt;TEntity, bool&gt;&gt; hasOrganizationIdFilter = e =&gt; EF.Property&lt;Guid&gt;(e, &quot;OrganizationId&quot;) == CurrentOrganizationIdProvider.CurrentOrganizationId;
            expression = expression == null ? hasOrganizationIdFilter : QueryFilterExpressionHelper.CombineExpressions(expression, hasOrganizationIdFilter);
        }

        return expression;
    }
}
</code></pre>
<h3>The CurrentOrganizationIdProvider</h3>
<p>This class is used to get the current <code>organization id</code>, We will use the <code>AsyncLocal</code> class to store the current <code>organization id</code>, The <code>Change</code> method is used to change the current <code>organization id</code>. This service is registered as a singleton service.</p>
<pre><code class="language-csharp">public class CurrentOrganizationIdProvider : ISingletonDependency
{
    private readonly AsyncLocal&lt;Guid?&gt; _currentOrganizationId = new AsyncLocal&lt;Guid?&gt;();

    public Guid? CurrentOrganizationId  =&gt; _currentOrganizationId.Value;

    public virtual IDisposable Change(Guid? organizationId)
    {
        var parent = CurrentOrganizationId;
        _currentOrganizationId.Value = organizationId;
        return new DisposeAction(() =&gt;
        {
            _currentOrganizationId.Value = parent;
        });
    }
}
</code></pre>
<h2>Domain Service Implementation</h2>
<p>We will store the current <code>organization id</code> in the cache for the logged-in user. at the same time, we want to store it per browser. So we also add the different browser info for every logged-in user.</p>
<p>In the <code>BrowserInfoClaimsPrincipalContributor</code> class, We add a random <code>BrowserInfo</code> claim to the logged-in user. And we will use <code>user id</code> and <code>browser info </code>as a cache key.</p>
<pre><code class="language-csharp">public static class CurrentUserExtensions
{
    public static Guid? GetBrowserInfo(this ICurrentUser currentUser)
    {
        var claimValue = currentUser.FindClaimValue(&quot;BrowserInfo&quot;);
        if (claimValue != null &amp;&amp; Guid.TryParse(claimValue, out var result))
        {
            return result;
        }
        return null;
    }
}

public class BrowserInfoClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
{
    public Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
    {
        var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
        identity?.AddClaim(new Claim(&quot;BrowserInfo&quot;, Guid.NewGuid().ToString()));
        return Task.CompletedTask;
    }
}
</code></pre>
<h2>Application Service Implementation</h2>
<p>The <code>CurrentOrganizationAppService</code> to get/change the current organization for the logged-in user. <code>BookAppService</code> to get the books based on the current <code>organization id</code>.</p>
<pre><code class="language-csharp">[Authorize]
public class CurrentOrganizationAppService : BookStoreAppService, ICurrentOrganizationAppService
{
    private readonly IdentityUserManager _identityUserManager;
    private readonly IDistributedCache&lt;CurrentOrganizationIdCacheItem&gt; _cache;

    public CurrentOrganizationAppService(IdentityUserManager identityUserManager, IDistributedCache&lt;CurrentOrganizationIdCacheItem&gt; cache)
    {
        _identityUserManager = identityUserManager;
        _cache = cache;
    }

    public virtual async Task&lt;List&lt;OrganizationDto&gt;&gt; GetOrganizationListAsync()
    {
        var user = await _identityUserManager.FindByIdAsync(CurrentUser.GetId().ToString());
        var organizationUnits = await _identityUserManager.GetOrganizationUnitsAsync(user);
        return organizationUnits.Select(ou =&gt; new OrganizationDto
        {
            Id = ou.Id,
            DisplayName = ou.DisplayName
        }).ToList();
    }

    public virtual async Task&lt;Guid?&gt; GetCurrentOrganizationIdAsync()
    {
        var cacheKey = CurrentUser.Id.ToString() + &quot;:&quot; + CurrentUser.GetBrowserInfo();
        return (await _cache.GetAsync(cacheKey))?.OrganizationId;
    }

    public virtual async Task ChangeAsync(Guid? organizationId)
    {
        var cacheKey = CurrentUser.Id.ToString() + &quot;:&quot; + CurrentUser.GetBrowserInfo();
        await _cache.SetAsync(cacheKey, new CurrentOrganizationIdCacheItem
        {
            OrganizationId = organizationId
        });
    }
}
</code></pre>
<pre><code class="language-csharp">[Authorize]
public class BookAppService : BookStoreAppService, IBookAppService
{
    private readonly IBasicRepository&lt;Book, Guid&gt; _bookRepository;

    public BookAppService(IBasicRepository&lt;Book, Guid&gt; bookRepository)
    {
        _bookRepository = bookRepository;
    }

    public virtual async Task&lt;List&lt;BookDto&gt;&gt; GetListAsync()
    {
        var books = await _bookRepository.GetListAsync();
        return books.Select(book =&gt; new BookDto
        {
            Id = book.Id,
            Name = book.Name,
            Isbn = book.Isbn,
            OrganizationId = book.OrganizationId
        }).ToList();
    }
}
</code></pre>
<h2>Seed Sample Data</h2>
<p>Let's seed some sample data for the <code>Book</code> and <code>Organization</code> entities.</p>
<p>We added two organization units, <code>USA Branch</code> and <code>Turkey Branch</code>, and some books to each organization unit. Also, we added the <code>admin</code> user to both organization units.</p>
<pre><code class="language-csharp">public class BooksDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    public Guid UsaBranchId = Guid.Parse(&quot;00000000-0000-0000-0000-000000000001&quot;);
    public Guid TurkeyBranchId = Guid.Parse(&quot;00000000-0000-0000-0000-000000000002&quot;);

    private readonly IBasicRepository&lt;Book, Guid&gt;  _bookRepository;
    private readonly OrganizationUnitManager _organizationUnitManager;
    private readonly IOrganizationUnitRepository _organizationUnitRepository;
    private readonly IdentityUserManager _identityUserManager;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    public BooksDataSeedContributor(
        IBasicRepository&lt;Book, Guid&gt; bookRepository,
        OrganizationUnitManager organizationUnitManager,
        IOrganizationUnitRepository organizationUnitRepository,
        IdentityUserManager identityUserManager,
        IUnitOfWorkManager unitOfWorkManager)
    {
        _bookRepository = bookRepository;
        _organizationUnitManager = organizationUnitManager;
        _organizationUnitRepository = organizationUnitRepository;
        _identityUserManager = identityUserManager;
        _unitOfWorkManager = unitOfWorkManager;
    }

    public virtual async Task SeedAsync(DataSeedContext context)
    {
        using (var uow = _unitOfWorkManager.Begin())
        {
            var usa = await _organizationUnitRepository.FindAsync(UsaBranchId);
            if (usa == null)
            {
                await _organizationUnitManager.CreateAsync(new OrganizationUnit(UsaBranchId, &quot;USA Branch&quot;));
            }

            var turkey = await _organizationUnitRepository.FindAsync(TurkeyBranchId);
            if (turkey == null)
            {
                await _organizationUnitManager.CreateAsync(new OrganizationUnit(TurkeyBranchId, &quot;Turkey Branch&quot;));
            }

            await uow.SaveChangesAsync();

            var admin = await _identityUserManager.FindByNameAsync(&quot;admin&quot;);
            Check.NotNull(admin, &quot;admin&quot;);

            await _identityUserManager.AddToOrganizationUnitAsync(admin.Id, UsaBranchId);
            await _identityUserManager.AddToOrganizationUnitAsync(admin.Id, TurkeyBranchId);

            if (await _bookRepository.GetCountAsync() &lt;= 0)
            {
                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;1984&quot;,
                    Isbn = &quot;978-0451524935&quot;,
                    OrganizationId = UsaBranchId
                });

                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;Animal Farm&quot;,
                    Isbn = &quot;978-0451526342&quot;,
                    OrganizationId = UsaBranchId
                });

                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;Brave New World&quot;,
                    Isbn = &quot;978-0060850524&quot;,
                    OrganizationId = UsaBranchId
                });

                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;Fahrenheit 451&quot;,
                    Isbn = &quot;978-1451673319&quot;,
                    OrganizationId = TurkeyBranchId
                });

                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;The Catcher in the Rye&quot;,
                    Isbn = &quot;978-0316769488&quot;,
                    OrganizationId = TurkeyBranchId
                });

                await _bookRepository.InsertAsync(new Book
                {
                    Name = &quot;To Kill a Mockingbird&quot;,
                    Isbn = &quot;978-0061120084&quot;,
                    OrganizationId = TurkeyBranchId
                });
            }

            await uow.CompleteAsync();
        }
    }
}
</code></pre>
<h2>Web Page Implementation</h2>
<p>We will add a dropdown list to the top right corner of the page to allow users to select the organization they belong to. When the dropdown list changes, we will call the application service api to change the current <code>organization id</code>.</p>
<pre><code class="language-csharp">public class OrganizationUnitComponent : AbpViewComponent
{
    public async Task&lt;IViewComponentResult&gt; InvokeAsync()
    {
        var currentOrganizationAppService = LazyServiceProvider.GetRequiredService&lt;ICurrentOrganizationAppService&gt;();
        var organizationDtos = await currentOrganizationAppService.GetOrganizationListAsync();
        var currentOrganizationId = await currentOrganizationAppService.GetCurrentOrganizationIdAsync();
        return View(&quot;/Components/OrganizationUnits/Default.cshtml&quot;, new OrganizationUnitComponentModel
        {
            CurrentOrganizationId = currentOrganizationId,
            OrganizationDtos = organizationDtos
        });
    }
}

public class OrganizationUnitComponentModel
{
    public Guid? CurrentOrganizationId { get; set; }

    public List&lt;OrganizationDto&gt; OrganizationDtos { get; set; }
}
</code></pre>
<pre><code class="language-html">@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers
@model BookStore.Web.Components.OrganizationUnits.OrganizationUnitComponentModel

&lt;div class=&quot;dropstart&quot;&gt;
    &lt;a href=&quot;#&quot; class=&quot;btn mt-2&quot; data-bs-toggle=&quot;dropdown&quot; type=&quot;button&quot;&gt;
        &lt;i class=&quot;fa fa-city m-auto&quot;&gt;&lt;/i&gt;
    &lt;/a&gt;
    &lt;ul class=&quot;dropdown-menu p-0&quot; style=&quot;width: 200px&quot;&gt;
        &lt;div class=&quot;list-group&quot;&gt;
            @foreach (var ou in Model.OrganizationDtos)
            {
                &lt;button type=&quot;button&quot; onclick=&quot;setOrganizationUnitId('@ou.Id')&quot; class=&quot;list-group-item list-group-item-action @(ou.Id == Model.CurrentOrganizationId ? &quot;active&quot; : &quot;&quot;)&quot;&gt;@ou.DisplayName&lt;/button&gt;
            }
        &lt;/div&gt;
    &lt;/ul&gt;
&lt;/div&gt;

&lt;script&gt;
    function setOrganizationUnitId(id) {
        bookStore.currentOrganization.currentOrganization.change(id).then(function () {
			location.reload();
        });
    }
&lt;/script&gt;
</code></pre>
<p>Add the <code>OrganizationUnitComponent</code> to the toolbar.</p>
<pre><code class="language-csharp">public class BookStoreToolbarContributor : IToolbarContributor
{
    public virtual Task ConfigureToolbarAsync(IToolbarConfigurationContext context)
    {
		// ...
    
        if (context.Toolbar.Name == StandardToolbars.Main)
        {
            context.Toolbar.Items.Add(new ToolbarItem(typeof(OrganizationUnitComponent)).RequireAuthenticated());
        }

        return Task.CompletedTask;
    }
}
</code></pre>
<p>In addition, we also need to add a middleware after <code>UseAuthorization</code> to change the current <code>organization id</code>.</p>
<pre><code class="language-csharp">app.UseAuthorization();
app.Use(async (httpContext, next) =&gt;
{
	var currentUser = httpContext.RequestServices.GetRequiredService&lt;ICurrentUser&gt;();
	var cacheKey = currentUser.Id.ToString() + &quot;:&quot; + currentUser.GetBrowserInfo();
	var cache = httpContext.RequestServices.GetRequiredService&lt;IDistributedCache&lt;CurrentOrganizationIdCacheItem&gt;&gt;();
	var cacheItem = await cache.GetAsync(cacheKey);
	if (cacheItem != null)
	{
		var currentOrganizationIdProvider = httpContext.RequestServices.GetRequiredService&lt;CurrentOrganizationIdProvider&gt;();
		currentOrganizationIdProvider.Change(cacheItem.OrganizationId);
	}
	await next(httpContext);
});
// ...
</code></pre>
<p>The <code>Index</code> page will show the books based on the current <code>organization id</code>.</p>
<pre><code class="language-cshtml">public class IndexModel : BookStorePageModel
{
    public List&lt;BookDto&gt; Books { get; set; } = new List&lt;BookDto&gt;();
    public string? OrganizationName { get; set; }

    protected readonly IBookAppService BookAppService;
    protected readonly ICurrentOrganizationAppService CurrentOrganizationAppService;
    protected readonly IOrganizationUnitRepository OrganizationUnitRepository;

    public IndexModel(
        IBookAppService bookAppService,
        ICurrentOrganizationAppService currentOrganizationAppService,
        IOrganizationUnitRepository organizationUnitRepository)
    {
        BookAppService = bookAppService;
        CurrentOrganizationAppService = currentOrganizationAppService;
        OrganizationUnitRepository = organizationUnitRepository;
    }

    public async Task OnGetAsync()
    {
        if (CurrentUser.IsAuthenticated)
        {
            var currentOrganizationId = await CurrentOrganizationAppService.GetCurrentOrganizationIdAsync();
            if (currentOrganizationId.HasValue)
            {
                OrganizationName = (await OrganizationUnitRepository.GetAsync(currentOrganizationId.Value)).DisplayName;
            }

            Books = await BookAppService.GetListAsync();
        }
    }
}
</code></pre>
<pre><code class="language-html">@page
@model BookStore.Web.Pages.IndexModel
@using Microsoft.AspNetCore.Mvc.Localization
@using BookStore.Localization
@inject IHtmlLocalizer&lt;BookStoreResource&gt; L

@if (!Model.OrganizationName.IsNullOrEmpty())
{
    &lt;h5&gt;The books belonging to @Model.OrganizationName organization&lt;/h5&gt;
}

&lt;ul class=&quot;list-group&quot;&gt;
    @foreach(var book in Model.Books)
    {
    &lt;li class=&quot;list-group-item&quot;&gt;Book Name: @book.Name, ISBN: @book.Isbn&lt;/li&gt;
    }
&lt;/ul&gt;
</code></pre>
<h3>Final UI</h3>
<p>The final UI will look like this:</p>
<p>The index page will show empty if the current organization id is not set.
After selecting the organization unit, the index page will show the books based on the selected organization unit.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2024-08-01-switching-between-organization-units/1.png" alt="1" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2024-08-01-switching-between-organization-units/2.png" alt="2" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2024-08-01-switching-between-organization-units/3.png" alt="3" /></p>
<h2>Summary</h2>
<p>In this blog post. We showd simple implementation of switching between organization units. You can extend this implementation to meet your requirements.</p>
<p>After <a href="https://github.com/abpframework/abp/pull/20065">ABP 8.3</a> we introduced <a href="https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping">User-defined function mapping</a> feature for global filters which will gain performance improvements.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/data-filtering">Data Filtering</a></li>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/authorization#claims-principal-factory">Claims Principal Factory</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a14681f-c6af-f757-f4df-7fc88e4e5cbd" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a14681f-c6af-f757-f4df-7fc88e4e5cbd" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/use-userdefined-function-mapping-for-global-filter-pht26l07</guid>
      <link>https://abp.io/community/posts/use-userdefined-function-mapping-for-global-filter-pht26l07</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>entity-framework-core</category>
      <category>abp</category>
      <title>Use User-Defined Function Mapping for Global Filter</title>
      <description>ABP provides data filters that can filter queries automatically based on some rules. This feature is useful for implementing multi-tenancy, soft delete, and other global filters. It uses EF Core's Global Query Filters system for the EF Core Integration.

EF Core Global Query Filters generate filter conditions and apply them to SQL queries. ABP controls whether this filter condition takes effect through a variable. However, this variable may cause performance losses in some scenarios.
</description>
      <pubDate>Tue, 02 Jul 2024 03:33:05 Z</pubDate>
      <a10:updated>2026-09-26T01:09:42Z</a10:updated>
      <content:encoded><![CDATA[<h1>Use User-Defined Function Mapping for Global Filter</h1>
<h2>Introduction</h2>
<p>ABP provides data filters that can filter queries automatically based on some rules. This feature is useful for implementing multi-tenancy, soft delete, and other global filters. It uses <a href="https://learn.microsoft.com/en-us/ef/core/querying/filters">EF Core's Global Query Filters system</a> for the EF Core Integration.</p>
<p>EF Core Global Query Filters generate filter conditions and apply them to SQL queries. ABP controls whether this filter condition takes effect through a variable. However, this variable may cause performance losses in some scenarios.</p>
<h2>The Filter Condition Variable</h2>
<p>Think of a scenario with a global filter <code>IIsActive</code>, which filters out inactive entities:</p>
<pre><code class="language-csharp">public class Book : IIsActive
{
    public string Name { get; set; }

    public bool IsActive { get; set; } 
}
</code></pre>
<p>The SQL generated by the <a href="https://learn.microsoft.com/en-us/ef/core/querying/filters">EF Core Global Query Filters</a> is as follows:</p>
<pre><code class="language-SQL">SELECT * FROM [AppBooks] AS [a]
WHERE (@__ef_filter__p_0 = CAST(1 AS bit) OR [a].[IsActive] = CAST(1 AS bit))
</code></pre>
<blockquote>
<p>The <code>__ef_filter__p_0</code> variable controls whether the filter condition takes effect.</p>
</blockquote>
<p>The generated SQL is not optimal, and some databases do not optimize it well.</p>
<h2>Using User-defined function mapping for global filters</h2>
<p>In the <a href="https://github.com/abpframework/abp/pull/20065">upcoming preview version of ABP, v8.3.0-rc.1</a>, we start the <a href="https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping">User-defined function mapping</a> to implement global filters more efficiently. This feature is enabled by default, so you don't need to make any changes if you create a new solution and start from scratch. Otherwise, you can enable it easily by following the instructions below.</p>
<p>To use this new feature for your custom global filters, you need to change your <code>DbContext</code> as follows:</p>
<pre><code class="language-csharp">protected bool IsActiveFilterEnabled =&gt; DataFilter?.IsEnabled&lt;IIsActive&gt;() ?? false;

protected override bool ShouldFilterEntity&lt;TEntity&gt;(IMutableEntityType entityType)
{
    if (typeof(IIsActive).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)
{
    var expression = base.CreateFilterExpression&lt;TEntity&gt;(modelBuilder);

    if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
    {
        Expression&lt;Func&lt;TEntity, bool&gt;&gt; isActiveFilter = e =&gt; !IsActiveFilterEnabled || EF.Property&lt;bool&gt;(e, &quot;IsActive&quot;);

        if (UseDbFunction())
        {
            isActiveFilter = e =&gt; IsActiveFilter(((IIsActive)e).IsActive, true);

            var abpEfCoreCurrentDbContext = this.GetService&lt;AbpEfCoreCurrentDbContext&gt;();
            modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!)
                .HasTranslation(args =&gt;
                {
                    // (bool isActive, bool boolParam)
                    var isActive = args[0];
                    var boolParam = args[1];

                    if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled&lt;IIsActive&gt;() == true)
                    {
                        // isActive == true
                        return new SqlBinaryExpression(
                            ExpressionType.Equal,
                            isActive,
                            new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping),
                            boolParam.Type,
                            boolParam.TypeMapping);
                    }

                    // empty where sql
                    return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping);
                });
        }

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

    return expression;
}

public static bool IsActiveFilter(bool isActive, bool boolParam)
{
    throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage);
}

public override string GetCompiledQueryCacheKey()
{
    return $&quot;{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}&quot;;
}
</code></pre>
<p>After these changes, the SQL generated by the EF Core Global Query Filters will be as follows:</p>
<p>Enabling the <code>IIsActive</code> filter:</p>
<pre><code class="language-SQL">SELECT * FROM [AppBooks] AS [a] WHERE 
[a].[IsActive] = CAST(1 AS bit)
</code></pre>
<p>Disabling the <code>IIsActive</code> filter:</p>
<pre><code class="language-SQL">SELECT * FROM [AppBooks] AS [a]
</code></pre>
<h2>Conclusion</h2>
<p>We have implemented global filters using <a href="https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping">User-defined function mapping</a>, which can generate more efficient SQL and thus improve performance.</p>
<p>Upgrade to the latest ABP version and enjoy the performance improvement!</p>
<h2>References</h2>
<ul>
<li><a href="https://docs.abp.io/en/abp/latest/Data-Filtering">ABP Framework Data Filtering</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/querying/filters">EF Core's Global Query Filters system</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/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-claim-type-works-in-asp-net-core-and-abp-framework-km5dw6g1</guid>
      <link>https://abp.io/community/posts/how-claim-type-works-in-asp-net-core-and-abp-framework-km5dw6g1</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>security</category>
      <category>claims</category>
      <title>How claim type works in ASP NET Core and ABP Framework</title>
      <description>This article can help you understand the claim type in the ABP Framework and ASP NET Core.</description>
      <pubDate>Wed, 08 May 2024 06:48:21 Z</pubDate>
      <a10:updated>2026-09-25T21:17:30Z</a10:updated>
      <content:encoded><![CDATA[<h1>How claim type works in ASP NET Core and ABP Framework</h1>
<h2>The Claim Type</h2>
<p>A web application may use one or more authentication schemes to obtain the current user's information, such as <code>Cookies</code>, <code>JwtBearer</code>, <code>OpenID Connect</code>, <code>Google</code> etc.</p>
<p>After authentication, we get a set of claims that can be issued using a trusted identity provider. A claim is a type/name-value pair representing the subject. The type property provides the semantic content of the claim, that is, it states what the claim is about.</p>
<p>The <a href="https://docs.abp.io/en/abp/latest/CurrentUser"><code>ICurrentUser</code></a> service of the ABP Framework provides a convenient way to access the current user's information from the claims.</p>
<p>The claim type is the key to getting the correct value of the current user, and we have a static <code>AbpClaimTypes</code> class that defines the names of the standard claims in the ABP Framework:</p>
<pre><code class="language-cs">public static class AbpClaimTypes
{
    public static string UserId { get; set; } = ClaimTypes.NameIdentifier;
    public static string UserName { get; set; } = ClaimTypes.Name;
    public static string Role { get; set; } = ClaimTypes.Role;
    public static string Email { get; set; } = ClaimTypes.Email;
    //...
}
</code></pre>
<p>As you can see, the default claim type of <code>AbpClaimTypes</code> comes from the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes"><code>System.Security.Claims.ClaimTypes</code></a> class, which is the recommended practice in NET.</p>
<h2>Claim type in different authentication schemes</h2>
<p>We usually see two types of claim types in our daily development. One of them is the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes"><code>System.Security.Claims.ClaimTypes</code></a> and the other one is the <code>OpenId Connect</code> <a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">standard claims</a>.</p>
<h3>ASP NET Core Identity</h3>
<p>There is a <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.claimsidentityoptions"><code>ClaimsIdentityOptions</code></a> property in the <code>IdentityOptions</code>, which can be used to configure the claim type:</p>
<p>| Property             | Description                                                                                                   |
|----------------------|---------------------------------------------------------------------------------------------------------------|
| EmailClaimType       | Gets or sets the ClaimType used for the user email claim. Defaults to Email.                                  |
| RoleClaimType        | Gets or sets the ClaimType used for a Role claim. Defaults to Role.                                           |
| SecurityStampClaimType | Gets or sets the ClaimType used for the security stamp claim. Defaults to &quot;AspNet.Identity.SecurityStamp&quot;.  |
| UserIdClaimType      | Gets or sets the ClaimType used for the user identifier claim. Defaults to NameIdentifier.                    |
| UserNameClaimType    | Gets or sets the ClaimType used for the user name claim. Defaults to Name.                                    |</p>
<ul>
<li>The Identity creates a <code>ClaimsIdentity</code> object with the claim type that you have configured in the <code>ClaimsIdentityOptions</code> class.</li>
<li>The ABP Framework configures it based on <code>AbpClaimTypes,</code> so usually you don't need to worry about it.</li>
</ul>
<h3>JwtBearer/OpenID Connect Client</h3>
<p>The <code>JwtBearer/OpenID Connect</code> gets claims from <code>id_token</code> or fetches user information from the <code>AuthServer</code>, and then maps/adds it to the current <code>ClaimsIdentity</code>.</p>
<p>To map the <a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">standard claim</a> type to the <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes"><code>System.Security.Claims.ClaimTypes</code></a> via <a href="https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet">azure-activedirectory-identitymodel-extensions-for-dotnet</a> library by default, which is maintained by the Microsoft team:</p>
<pre><code class="language-cs">Dictionary&lt;string, string&gt; ClaimTypeMapping = new Dictionary&lt;string, string&gt;
{
    { &quot;actort&quot;, ClaimTypes.Actor },
    { &quot;birthdate&quot;, ClaimTypes.DateOfBirth },
    { &quot;email&quot;, ClaimTypes.Email },
    { &quot;family_name&quot;, ClaimTypes.Surname },
    { &quot;gender&quot;, ClaimTypes.Gender },
    { &quot;given_name&quot;, ClaimTypes.GivenName },
    { &quot;nameid&quot;, ClaimTypes.NameIdentifier },
    { &quot;sub&quot;, ClaimTypes.NameIdentifier },
    { &quot;website&quot;, ClaimTypes.Webpage },
    { &quot;unique_name&quot;, ClaimTypes.Name },
    { &quot;oid&quot;, &quot;http://schemas.microsoft.com/identity/claims/objectidentifier&quot; },
    { &quot;scp&quot;, &quot;http://schemas.microsoft.com/identity/claims/scope&quot; },
    { &quot;tid&quot;, &quot;http://schemas.microsoft.com/identity/claims/tenantid&quot; },
    { &quot;acr&quot;, &quot;http://schemas.microsoft.com/claims/authnclassreference&quot; },
    { &quot;adfs1email&quot;, &quot;http://schemas.xmlsoap.org/claims/EmailAddress&quot; },
    { &quot;adfs1upn&quot;, &quot;http://schemas.xmlsoap.org/claims/UPN&quot; },
    { &quot;amr&quot;, &quot;http://schemas.microsoft.com/claims/authnmethodsreferences&quot; },
    { &quot;authmethod&quot;, ClaimTypes.AuthenticationMethod },
    { &quot;certapppolicy&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/applicationpolicy&quot; },
    { &quot;certauthoritykeyidentifier&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/authoritykeyidentifier&quot; },
    { &quot;certbasicconstraints&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/basicconstraints&quot; },
    { &quot;certeku&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/eku&quot; },
    { &quot;certissuer&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/issuer&quot; },
    { &quot;certissuername&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/issuername&quot; },
    { &quot;certkeyusage&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/keyusage&quot; },
    { &quot;certnotafter&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/notafter&quot; },
    { &quot;certnotbefore&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/notbefore&quot; },
    { &quot;certpolicy&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatepolicy&quot; },
    { &quot;certpublickey&quot;, ClaimTypes.Rsa },
    { &quot;certrawdata&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/rawdata&quot; },
    { &quot;certserialnumber&quot;, ClaimTypes.SerialNumber },
    { &quot;certsignaturealgorithm&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/signaturealgorithm&quot; },
    { &quot;certsubject&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/subject&quot; },
    { &quot;certsubjectaltname&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/san&quot; },
    { &quot;certsubjectkeyidentifier&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/subjectkeyidentifier&quot; },
    { &quot;certsubjectname&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/subjectname&quot; },
    { &quot;certtemplateinformation&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatetemplateinformation&quot; },
    { &quot;certtemplatename&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/extension/certificatetemplatename&quot; },
    { &quot;certthumbprint&quot;, ClaimTypes.Thumbprint },
    { &quot;certx509version&quot;, &quot;http://schemas.microsoft.com/2012/12/certificatecontext/field/x509version&quot; },
    { &quot;clientapplication&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-application&quot; },
    { &quot;clientip&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-ip&quot; },
    { &quot;clientuseragent&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-client-user-agent&quot; },
    { &quot;commonname&quot;, &quot;http://schemas.xmlsoap.org/claims/CommonName&quot; },
    { &quot;denyonlyprimarygroupsid&quot;, ClaimTypes.DenyOnlyPrimaryGroupSid },
    { &quot;denyonlyprimarysid&quot;, ClaimTypes.DenyOnlyPrimarySid },
    { &quot;denyonlysid&quot;, ClaimTypes.DenyOnlySid },
    { &quot;devicedispname&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/displayname&quot; },
    { &quot;deviceid&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/identifier&quot; },
    { &quot;deviceismanaged&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/ismanaged&quot; },
    { &quot;deviceostype&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/ostype&quot; },
    { &quot;deviceosver&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/osversion&quot; },
    { &quot;deviceowner&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/userowner&quot; },
    { &quot;deviceregid&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/registrationid&quot; },
    { &quot;endpointpath&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-endpoint-absolute-path&quot; },
    { &quot;forwardedclientip&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-forwarded-client-ip&quot; },
    { &quot;group&quot;, &quot;http://schemas.xmlsoap.org/claims/Group&quot; },
    { &quot;groupsid&quot;, ClaimTypes.GroupSid },
    { &quot;idp&quot;, &quot;http://schemas.microsoft.com/identity/claims/identityprovider&quot; },
    { &quot;insidecorporatenetwork&quot;, &quot;http://schemas.microsoft.com/ws/2012/01/insidecorporatenetwork&quot; },
    { &quot;isregistereduser&quot;, &quot;http://schemas.microsoft.com/2012/01/devicecontext/claims/isregistereduser&quot; },
    { &quot;ppid&quot;, &quot;http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier&quot; },
    { &quot;primarygroupsid&quot;, ClaimTypes.PrimaryGroupSid },
    { &quot;primarysid&quot;, ClaimTypes.PrimarySid },
    { &quot;proxy&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/x-ms-proxy&quot; },
    { &quot;pwdchgurl&quot;, &quot;http://schemas.microsoft.com/ws/2012/01/passwordchangeurl&quot; },
    { &quot;pwdexpdays&quot;, &quot;http://schemas.microsoft.com/ws/2012/01/passwordexpirationdays&quot; },
    { &quot;pwdexptime&quot;, &quot;http://schemas.microsoft.com/ws/2012/01/passwordexpirationtime&quot; },
    { &quot;relyingpartytrustid&quot;, &quot;http://schemas.microsoft.com/2012/01/requestcontext/claims/relyingpartytrustid&quot; },
    { &quot;role&quot;, ClaimTypes.Role },
    { &quot;roles&quot;, ClaimTypes.Role },
    { &quot;upn&quot;, ClaimTypes.Upn },
    { &quot;winaccountname&quot;, ClaimTypes.WindowsAccountName },
};
</code></pre>
<h4>Disable JwtBearer/OpenID Connect Client Claim Type Mapping</h4>
<p>To turn off the claim type mapping, you can set the <code>MapInboundClaims</code> property of <code>JwtBearerOptions</code> or <code>OpenIdConnectOptions</code> to <code>false</code>. Then, you can get the original claim types from the token(<code>access_token</code> or <code>id_token</code>):</p>
<p>JWT Example:</p>
<pre><code class="language-json">{
  &quot;iss&quot;: &quot;https://localhost:44305/&quot;,
  &quot;exp&quot;: 1714466127,
  &quot;iat&quot;: 1714466127,
  &quot;aud&quot;: &quot;MyProjectName&quot;,
  &quot;scope&quot;: &quot;MyProjectName offline_access&quot;,
  &quot;sub&quot;: &quot;ed7f5cfd-7311-0402-245c-3a123ff787f9&quot;,
  &quot;unique_name&quot;: &quot;admin&quot;,
  &quot;preferred_username&quot;: &quot;admin&quot;,
  &quot;given_name&quot;: &quot;admin&quot;,
  &quot;role&quot;: &quot;admin&quot;,
  &quot;email&quot;: &quot;admin@abp.io&quot;,
  &quot;email_verified&quot;: &quot;False&quot;,
  &quot;phone_number_verified&quot;: &quot;False&quot;,
}
</code></pre>
<h3>OAuth2(Google, Facebook, Twitter, Microsoft) Extenal Login Client</h3>
<p>The <code>OAuth2 handler</code> fetchs a JSON containing user information from the <code>OAuth2</code> server. The third-party provider issues the claim type based on their standard server and then maps/adds it to the current <code>ClaimsIdentity</code>. The ASP NET Core provides some built-in claim-type mappings for different providers as can be seen below examples:</p>
<p><strong>Example</strong>: The <code>ClaimActions</code> property of the <code>GoogleOptions</code> maps the Google's claim types to <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes"><code>System.Security.Claims.ClaimTypes</code></a>:</p>
<pre><code class="language-cs">ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, &quot;id&quot;); // v2
ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, &quot;sub&quot;); // v3
ClaimActions.MapJsonKey(ClaimTypes.Name, &quot;name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.GivenName, &quot;given_name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Surname, &quot;family_name&quot;);
ClaimActions.MapJsonKey(&quot;urn:google:profile&quot;, &quot;link&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Email, &quot;email&quot;);
</code></pre>
<p><strong>Example</strong>: The <code>ClaimActions</code> property of the <code>FacebookOptions</code> maps the Facebook's claim types to <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes"><code>System.Security.Claims.ClaimTypes</code></a>:</p>
<pre><code class="language-cs">ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, &quot;id&quot;);
ClaimActions.MapJsonSubKey(&quot;urn:facebook:age_range_min&quot;, &quot;age_range&quot;, &quot;min&quot;);
ClaimActions.MapJsonSubKey(&quot;urn:facebook:age_range_max&quot;, &quot;age_range&quot;, &quot;max&quot;);
ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, &quot;birthday&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Email, &quot;email&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Name, &quot;name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.GivenName, &quot;first_name&quot;);
ClaimActions.MapJsonKey(&quot;urn:facebook:middle_name&quot;, &quot;middle_name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Surname, &quot;last_name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Gender, &quot;gender&quot;);
ClaimActions.MapJsonKey(&quot;urn:facebook:link&quot;, &quot;link&quot;);
ClaimActions.MapJsonSubKey(&quot;urn:facebook:location&quot;, &quot;location&quot;, &quot;name&quot;);
ClaimActions.MapJsonKey(ClaimTypes.Locality, &quot;locale&quot;);
ClaimActions.MapJsonKey(&quot;urn:facebook:timezone&quot;, &quot;timezone&quot;);
</code></pre>
<h3>OpenIddict AuthServer</h3>
<p>The <code>OpenIddict</code> uses the <a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">standard claims</a> as the claim type of the <code>id_token</code> or <code>access_token</code> and <code>UserInfo</code> endpoint response, etc.</p>
<ul>
<li>For JWT token, it also uses the <a href="https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet">azure-activedirectory-identitymodel-extensions-for-dotnet</a> to get the claims from the <code>id_token</code> or <code>access_token</code>.</li>
<li>For reference token, it gets the claims from the <code>database</code>.</li>
</ul>
<h2>Summary</h2>
<p>Once you find the claims you received do not meet your expectations, follow the instructions above to troubleshoot the problem.</p>
<p>This article can help you understand the claim type in the ABP Framework and ASP NET Core.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a126922-a87b-6933-0452-72c453801834" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a126922-a87b-6933-0452-72c453801834" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-share-the-cookies-between-subdomains-jfrzggc2</guid>
      <link>https://abp.io/community/posts/how-to-share-the-cookies-between-subdomains-jfrzggc2</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>authentication</category>
      <category>security</category>
      <title>How to share the cookies between subdomains</title>
      <description>Sharing cookies between subdomains is a common requirement in web development. For example, you have a website with multiple subdomains, and you want to share the login status between these subdomains. Once a user logs in to one subdomain, the user should be logged in to all subdomains.</description>
      <pubDate>Mon, 11 Mar 2024 08:13:13 Z</pubDate>
      <a10:updated>2026-09-26T01:32:13Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to share the cookies between subdomains</h1>
<h2>Introduction</h2>
<p>Sharing cookies between subdomains is a common requirement in web development. For example, you have a website with multiple subdomains, and you want to share the login status between these subdomains. Once a user logs in to one subdomain, the user should be logged in to all subdomains.</p>
<p>This article will show you how to achieve this in an ASP.NET Core application.</p>
<h2>Implementation principle</h2>
<p>The <code>cookie</code> has a <code>Domain</code> attribute which specifies which server can receive a cookie.
If specified, then cookies are available on the server and its subdomains. For example, if you set <code>Domain=.abp.io</code>, cookies are available on <code>abp.io</code> and its subdomains like <code>community.abp.io</code>.</p>
<p>If the server does not specify a <strong>Domain</strong>, the cookies are available on the server but not on its subdomains. Therefore, specifying the <strong>Domain</strong> is less restrictive than omitting it. However, it can be helpful when subdomains need to share information about a user.</p>
<h2>Change the domain of the cookie in ASP.NET Core</h2>
<p>There is a <code>CookiePolicyMiddleware</code> in ASP.NET Core, you can add some policies to the <code>CookiePolicyOptions</code> during cookies are appended or deleted.</p>
<p>We will add a policy to the <code>CookiePolicyOptions</code> to change the <code>domain</code> of the cookie:</p>
<pre><code class="language-csharp">services.Configure&lt;CookiePolicyOptions&gt;(options =&gt;
{
    options.OnAppendCookie = cookieContext =&gt;
    {
        ChangeCookieDomain(cookieContext, null);
    };

    options.OnDeleteCookie = cookieContext =&gt;
    {
        ChangeCookieDomain(null, cookieContext);
    };
});

private static void ChangeCookieDomain(AppendCookieContext appendCookieContext, DeleteCookieContext deleteCookieContext)
{
    if (appendCookieContext != null)
    {
        // Change the domain of all cookies
        //appendCookieContext.CookieOptions.Domain = &quot;.abp.io&quot;;

        // Change the domain of the specific cookie
        if (appendCookieContext.CookieName == &quot;.AspNetCore.Culture&quot;)
        {
            appendCookieContext.CookieOptions.Domain = &quot;.abp.io&quot;;
        }
    }

    if (deleteCookieContext != null)
    { 
        // Change the domain of all cookies
        //appendCookieContext.CookieOptions.Domain = &quot;.abp.io&quot;;

        // Change the domain of the specific cookie
        if (deleteCookieContext.CookieName == &quot;.AspNetCore.Culture&quot;)
        {
            deleteCookieContext.CookieOptions.Domain = &quot;.abp.io&quot;;
        }
    }
}
</code></pre>
<p>Add the <code>app.UseCookiePolicy()</code> in the ASP.NET Core pipeline:</p>
<pre><code class="language-csharp">//...
app.UseStaticFiles();
app.UseCookiePolicy();
//...
</code></pre>
<p>If you check the HTTP response headers, you will see the <code>Set-Cookie</code> header with the <code>domain</code> attribute as follows:</p>
<pre><code class="language-http">Set-Cookie: .AspNetCore.Culture=c%3Den%7Cuic%3Den; expires=Mon, 09 Mar 2026 02:00:00 GMT; domain=.abp.io; path=/
</code></pre>
<p>The subdomains can share the <code>.AspNetCore.Culture</code> cookie now.</p>
<p>In another community article, we use the same middleware to <a href="https://community.abp.io/posts/patch-for-chrome-login-issue-identityserver4-samesite-cookie-problem-weypwp3n">fix the Chrome login issue for the IdentityServer4</a></p>
<h2>Summary</h2>
<p>The <code>CookiePolicy</code> middleware provides a way to control cookies in an ASP.NET Core,  It is very useful if you have more complex requirements for Cookies.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a113ebf-828f-0a07-8664-e870a90b7798" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a113ebf-828f-0a07-8664-e870a90b7798" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-testcontainers-in-abp-unit-test-b67gzpxg</guid>
      <link>https://abp.io/community/posts/using-testcontainers-in-abp-unit-test-b67gzpxg</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>testing</category>
      <category>unit-tests</category>
      <title>Using Testcontainers in ABP Unit Test</title>
      <description>Testcontainers is a library that provides easy and lightweight APIs for bootstrapping local development and test dependencies with real services wrapped in Docker containers.</description>
      <pubDate>Mon, 04 Mar 2024 08:32:46 Z</pubDate>
      <a10:updated>2026-09-26T01:04:01Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using Testcontainers in ABP Unit Test</h1>
<h2>What is Testcontainers?</h2>
<p>Testcontainers is a library that provides easy and lightweight APIs for bootstrapping local development and test dependencies with real services wrapped in Docker containers.</p>
<p>Using Testcontainers, you can write tests that depend on the same services you use in production without mocks or in-memory services.</p>
<p>Get more information about Testcontainers from <a href="https://testcontainers.com/">https://testcontainers.com/</a>.</p>
<h2>How to Use Testcontainers in ABP Unit Test?</h2>
<p>ABP Framework provides a built-in unit test infrastructure, allowing you to add your unit and integration tests easily.</p>
<p>It uses <a href="https://learn.microsoft.com/en-us/ef/core/testing/testing-without-the-database#sqlite-in-memory">SQLite in-memory</a> and <a href="https://github.com/asimmon/ephemeral-mongo">EphemeralMongo</a>  as the default database for unit tests and it's enough for most of the cases. However, you may need to test your code with a real database like PostgreSQL, MySQL, SQL Server, etc.</p>
<p>In this article, I will show you how to use Testcontainers in ABP unit tests to test your code with a real database from a Docker container.</p>
<blockquote>
<p>The Testcontainers will pull the Docker images of the databases you want to use. You can pull them manually before running the tests to speed them up.</p>
</blockquote>
<pre><code class="language-bash">docker pull mcr.microsoft.com/mssql/server:2019-CU18-ubuntu-20.04
docker pull mongo:6.0
</code></pre>
<h3>Code Changes For Entity Framework Core Tests</h3>
<ol>
<li>Remove <code>Volo.Abp.EntityFrameworkCore.Sqlite</code> package and add the <code>Testcontainers.MsSql</code> package to the <code>MyProjectName.EntityFrameworkCore.Tests</code> project.</li>
</ol>
<pre><code class="language-csharp">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  &lt;Import Project=&quot;..\..\common.props&quot; /&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;RootNamespace&gt;MyCompanyName.MyProjectName&lt;/RootNamespace&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.EntityFrameworkCore\MyCompanyName.MyProjectName.EntityFrameworkCore.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.NET.Test.Sdk&quot; Version=&quot;17.8.0&quot; /&gt;
    &lt;PackageReference Include=&quot;Testcontainers.MsSql&quot; Version=&quot;3.7.0&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>
<ol start="2">
<li>Update <code>MyProjectNameEntityFrameworkCoreFixture</code> class as shown below:</li>
</ol>
<p>We start an SQL Server container in the <code>InitializeAsync</code> method and dispose of it in the <code>DisposeAsync</code> method. The <code>GetRandomConnectionString</code> method sets a random database for each test.</p>
<pre><code class="language-csharp">using System;
using System.Threading.Tasks;
using Testcontainers.MsSql;
using Xunit;

namespace MyCompanyName.MyProjectName.EntityFrameworkCore;

public class MyProjectNameEntityFrameworkCoreFixture : IAsyncLifetime
{
    private readonly static MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build();

    public async Task InitializeAsync()
    {
        await _msSqlContainer.StartAsync();
    }

    public static string GetRandomConnectionString()
    {
        var randomDbName = &quot;Database=Db_&quot; + Guid.NewGuid().ToString(&quot;N&quot;);
        return _msSqlContainer.GetConnectionString().Replace(&quot;Database=master&quot;, randomDbName, StringComparison.OrdinalIgnoreCase);
    }

    public async Task DisposeAsync()
    {
        await _msSqlContainer.DisposeAsync().AsTask();
    }
}
</code></pre>
<ol start="3">
<li>Update <code>MyProjectNameEntityFrameworkCoreTestModule</code> class as shown below:</li>
</ol>
<pre><code class="language-csharp">using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
using Volo.Abp.SettingManagement;
using Volo.Abp.Uow;

namespace MyCompanyName.MyProjectName.EntityFrameworkCore;

[DependsOn(
    typeof(MyProjectNameApplicationTestModule),
    typeof(MyProjectNameEntityFrameworkCoreModule)
    )]
public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        ConfigureMsSqlDatabase(context.Services);
    }

    private void ConfigureMsSqlDatabase(IServiceCollection services)
    {
        var connectionString = MyProjectNameEntityFrameworkCoreFixture.GetRandomConnectionString();
        using (var context = new MyProjectNameDbContext(new DbContextOptionsBuilder&lt;MyProjectNameDbContext&gt;()
                   .UseSqlServer(connectionString)
                   .Options))
        {
            context.Database.Migrate();
        }
        services.Configure&lt;AbpDbContextOptions&gt;(options =&gt;
        {
            options.Configure(context =&gt;
            {
                context.DbContextOptions.UseSqlServer(connectionString);
            });
        });
    }
}
</code></pre>
<p>The EF Core unit tests results will be like the following:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-03-02-Using-Testcontainers-In-ABP-Unit-Test/efcore.png" alt="ef core" /></p>
<h3>Code Changes For MongoDB Tests</h3>
<ol>
<li>Remove <code>EphemeralMongo</code> related packages and add the <code>Testcontainers.MongoDb</code> package to the <code>MyProjectName.EntityFrameworkCore.Tests</code> project.</li>
</ol>
<pre><code class="language-csharp">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

  &lt;Import Project=&quot;..\..\common.props&quot; /&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;RootNamespace&gt;MyCompanyName.MyProjectName&lt;/RootNamespace&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\..\src\MyCompanyName.MyProjectName.MongoDB\MyCompanyName.MyProjectName.MongoDB.csproj&quot; /&gt;
    &lt;ProjectReference Include=&quot;..\MyCompanyName.MyProjectName.Application.Tests\MyCompanyName.MyProjectName.Application.Tests.csproj&quot; /&gt;
  &lt;/ItemGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.NET.Test.Sdk&quot; Version=&quot;17.8.0&quot; /&gt;
    &lt;PackageReference Include=&quot;Testcontainers.MongoDb&quot; Version=&quot;3.7.0&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>
<ol start="2">
<li>Update <code>MyProjectNameMongoDbFixture</code> class as shown below:</li>
</ol>
<p>We start a MongoDB container in the <code>InitializeAsync</code> method and dispose of it in the <code>DisposeAsync</code> method. The <code>GetRandomConnectionString</code> method sets a random database for each test.</p>
<pre><code class="language-csharp">using System;
using System.Threading.Tasks;
using Testcontainers.MongoDb;
using Xunit;

namespace MyCompanyName.MyProjectName.MongoDB;

public class MyProjectNameMongoDbFixture : IAsyncLifetime
{
    private readonly static MongoDbContainer _mongoDbContainer = new MongoDbBuilder().WithCommand().Build();

    public async Task InitializeAsync()
    {
        await _mongoDbContainer.StartAsync();
    }

    public static string GetRandomConnectionString()
    {
        var randomDbName = &quot;Db_&quot; + Guid.NewGuid().ToString(&quot;N&quot;);
        return _mongoDbContainer.GetConnectionString().EnsureEndsWith('/') + randomDbName + &quot;?authSource=admin&quot;;
    }

    public async Task DisposeAsync()
    {
        await _mongoDbContainer.DisposeAsync().AsTask();
    }
}
</code></pre>
<p>The MongoDB unit tests results will be like the following:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-03-02-Using-Testcontainers-In-ABP-Unit-Test/mongodb.png" alt="mongodb" /></p>
<h2>Summary</h2>
<p>The Testcontainers works well with ABP Framework and it's easy to use. If you need to test your code with a real database, Testcontainers is a good choice for you.</p>
<p>While it still needs to be faster than in-memory databases, but its advantages are obvious.</p>
<p>Enjoy testing with Testcontainers!</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a111ac4-e4f9-e1da-0eda-4e767333078b" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a111ac4-e4f9-e1da-0eda-4e767333078b" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/2024-first-community-event-3kfx560g</guid>
      <link>https://abp.io/community/posts/2024-first-community-event-3kfx560g</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>2024 First Community Event</title>
      <description> ## 2024 First Community Event.The first .NET community event in 2024 was successfully held in Shenzhen on January 14, 2024.This event is co-organized by **Microsoft MVP China Team**, **Micros</description>
      <pubDate>Mon, 15 Jan 2024 08:12:28 Z</pubDate>
      <a10:updated>2026-09-25T19:16:48Z</a10:updated>
      <content:encoded><![CDATA[<h2>2024 First Community Event.</h2>
<p>The first .NET community event in 2024 was successfully held in Shenzhen on January 14, 2024.</p>
<p>This event is co-organized by <strong>Microsoft MVP China Team</strong>, <strong>Microsoft Reactor</strong>, <strong>China .NET Community</strong> and <strong>Shenzhen .NET Club</strong>.</p>
<p><strong>ABP.IO</strong> continues to strongly support the community, and we have prepared exquisite gifts for participants.</p>
<p>The event includes four wonderful technical lectures to reveal big data and AI's potential opportunities and innovations. It is a transfer of knowledge and a platform for communication and cooperation among technology enthusiasts.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e59366a9beb61202ddf67a48af0.jpg" alt="1.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e5964f409df4857352a7750144f.jpg" alt="2.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e598d63941f1b4eb0bbb9c1a7bf.jpg" alt="3.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e59b2bc78fcdb2805b6b2c2e0d8.jpg" alt="4.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e59fc9f52bfdd52a5b2a6fd629b.jpg" alt="51.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e59d73b779a17a42733619a5082.jpg" alt="5.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e5a3c4d33948a14f739566c3e91.jpg" alt="6.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e5a65ed0e44544fba86d71ba538.jpg" alt="8.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-01-15-2024-first-community-event/3a101e5a8d62fe81356f87616a4c00bc.jpg" alt="9.jpg" /></p>
<p><strong>See you at the next community event!</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a101e5a-b48e-c9ea-c8c4-ccb0b877d885" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a101e5a-b48e-c9ea-c8c4-ccb0b877d885" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-at-china-net-conf-2023-po90czre</guid>
      <link>https://abp.io/community/posts/abp-at-china-net-conf-2023-po90czre</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <title>ABP at China NET Conf 2023</title>
      <description>NET Conf China 2023, China's most influential NET event, officially kicked off on December 16, 2023.</description>
      <pubDate>Mon, 18 Dec 2023 07:42:03 Z</pubDate>
      <a10:updated>2026-09-25T18:13:40Z</a10:updated>
      <content:encoded><![CDATA[<h3>.NET Conf China 2023</h3>
<p>China's most influential .NET event officially kicked off on December 16, 2023. Although a heavy snowfall a few days ago impacted traffic, It did not stop the enthusiastic developers.</p>
<p>The conference has invited 30+ technical experts from various fields to share the new features of .NET 8, full-stack Blazor, AI and .NET MAUI and other trend-setting technology highlights, focusing on the theme of Intelligent · Open Source · Security. In-depth discussion of artificial Intelligence, web development, front-end &amp; security and other hot technical topics, and more .NET technical experts shared their valuable practical experience over the past year.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e08dc3131b22f8e8cdb714f2ef4.jpeg" alt="1.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e08f5f98360ad38c8dc2b96c5a9.jpg" alt="2.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e090fd29b0f78520367930777fa.jpg" alt="3.jpg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0923b0f7d003a0ff46e6ad9da4.jpg" alt="4.jpg" /></p>
<h3>As one of the community partners of .NET Conf China 2023</h3>
<p>Our ABP.IO China team arrived at the venue much earlier than it started and was carefully prepared to welcome the developers.</p>
<p>At the event, we showed developers the latest news and related updates on ABP.IO. We also held face-to-face interactive conversations with multiple developers, including senior ABP developers, framework fans and ABP Commercial customers. We listened to everyone's feedback and discussed how to make ABP.IO better serve developers.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e097cb0a3425b5268f32c730ff5.jpeg" alt="10.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e098c741838d700e4fd5982d9af.jpeg" alt="11.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e09ab67925b359e1ef210f37742.jpeg" alt="12.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e09b9a1b5380afcafec58f75df2.jpeg" alt="13.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e09def79c37f4280478fe3b870a.jpeg" alt="14.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e09f09cf1d44f136068714a71fa.jpeg" alt="15.jpeg" /></p>
<h3>As always, we have a raffle.</h3>
<p>Including ABP Commercial's TEAM and PERSONAL licenses, <a href="https://halilibrahimkalkan.com/">Halil İbrahim Kalkan</a>'s latest book <strong>Mastering ABP Framework</strong>, the ABP community's popular <strong>Implementing Domain Driven Design</strong> book, Bluetooth headset and other ABP peripheral brochures, stickers...</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0a19860cefa46476eb171b5e08.jpeg" alt="19.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0a2e23f7395ab84f7ca206b4ee.jpeg" alt="16.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0a3c8271fa658eb4fd1e3d2fd0.jpeg" alt="17.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0a49bb67213ac3bfc49a23e355.jpeg" alt="18.jpeg" /></p>
<h3>Through this event,</h3>
<p>We gained a lot and felt the enthusiasm and support of the developers community for ABP.IO. We will continue to work hard to provide better services for developers and contribute to the development of ABP.IO.</p>
<h3>See you at the next community event!</h3>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0ad416925db11783b31331826c.jpeg" alt="20.jpeg" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-12-18-abp-at-china-net-conf-2023/3a0f8e0ac3b558e62dd95cd5c9e9ec80.jpeg" alt="21.jpeg" /></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a0f8e0c-cad7-30d0-32c4-d416264142bd" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a0f8e0c-cad7-30d0-32c4-d416264142bd" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/changes-with-containers-in-net-8.0.-brjzgim6</guid>
      <link>https://abp.io/community/posts/changes-with-containers-in-net-8.0.-brjzgim6</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>docker</category>
      <category>kubernetes</category>
      <category>net8</category>
      <title>Changes with Containers in NET 8.0.</title>
      <description>This article will show the the changes of container images with NET 8.0.</description>
      <pubDate>Tue, 07 Nov 2023 06:26:10 Z</pubDate>
      <a10:updated>2026-09-26T01:54:53Z</a10:updated>
      <content:encoded><![CDATA[<h1>New Containers feature with NET 8.0</h1>
<p>This article will show you the new feature of containers with NET 8.0.</p>
<h2>Non-root user</h2>
<p>The <code>Non-root user</code> feature on net 8 is a security measure that allows users to have limited access to the system without having full administrative privileges. Hosting containers as <code>non-root</code> aligns with the principle of least privilege.
It’s free security provided by the operating system. If you run your app as root, your app process can do anything in the container, like modify files, install packages, or run arbitrary executables.
That’s a concern if your app is ever attacked. If you run your app as non-root, your app process cannot do much, greatly limiting what a bad actor could accomplish.</p>
<h2>Default ASP.NET Core port changed from 80 to 8080</h2>
<p>In .NET 8, there has been a change in the default port used by ASP.NET Core applications. Previously, the default port assigned to ASP.NET Core applications was <code>80</code>. However, starting from .NET 8, the default port has been changed to <code>8080</code>.
This change was made to avoid conflicts with other applications and services that commonly use port 80, such as web servers like IIS or Apache. By using port 8080 as the default, there is less potential for clashes and easier deployment of ASP.NET Core applications alongside other services.</p>
<p>It's important to note that this change only affects the default port used when an ASP.NET Core application is run without explicitly specifying a port.</p>
<p>If you want your application to continue using port 80, you can still specify it during the application launch or configure it in the application settings.</p>
<ul>
<li>Recommended: Explicitly set the <code>ASPNETCORE_HTTP_PORTS</code>, <code>ASPNETCORE_HTTPS_PORTS</code>, and <code>ASPNETCORE_URLS</code> environment variables to the desired port. Example: <code>docker run --rm -it -p 9999:80 -e ASPNETCORE_HTTP_PORTS=80 &lt;my-app&gt;</code></li>
<li>Update existing commands and configuration that rely on the expected default port of port 80 to reference port 8080 instead. Example: <code>docker run --rm -it -p 9999:8080 &lt;my-app&gt;</code></li>
</ul>
<blockquote>
<p>The <code>dockerfile</code> of ABP templates has been updated to use port <code>80</code>.</p>
</blockquote>
<h2>References</h2>
<ul>
<li><a href="https://devblogs.microsoft.com/dotnet/securing-containers-with-rootless/">Secure your .NET cloud apps with rootless Linux Containers</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#containers">Containers breaking changes</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/compatibility/8.0#containers">ASP.NET Core apps use port 8080 by default</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/docker/building-net-docker-images?view=aspnetcore-8.0">Docker images for ASP.NET Core</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a0ebaa2-973d-abb0-cb88-1ba63e315858" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a0ebaa2-973d-abb0-cb88-1ba63e315858" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/new-raw-sql-queries-for-unmapped-types-with-ef-core-8.0-ahc815sn</guid>
      <link>https://abp.io/community/posts/new-raw-sql-queries-for-unmapped-types-with-ef-core-8.0-ahc815sn</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>entity-framework-core</category>
      <category>net8</category>
      <category>efcore8</category>
      <title>New Raw SQL queries for unmapped types with EF Core 8.0</title>
      <description>I would love to talk about the new feature in EF Core 8.0, specifically the raw SQL queries for unmapped types. </description>
      <pubDate>Tue, 07 Nov 2023 06:26:07 Z</pubDate>
      <a10:updated>2026-09-26T01:20:08Z</a10:updated>
      <content:encoded><![CDATA[<h1>New Raw SQL queries for unmapped types feature with EF Core 8.0</h1>
<h2>Introduction</h2>
<p>I would love to talk about the new feature in EF Core 8.0, specifically the <code>raw SQL queries for unmapped types</code>.
This feature was recently introduced by Microsoft and is aimed at providing more flexibility and customization in database queries.</p>
<h2>What is the raw SQL queries for the unmapped types feature?</h2>
<p>To give you a better understanding, let's look at a sample repository method with the ABP framework.
Here is an example of a raw SQL query using the new feature:</p>
<pre><code class="language-csharp">public interface IAuthorRepository : IRepository&lt;Author, Guid&gt;
{
    Task&lt;List&lt;AuthorIdWithNames&gt;&gt; GetAllAuthorNamesAsync();
}

public class AuthorIdWithNames
{
    public Guid Id { get; set; }

    public string Name { get; set; }
}

public class EfCoreAuthorRepository : EfCoreRepository&lt;BookStoreDbContext, Author, Guid&gt;, IAuthorRepository
{
    public EfCoreAuthorRepository(IDbContextProvider&lt;BookStoreDbContext&gt; dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public virtual async Task&lt;List&lt;AuthorIdWithNames&gt;&gt; GetAllAuthorNamesAsync()
    {
        return await (await GetDbContextAsync()).Database.SqlQuery&lt;AuthorIdWithNames&gt;(@$&quot;SELECT Id, Name FROM Authors&quot;).ToListAsync();
    }
}
</code></pre>
<p>In this code, we can see that we are using the <code>SqlQuery</code> method to execute a raw SQL query on a custom type, <code>AuthorIdWithNames</code> in this case. This allows us to retrieve data that may not be mapped to any of our entity classes in the context.</p>
<h2>In summary</h2>
<p>This feature can be particularly useful in scenarios where we need to access data from tables or views that are not directly mapped to our entities. It also provides an alternative to using stored procedures for querying data.</p>
<p>However, it's important to note that using raw SQL queries can increase the risk of SQL injection attacks. So, it's recommended to use parameterized queries to prevent this. Additionally, this feature may not work with certain database providers, so it's important to check for compatibility before implementing it.</p>
<p>In conclusion, the raw SQL queries for unmapped types feature in EF Core 8.0 is a great addition for developers looking for more flexibility in database queries. It allows us to work with data that may not be directly mapped to our entities and can be a useful tool in certain scenarios. Just remember to use parameterized queries and check for compatibility before implementing it.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#raw-sql-queries-for-unmapped-types">Raw SQL queries for unmapped types</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/querying/sql-queries#querying-scalar-(non-entity)-types">SQL Queries</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a0ebaa2-8915-6b6b-d7ef-a9e68a8b4f01" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a0ebaa2-8915-6b6b-d7ef-a9e68a8b4f01" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/converting-createedit-modal-to-page-4ps5v60m</guid>
      <link>https://abp.io/community/posts/converting-createedit-modal-to-page-4ps5v60m</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>mvc</category>
      <category>abp</category>
      <category>razor page</category>
      <category>modal</category>
      <title>Converting Create/Edit Modal to Page</title>
      <description>In this document we will explain how to convert BookStore's Books create &amp; edit modals to regular razor pages.</description>
      <pubDate>Mon, 06 Feb 2023 02:57:02 Z</pubDate>
      <a10:updated>2026-09-26T00:47:32Z</a10:updated>
      <content:encoded><![CDATA[<h1>Converting Create/Edit Modal to Page</h1>
<p>In this document we will explain how to convert BookStore's <code>Books</code> create &amp; edit modals to regular razor pages.</p>
<h2>Before</h2>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/old.gif" alt="before" /></p>
<h2>Now</h2>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-02-06-Converting-Create-Edit-Modal-To-Page/images/new.gif" alt="after" /></p>
<h2>Index page</h2>
<p>Repalce <code>abp-button(NewBookButton)</code> buttom with <code>&lt;a class=&quot;btn btn-primary&quot; href=&quot;/Books/CreateModal&quot;&gt;&lt;i class=&quot;fa fa-plus&quot;&gt;&lt;/i&gt; @L[&quot;NewBook&quot;].Value&lt;/a&gt;</code>.</p>
<h2>Index js file</h2>
<p>Remove the related codes of <code>createModal</code> and <code>editModal</code>.</p>
<p>Change the <code>Edit row action</code> with <code>location.href = &quot;/Books/EditModal?id=&quot; + data.record.id;</code></p>
<h2>Create/Edit Book page</h2>
<p>Remove <code>Layout = null;</code> and add some custom style and javascript code to <code>CreateModal.cshtml</code> &amp; <code>EditModal.cshtml</code>.</p>
<pre><code class="language-csharp">@section styles {
    &lt;style&gt;
        .abp-view-modal .modal {
                position: static;
                display: block;
                opacity: inherit !important;
        }
        .abp-view-modal .modal.fade .modal-dialog {
            transition: inherit !important;
            transform: inherit !important;;
        }
        .abp-view-modal .modal-header .btn-close {
            display: none;
        }
    &lt;/style&gt;
}
@section scripts {
    &lt;script&gt;
        $(&quot;.abp-view-modal form&quot;).abpAjaxForm().on('abp-ajax-success', function () {
            location.href = &quot;/Books&quot;;
        });
    &lt;/script&gt;
}
</code></pre>
<p>Add a <code>div</code> element with <code>abp-view-modal</code> class to wrap the <code>abp-dynamic-form</code>, Set size of <code>abp-modal</code> to <code>ExtraLarge</code> and remove the <code>AbpModalButtons.Cancel</code> button from <code>abp-modal-footer</code>.</p>
<h3>CreateModal</h3>
<pre><code class="language-csharp">&lt;div class=&quot;abp-view-modal&quot;&gt;
    &lt;abp-dynamic-form abp-model=&quot;Book&quot; asp-page=&quot;/Books/CreateModal&quot;&gt;
        &lt;abp-modal static=&quot;true&quot; size=&quot;ExtraLarge&quot;&gt;
            &lt;abp-modal-header title=&quot;@L[&quot;NewBook&quot;].Value&quot;&gt;&lt;/abp-modal-header&gt;
            &lt;abp-modal-body&gt;
                &lt;abp-form-content /&gt;
            &lt;/abp-modal-body&gt;
            &lt;abp-modal-footer buttons=&quot;@(AbpModalButtons.Save)&quot;&gt;&lt;/abp-modal-footer&gt;
        &lt;/abp-modal&gt;
    &lt;/abp-dynamic-form&gt;
&lt;/div&gt;
</code></pre>
<h3>EditModal</h3>
<pre><code class="language-csharp">&lt;div class=&quot;abp-view-modal&quot;&gt;
    &lt;abp-dynamic-form abp-model=&quot;Book&quot; asp-page=&quot;/Books/EditModal&quot;&gt;
        &lt;abp-modal size=&quot;ExtraLarge&quot;&gt;
            &lt;abp-modal-header title=&quot;@L[&quot;Update&quot;].Value&quot;&gt;&lt;/abp-modal-header&gt;
            &lt;abp-modal-body&gt;
                &lt;abp-form-content /&gt;
            &lt;/abp-modal-body&gt;
            &lt;abp-modal-footer buttons=&quot;@(AbpModalButtons.Save)&quot;&gt;&lt;/abp-modal-footer&gt;
        &lt;/abp-modal&gt;
    &lt;/abp-dynamic-form&gt;
&lt;/div&gt;
</code></pre>
<p>You can check this Git commit for details.</p>
<p>https://github.com/abpframework/abp-samples/commit/f3014e0ec422cb2d8816d0e00dd6ab9cc1adfc21</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/the-new-ef-core-interceptors-in-entity-framework-core-7.0-gzpm29hp</guid>
      <link>https://abp.io/community/posts/the-new-ef-core-interceptors-in-entity-framework-core-7.0-gzpm29hp</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>entity-framework-core</category>
      <category>dotnet7</category>
      <category>dotnet</category>
      <title>The new EF Core interceptors in Entity Framework Core 7.0</title>
      <description>The new EF Core 7 interceptors.</description>
      <pubDate>Wed, 23 Nov 2022 06:04:33 Z</pubDate>
      <a10:updated>2026-09-26T01:14:41Z</a10:updated>
      <content:encoded><![CDATA[<h1>The new EF Core interceptors</h1>
<h2>Interceptors</h2>
<p>EF Core 7 has made a lot of enhancements to interceptors, You can see the list from <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#new-and-improved-interceptors-and-events">EF Core improved interceptors</a>.</p>
<ul>
<li>Interception for creating and populating new entity instances (aka &quot;materialization&quot;)</li>
<li>Interception to modify the LINQ expression tree before a query is compiled</li>
<li>Interception for optimistic concurrency handling (DbUpdateConcurrencyException)</li>
<li>Interception for connections before checking if the connection string has been set</li>
<li>Interception for when EF Core has finished consuming a result set, but before that result set is closed</li>
<li>Interception for the creation of a DbConnection by EF Core</li>
<li>Interception for DbCommand after it has been initialized</li>
</ul>
<h2>Lazy initialization of <code>connection string</code></h2>
<p>You generally don't need to use <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#lazy-initialization-of-a-connection-string">this</a> feature, ABP has its own <a href="https://docs.abp.io/en/abp/latest/Connection-Strings">connection string feature</a>.</p>
<p>The framework will automatically handle the module or multi-tenant connection string</p>
<h2>Add interceptors in <code>AbpDbContext</code></h2>
<p><a href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#registering-interceptors">Add interceptors</a> is very simple, Add your <code>interceptors</code> in the <code>OnConfiguring</code> method of <code>DbContext</code></p>
<pre><code class="language-csharp">public class BookStoreDbContext : AbpDbContext&lt;BookStoreDbContext&gt;,
{

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

    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.AddInterceptors(new MyEfCorenterceptor());

        base.OnConfiguring(optionsBuilder);
    }
}
</code></pre>
<blockquote>
<p>Some interceptors may be <a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0#service-lifetimes">Singleton</a> services. This means a single instance is used by many <code>DbContext</code> instances. The implementation must be thread-safe.</p>
</blockquote>
<p>See the <a href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors">EF Core Interceptors documentation</a> for more information.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/dddbf507-6ed8-ffce-c442-3a07b543607a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/dddbf507-6ed8-ffce-c442-3a07b543607a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z</guid>
      <link>https://abp.io/community/posts/how-to-add-a-custom-grant-type-in-openiddict.-6v0df94z</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>openiddict</category>
      <title>How to add a custom grant type in OpenIddict.</title>
      <description>How to add a custom grant type in OpenIddict</description>
      <pubDate>Tue, 15 Nov 2022 09:19:15 Z</pubDate>
      <a10:updated>2026-09-25T20:42:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to add a custom grant type in OpenIddict</h1>
<h2>ITokenExtensionGrant</h2>
<p>Create a <code>MyTokenExtensionGrant</code> class that inherits <code>ITokenExtensionGrant</code>, and then register it with the framework.</p>
<pre><code class="language-cs">public override void PreConfigureServices(ServiceConfigurationContext context)
{
    //...
    PreConfigure&lt;OpenIddictServerBuilder&gt;(builder =&gt;
    {
        builder.Configure(openIddictServerOptions =&gt;
        {
            openIddictServerOptions.GrantTypes.Add(MyTokenExtensionGrant.ExtensionGrantName);
        });
    });
    //...
}

public override void ConfigureServices(ServiceConfigurationContext context)
{
    //...
    Configure&lt;AbpOpenIddictExtensionGrantsOptions&gt;(options =&gt;
    {
        options.Grants.Add(MyTokenExtensionGrant.ExtensionGrantName, new MyTokenExtensionGrant());
    });
    //...
}
</code></pre>
<h2>Generate a new token response</h2>
<p>In the <code>MyTokenExtensionGrant</code> class below we have two methods to get a new token using a user token or user API key. You can choose one of them based on your business.</p>
<p>These methods are just examples. Please add more logic to validate input data.</p>
<pre><code class="language-cs">using System.Collections.Immutable;
using System.Security.Principal;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using Volo.Abp.Identity;
using Volo.Abp.OpenIddict;
using Volo.Abp.OpenIddict.ExtensionGrantTypes;
using IdentityUser = Volo.Abp.Identity.IdentityUser;
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;

namespace OpenIddict.Demo.Server.ExtensionGrants;

public class MyTokenExtensionGrant : ITokenExtensionGrant
{
    public const string ExtensionGrantName = &quot;MyTokenExtensionGrant&quot;;

    public string Name =&gt; ExtensionGrantName;

    public async Task&lt;IActionResult&gt;  HandleAsync(ExtensionGrantContext context)
    {
        // You can get a new token using any of the following methods based on your business.
        // They are just examples. You can implement your own logic here.

        return await HandleUserAccessTokenAsync(context);
        return await HandleUserApiKeyAsync(context);
    }

    public async Task&lt;IActionResult&gt;  HandleUserAccessTokenAsync(ExtensionGrantContext context)
    {
        var userToken = context.Request.GetParameter(&quot;token&quot;).ToString();

        if (string.IsNullOrEmpty(userToken))
        {
            return new ForbidResult(
                new[] {OpenIddictServerAspNetCoreDefaults.AuthenticationScheme},
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest
                }!));
        }

        // We will validate the user token
        // The Token is issued by the OpenIddict server, So we can validate it using the introspection endpoint

        var transaction = await context.HttpContext.RequestServices.GetRequiredService&lt;IOpenIddictServerFactory&gt;().CreateTransactionAsync();
        transaction.EndpointType = OpenIddictServerEndpointType.Introspection;
        transaction.Request = new OpenIddictRequest
        {
            ClientId = context.Request.ClientId,
            ClientSecret = context.Request.ClientSecret,
            Token = userToken
        };

        var notification = new OpenIddictServerEvents.ProcessAuthenticationContext(transaction);
        var dispatcher = context.HttpContext.RequestServices.GetRequiredService&lt;IOpenIddictServerDispatcher&gt;();
        await dispatcher.DispatchAsync(notification);

        if (notification.IsRejected)
        {
            return new ForbidResult(
                new []{ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri
                }));
        }

        var principal = notification.GenericTokenPrincipal;
        if (principal == null)
        {
            return new ForbidResult(
                new []{ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme },
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = notification.Error ?? OpenIddictConstants.Errors.InvalidRequest,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = notification.ErrorDescription,
                    [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = notification.ErrorUri
                }));
        }

        // We have validated the user token and got the user id

        var userId = principal.FindUserId();
        var userManager = context.HttpContext.RequestServices.GetRequiredService&lt;IdentityUserManager&gt;();
        var user = await userManager.GetByIdAsync(userId.Value);
        var userClaimsPrincipalFactory = context.HttpContext.RequestServices.GetRequiredService&lt;IUserClaimsPrincipalFactory&lt;IdentityUser&gt;&gt;();
        var claimsPrincipal = await userClaimsPrincipalFactory.CreateAsync(user);

        // Prepare the scopes
        var scopes = GetScopes(context);

        claimsPrincipal.SetScopes(scopes);
        claimsPrincipal.SetResources(await GetResourcesAsync(context, scopes));
        await context.HttpContext.RequestServices.GetRequiredService&lt;AbpOpenIddictClaimsPrincipalManager&gt;().HandleAsync(context.Request, principal);
        return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal);
    }


    protected async Task&lt;IActionResult&gt; HandleUserApiKeyAsync(ExtensionGrantContext context)
    {
        var userApiKey = context.Request.GetParameter(&quot;user_api_key&quot;).ToString();

        if (string.IsNullOrEmpty(userApiKey))
        {
            return new ForbidResult(
                new[] {OpenIddictServerAspNetCoreDefaults.AuthenticationScheme},
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest
                }!));
        }

        // Here we can validate the user API key and get the user id
        if (false) // Add your own logic here
        {
            // If the user API key is invalid
            return new ForbidResult(
                new[] {OpenIddictServerAspNetCoreDefaults.AuthenticationScheme},
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest
                }!));
        }

        // Add your own logic to get the user by API key
        var userManager = context.HttpContext.RequestServices.GetRequiredService&lt;IdentityUserManager&gt;();
        var user = await userManager.FindByNameAsync(&quot;admin&quot;);
        if (user == null)
        {
            return new ForbidResult(
                new[] {OpenIddictServerAspNetCoreDefaults.AuthenticationScheme},
                properties: new AuthenticationProperties(new Dictionary&lt;string, string&gt;
                {
                    [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidRequest
                }!));
        }

        // Create a principal for the user
        var userClaimsPrincipalFactory = context.HttpContext.RequestServices.GetRequiredService&lt;IUserClaimsPrincipalFactory&lt;IdentityUser&gt;&gt;();
        var claimsPrincipal = await userClaimsPrincipalFactory.CreateAsync(user);

        // Prepare the scopes
        var scopes = GetScopes(context);

        claimsPrincipal.SetScopes(scopes);
        claimsPrincipal.SetResources(await GetResourcesAsync(context, scopes));
        await context.HttpContext.RequestServices.GetRequiredService&lt;AbpOpenIddictClaimsPrincipalManager&gt;().HandleAsync(context.Request, claimsPrincipal);
        return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, claimsPrincipal);
    }

    private ImmutableArray&lt;string&gt; GetScopes(ExtensionGrantContext context)
    {
        // Prepare the scopes
        // The scopes must be defined in the OpenIddict server

        // If you want to get the scopes from the request, you have to add `scope` parameter in the request
        // scope: AbpAPI profile roles email phone offline_access

        //var scopes = context.Request.GetScopes();

        // If you want to set the scopes here, you can use the following code
        var scopes = new[] { &quot;AbpAPI&quot;, &quot;profile&quot;, &quot;roles&quot;, &quot;email&quot;, &quot;phone&quot;, &quot;offline_access&quot; }.ToImmutableArray();

        return scopes;
    }

    private async Task&lt;IEnumerable&lt;string&gt;&gt; GetResourcesAsync(ExtensionGrantContext context, ImmutableArray&lt;string&gt; scopes)
    {
        var resources = new List&lt;string&gt;();
        if (!scopes.Any())
        {
            return resources;
        }

        await foreach (var resource in context.HttpContext.RequestServices.GetRequiredService&lt;IOpenIddictScopeManager&gt;().ListResourcesAsync(scopes))
        {
            resources.Add(resource);
        }
        return resources;
    }
}
</code></pre>
<h3>Get a new token using user access token</h3>
<ul>
<li>Get a user token using the <code>password</code> grant type.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-11-14-How-to-add-a-custom-grant-type-in-OpenIddict/1.png" alt="Http request 1" /></p>
<ul>
<li>Use the user token to get a new token using the <code>HandleUserAccessTokenAsync</code> method.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-11-14-How-to-add-a-custom-grant-type-in-OpenIddict/2.png" alt="Http request 2" /></p>
<h3>Get a new token using user API key</h3>
<ul>
<li>Directly get a new token using the <code>HandleUserApiKeyAsync</code> method.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-11-14-How-to-add-a-custom-grant-type-in-OpenIddict/3.png" alt="Http request 3" /></p>
<h2>Source code</h2>
<p>https://github.com/abpframework/abp/blob/dev/modules/openiddict/app/OpenIddict.Demo.Server/ExtensionGrants/MyTokenExtensionGrant.cs</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/588208b1-a312-43d8-2c43-3a078cc2beae" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/588208b1-a312-43d8-2c43-3a078cc2beae" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/videos/introduction-to-abp-framework-lr2g8a91</guid>
      <link>https://abp.io/community/videos/introduction-to-abp-framework-lr2g8a91</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <title>Introduction to ABP Framework</title>
      <description>We explain why to use the framework and the main features of the ABP framework and share a demo app.

我们解释了为什么要使用框架和ABP框架的主要特性并分享一个演示应用程序。</description>
      <pubDate>Mon, 28 Feb 2022 01:24:29 Z</pubDate>
      <a10:updated>2026-04-25T10:53:55Z</a10:updated>
      <content:encoded><![CDATA[We explain why to use the framework and the main features of the ABP framework and share a demo app.

我们解释了为什么要使用框架和ABP框架的主要特性并分享一个演示应用程序。 <br \> <a href="https://www.youtube.com/watch?v=ND01AOE-yr4" 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/3a3f661f-6baf-15c6-2cf9-3a02501aa6c6" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a3f661f-6baf-15c6-2cf9-3a02501aa6c6" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-test-blazor-components-in-abp-phcijx8e</guid>
      <link>https://abp.io/community/posts/how-to-test-blazor-components-in-abp-phcijx8e</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>test</category>
      <category>blazor-components</category>
      <title>How to Test Blazor Components in ABP</title>
      <description>In this article, I will use bUnit for a simple test of a Blazor component.</description>
      <pubDate>Thu, 20 Jan 2022 01:59:14 Z</pubDate>
      <a10:updated>2026-09-26T01:32:00Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Test Blazor Components in ABP</h1>
<h2>Source Code</h2>
<p>You can find the source of the example solution used in this article <a href="https://github.com/abpframework/abp-samples/tree/master/BlazorPageUniTest">here</a>.</p>
<p>In this article, I will use <a href="https://github.com/bUnit-dev/bUnit">bUnit</a> for a simple test of a Blazor component.</p>
<h2>Getting Started</h2>
<p>Use the ABP CLI to create a blazor app</p>
<p><code>abp new BookStore -t app -u blazor</code></p>
<p>Then add the <code>BookStore.Blazor.Tests</code> xunit test project to the solution, and add <a href="https://github.com/bUnit-dev/bUnit">bUnit</a> package and <code>ProjectReference</code> to the test project.</p>
<p>The contents of <code>BookStore.Blazor.Tests.csproj</code></p>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk&quot;&gt;

    &lt;PropertyGroup&gt;
        &lt;TargetFramework&gt;net8.0&lt;/TargetFramework&gt;
        &lt;Nullable&gt;enable&lt;/Nullable&gt;

        &lt;IsPackable&gt;false&lt;/IsPackable&gt;
    &lt;/PropertyGroup&gt;

    &lt;ItemGroup&gt;
        &lt;PackageReference Include=&quot;bunit&quot; Version=&quot;1.28.9&quot; /&gt;
        &lt;PackageReference Include=&quot;Microsoft.NET.Test.Sdk&quot; Version=&quot;17.9.0&quot; /&gt;
        &lt;PackageReference Include=&quot;Volo.Abp.Authorization.Abstractions&quot; Version=&quot;8.1.1&quot; /&gt;
        &lt;PackageReference Include=&quot;xunit&quot; Version=&quot;2.7.1&quot; /&gt;
        &lt;PackageReference Include=&quot;xunit.runner.visualstudio&quot; Version=&quot;2.5.8&quot;&gt;
            &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;/IncludeAssets&gt;
            &lt;PrivateAssets&gt;all&lt;/PrivateAssets&gt;
        &lt;/PackageReference&gt;
        &lt;PackageReference Include=&quot;coverlet.collector&quot; Version=&quot;6.0.2&quot;&gt;
            &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;/IncludeAssets&gt;
            &lt;PrivateAssets&gt;all&lt;/PrivateAssets&gt;
        &lt;/PackageReference&gt;
    &lt;/ItemGroup&gt;

    &lt;ItemGroup&gt;
      &lt;ProjectReference Include=&quot;..\..\src\BookStore.Blazor\BookStore.Blazor.csproj&quot; /&gt;
      &lt;ProjectReference Include=&quot;..\BookStore.EntityFrameworkCore.Tests\BookStore.EntityFrameworkCore.Tests.csproj&quot; /&gt;
    &lt;/ItemGroup&gt;

&lt;/Project&gt;

</code></pre>
<p>Create <code>BookStoreBlazorTestModule</code> that depends on <code>AbpAspNetCoreComponentsModule</code> and <code>BookStoreEntityFrameworkCoreTestModule</code>.</p>
<pre><code class="language-cs">[DependsOn(
    typeof(AbpAspNetCoreComponentsModule),
    typeof(BookStoreEntityFrameworkCoreTestModule)
)]
public class BookStoreBlazorTestModule : AbpModule
{

}
</code></pre>
<p>Create a <code>BookStoreBlazorTestBase</code> class and add the <code>CreateTestContext</code> method. The <code>CreateTestContext</code> have key code.</p>
<p>It creates a <code>AutofacServiceProvider</code> and add all ABP's services to the <code>TestContext</code>.</p>
<pre><code class="language-cs">public abstract class BookStoreBlazorTestBase : BookStoreTestBase&lt;BookStoreBlazorTestModule&gt;
{
    protected virtual TestContext CreateTestContext()
    {
        var testContext = new TestContext();
        var blazorise = testContext.JSInterop.SetupModule(&quot;./_content/Blazorise/utilities.js?v=1.5.1.0&quot;);
        blazorise.SetupVoid(&quot;log&quot;, _ =&gt; true);

        testContext.Services.UseServiceProviderFactory(serviceCollection =&gt;
        {
            foreach (var service in ServiceProvider.GetRequiredService&lt;IAbpApplicationWithExternalServiceProvider&gt;().Services)
            {
                serviceCollection.Add(service);
            }
            var containerBuilder = new ContainerBuilder();
            containerBuilder.Populate(serviceCollection);
            return new AutofacServiceProvider(containerBuilder.Build());
        });

        testContext.Services.AddBlazorise().AddBootstrap5Providers().AddFontAwesomeIcons();
        testContext.Services.Replace(ServiceDescriptor.Transient&lt;IComponentActivator, ServiceProviderComponentActivator&gt;());

        return testContext;
    }
}
</code></pre>
<p>Finally, we add an <code>Index_Tests</code> class to test the <code>Index</code> component.</p>
<pre><code class="language-cs">public class Index_Tests : BookStoreBlazorTestBase
{
[Fact]
    public void Index_Test()
    {
        // Arrange
        var ctx = CreateTestContext();

        // Act
        var cut = ctx.RenderComponent&lt;BookStore.Blazor.Pages.Index&gt;();

        // Assert
        cut.Find(&quot;.lead&quot;).InnerHtml.Contains(&quot;Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.&quot;).ShouldBeTrue();
        cut.Find(&quot;#username&quot;).InnerHtml.Contains(&quot;Welcome admin&quot;).ShouldBeTrue();
    }
}
</code></pre>
<h2>Reference document</h2>
<p>https://github.com/bUnit-dev/bUnit</p>
<p>https://docs.microsoft.com/en-us/aspnet/core/blazor/test</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/7d54cbe2-bb54-f2e0-d470-3a018762750b" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/7d54cbe2-bb54-f2e0-d470-3a018762750b" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-miniprofiler-with-the-abp-framework-6el5dziz</guid>
      <link>https://abp.io/community/posts/using-miniprofiler-with-the-abp-framework-6el5dziz</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>MiniProfiler</category>
      <title>Using MiniProfiler with the ABP Framework</title>
      <description>MiniProfiler is a library and UI for profiling your application. By letting you see where your time is spent, which queries are run, and any other custom timings you want to add, MiniProfiler helps you debug issues and optimize performance.</description>
      <pubDate>Mon, 22 Nov 2021 09:07:15 Z</pubDate>
      <a10:updated>2026-09-25T22:02:48Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using MiniProfiler with the ABP Framework</h1>
<p>This is an example project that demonstrates using MiniProfiler with the ABP Framework. See the article that explain this project:</p>
<p><strong>https://abp.io/community/posts/using-miniprofiler-with-the-abp-framework-6el5dziz</strong></p>
<p><strong>https://github.com/abpframework/abp-samples/tree/master/MiniProfiler</strong></p>
<p><strong>https://github.com/abpframework/abp-samples/tree/master/MiniProfiler-Tiered</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-override-localization-strings-of-depending-modules-ba1oy03l</guid>
      <link>https://abp.io/community/posts/how-to-override-localization-strings-of-depending-modules-ba1oy03l</link>
      <a10:author>
        <a10:name>maliming</a10:name>
        <a10:uri>https://abp.io/community/members/maliming</a10:uri>
      </a10:author>
      <category>localization</category>
      <title>How to override localization strings of depending modules</title>
      <description>We will use a real case to explain how to override localization strings.</description>
      <pubDate>Wed, 29 Sep 2021 02:32:56 Z</pubDate>
      <a10:updated>2026-09-26T01:33:07Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to override localization strings of depending modules</h1>
<h2>Source Code</h2>
<p>You can find the source of the example solution used in this article <a href="https://github.com/abpframework/abp-samples/tree/master/DocumentationSamples/ExtendLocalizationResource">here</a>.</p>
<h2>Getting Started</h2>
<p>This example is based on the following document
https://docs.abp.io/en/abp/latest/Localization#extending-existing-resource</p>
<p>We will change the default <code>DisplayName:Abp.Timing.Timezone</code> and <code>Description:Abp.Timing.Timezone</code> of <a href="https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/AbpTimingResource.cs"><code>AbpTimingResource</code></a> and add localized text in <a href="https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/en.json">Russian language(<code>ru</code>)</a>.</p>
<p>I created the <code>AbpTiming</code> folder in the <code>Localization</code> directory of the <code>ExtendLocalizationResource.Domain.Shared</code> project.</p>
<p>Create <code>en.json</code> and <code>ru.json</code> in its directory.</p>
<p><code>en.json</code></p>
<pre><code class="language-json">{
  &quot;culture&quot;: &quot;en&quot;,
  &quot;texts&quot;: {
    &quot;DisplayName:Abp.Timing.Timezone&quot;: &quot;My Time zone&quot;,
    &quot;Description:Abp.Timing.Timezone&quot;: &quot;My Application time zone&quot;
  }
}
</code></pre>
<p><code>ru.json</code></p>
<pre><code class="language-json">{
  &quot;culture&quot;: &quot;ru&quot;,
  &quot;texts&quot;: {
    &quot;DisplayName:Abp.Timing.Timezone&quot;: &quot;Часовой пояс&quot;,
    &quot;Description:Abp.Timing.Timezone&quot;: &quot;Часовой пояс приложения&quot;
  }
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2021-09-25-How-to-Override-Localization-Strings-Of-Depending-Modules/1.png" alt="" /></p>
<p>We have below content in <code>ExtendLocalizationResource.Domain.Shared.csproj</code> file, See <a href="https://docs.abp.io/en/abp/latest/Virtual-File-System#working-with-the-embedded-files">Virtual-File-System</a> understand how it works.</p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
    &lt;EmbeddedResource Include=&quot;Localization\ExtendLocalizationResource\*.json&quot; /&gt;
    &lt;Content Remove=&quot;Localization\ExtendLocalizationResource\*.json&quot; /&gt;
&lt;/ItemGroup&gt;

&lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;Microsoft.Extensions.FileProviders.Embedded&quot; Version=&quot;5.0.*&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
<p>Change the code of the <code>ConfigureServices</code> method in <code>ExtendLocalizationResourceDomainSharedModule</code>.</p>
<pre><code class="language-cs">Configure&lt;AbpLocalizationOptions&gt;(options =&gt;
{
    options.Resources
        .Add&lt;ExtendLocalizationResourceResource&gt;(&quot;en&quot;)
        .AddBaseTypes(typeof(AbpValidationResource))
        .AddVirtualJson(&quot;/Localization/ExtendLocalizationResource&quot;);

    //add following code
    options.Resources
        .Get&lt;AbpTimingResource&gt;()
        .AddVirtualJson(&quot;/Localization/AbpTiming&quot;);

    options.DefaultResourceType = typeof(ExtendLocalizationResourceResource);
});
</code></pre>
<p>Execute <code>ExtendLocalizationResource.DbMigrator</code> to migrate the database and run <code>ExtendLocalizationResource.Web</code>.</p>
<p>We have changed the English localization text and added Russian localization.</p>
<h3>Index page</h3>
<pre><code class="language-cs">&lt;p&gt;@AbpTimingResource[&quot;DisplayName:Abp.Timing.Timezone&quot;]&lt;/p&gt;
@using(CultureHelper.Use(&quot;ru&quot;))
{
    &lt;p&gt;@AbpTimingResource[&quot;DisplayName:Abp.Timing.Timezone&quot;]&lt;/p&gt;
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2021-09-25-How-to-Override-Localization-Strings-Of-Depending-Modules/2.png" alt="" /></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
  </channel>
</rss>