Browse Source

Merge branch 'dev' into enhance-link-user

pull/25450/head
maliming 5 months ago
parent
commit
11eef282d8
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 16
      .github/scripts/CheckDocsSyntax/CheckDocsSyntax.csproj
  2. 347
      .github/scripts/CheckDocsSyntax/Program.cs
  3. 221
      .github/workflows/check-docs-syntax.yml
  4. 43
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  5. 4
      common.props
  6. 89
      docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/POST.md
  7. BIN
      docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/cover-image.png
  8. BIN
      docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/upgrade-abp-packages.png
  9. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-ai-review.gif
  10. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-analyze-engine.png
  11. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-code-generation.gif
  12. 244
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-studio-ai-announcement.md
  13. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-studio-new-design.png
  14. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/community-talk-cover-image.png
  15. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/cover.png
  16. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/studio-ai-coding-assistant.jpg
  17. BIN
      docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/studio-solution-runner-and-monitor.png
  18. 4
      docs/en/docs-nav.json
  19. 72
      docs/en/framework/ui/react-native/index.md
  20. 159
      docs/en/framework/ui/react-native/styling-with-nativewind.md
  21. BIN
      docs/en/get-started/images/abp-studio-mobile-sample.gif
  22. BIN
      docs/en/images/rn-home-screen.png
  23. BIN
      docs/en/images/rn-login-iphone.png
  24. BIN
      docs/en/images/rn-nav-comparison.png
  25. 12
      docs/en/modules/openiddict.md
  26. 12
      docs/en/solution-templates/layered-web-application/mobile-applications.md
  27. 12
      docs/en/solution-templates/microservice/mobile-applications.md
  28. 6
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs
  29. 6
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs
  30. 17
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs
  31. 6
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs
  32. 8
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/ISupportsCronScheduling.cs
  33. 8
      framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/ISupportsRuntimeRegistration.cs
  34. 42
      framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json
  35. 51
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs
  36. 9
      latest-versions.json
  37. 4
      modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj
  38. 7
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs
  39. 29
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs
  40. 92
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Claims/AbpDefaultScopesHandler.cs

16
.github/scripts/CheckDocsSyntax/CheckDocsSyntax.csproj

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Volo.Abp.Docs.SyntaxCheck</RootNamespace>
<AssemblyName>CheckDocsSyntax</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Scriban" />
</ItemGroup>
</Project>

347
.github/scripts/CheckDocsSyntax/Program.cs

@ -0,0 +1,347 @@
using System.Text.Json;
using Scriban;
using Scriban.Runtime;
using Scriban.Syntax;
// Validates the Scriban template syntax embedded in `docs/en/` Markdown files.
//
// For each input file we run `Template.Parse` and a strict-mode render with the
// same parameter set the docs renderer injects at runtime (each docs-params.json
// `<name>` and its `<name>_Value` companion, plus Document_Language_Code,
// Document_Version and Release_Status). StrictVariables is enabled on purpose so
// references that would otherwise be silently rendered as empty strings surface
// as build failures here.
//
// Known limitations:
// - Partial template inlining is not executed: partial bodies are loaded from
// external storage at render time, so they cannot be resolved in CI. Files
// under `docs/en/` currently have no `//[doc-template]` references; if one is
// added later, errors inside the partial body must be reviewed manually.
// - Cookie- and query-string-driven parameter overrides are not injected, but
// their keys still resolve to empty strings because they layer on top of the
// same `<name>` / `<name>_Value` entries that are already injected.
namespace Volo.Abp.Docs.SyntaxCheck;
internal static class Program
{
private const string DefaultDocsRoot = "docs/en";
private const string DocsParamsFileName = "docs-params.json";
private static readonly string[] BuiltInVariables =
{
"Document_Language_Code",
"Document_Version",
"Release_Status"
};
public static int Main(string[] args)
{
var useGitHubAnnotations = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true";
var inputPaths = args.Length == 0
? new[] { DefaultDocsRoot }
: args;
var files = new List<string>();
foreach (var path in inputPaths)
{
if (File.Exists(path))
{
if (path.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
{
files.Add(Path.GetFullPath(path));
}
}
else if (Directory.Exists(path))
{
foreach (var file in Directory.EnumerateFiles(path, "*.md", SearchOption.AllDirectories))
{
files.Add(Path.GetFullPath(file));
}
}
else
{
Console.Error.WriteLine($"WARN: path does not exist: {path}");
}
}
if (files.Count == 0)
{
Console.WriteLine("No markdown files to check.");
return 0;
}
Dictionary<string, string> renderParameters;
try
{
renderParameters = BuildRenderParameters(files);
}
catch (Exception ex)
{
Console.Error.WriteLine($"ERROR: failed to load docs-params: {ex.Message}");
if (useGitHubAnnotations)
{
Console.WriteLine($"::error::docs-params: {EscapeAnnotation(ex.Message)}");
}
return 1;
}
var errorCount = 0;
var warningCount = 0;
var fileIssueCount = 0;
var repoRoot = TryFindRepoRoot(Directory.GetCurrentDirectory());
foreach (var file in files)
{
var fileIssues = CheckFile(file, renderParameters);
if (fileIssues.Count == 0)
{
continue;
}
fileIssueCount++;
foreach (var issue in fileIssues)
{
if (issue.Severity == IssueSeverity.Error)
{
errorCount++;
}
else
{
warningCount++;
}
var displayPath = repoRoot != null
? Path.GetRelativePath(repoRoot, file)
: file;
var severityLabel = issue.Severity == IssueSeverity.Error ? "error" : "warning";
Console.WriteLine(
$"{displayPath}:{issue.Line}:{issue.Column}: {severityLabel}: [{issue.Kind}] {issue.Message}");
if (useGitHubAnnotations)
{
var command = issue.Severity == IssueSeverity.Error ? "error" : "warning";
Console.WriteLine(
$"::{command} file={displayPath},line={issue.Line},col={issue.Column}::" +
$"{issue.Kind}: {EscapeAnnotation(issue.Message)}");
}
}
}
Console.WriteLine();
Console.WriteLine($"Checked {files.Count} markdown file(s). " +
$"{fileIssueCount} file(s) with issues, " +
$"{errorCount} error(s), {warningCount} warning(s).");
if (errorCount > 0 || warningCount > 0)
{
Console.WriteLine();
Console.WriteLine("Tip: wrap inline Scriban-looking text with `{%{{{ ... }}}%}` " +
"or wrap whole code blocks with `{%{` ... `}%}` to escape Scriban parsing.");
}
return errorCount > 0 ? 1 : 0;
}
private static List<Issue> CheckFile(string file, IReadOnlyDictionary<string, string> renderParameters)
{
var issues = new List<Issue>();
string content;
try
{
content = File.ReadAllText(file);
}
catch (Exception ex)
{
issues.Add(new Issue("Read", 1, 1, ex.Message, IssueSeverity.Error));
return issues;
}
var template = Template.Parse(content, file);
foreach (var message in template.Messages)
{
var severity = message.Type switch
{
Scriban.Parsing.ParserMessageType.Error => IssueSeverity.Error,
Scriban.Parsing.ParserMessageType.Warning => IssueSeverity.Warning,
_ => (IssueSeverity?)null
};
if (severity is null)
{
continue;
}
var kind = severity == IssueSeverity.Error ? "ScribanParseError" : "ScribanParseWarning";
issues.Add(new Issue(
kind,
message.Span.Start.Line + 1,
message.Span.Start.Column + 1,
message.Message,
severity.Value));
}
if (template.HasErrors)
{
return issues;
}
try
{
var context = new TemplateContext
{
StrictVariables = true
};
var scriptObject = new ScriptObject();
foreach (var entry in renderParameters)
{
scriptObject[entry.Key] = entry.Value;
}
context.PushGlobal(scriptObject);
template.Render(context);
}
catch (ScriptRuntimeException ex)
{
issues.Add(new Issue(
"ScribanRenderError",
ex.Span.Start.Line + 1,
ex.Span.Start.Column + 1,
ex.OriginalMessage,
IssueSeverity.Error));
}
catch (Exception ex)
{
issues.Add(new Issue("ScribanRenderError", 1, 1, ex.Message, IssueSeverity.Error));
}
return issues;
}
private static Dictionary<string, string> BuildRenderParameters(IEnumerable<string> files)
{
// Reproduces the keys the docs renderer places into its parameter
// dictionary before rendering a documentation page.
var parameters = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var name in BuiltInVariables)
{
parameters[name] = string.Empty;
}
foreach (var paramName in DiscoverParameterNames(files))
{
parameters[paramName] = string.Empty;
parameters[paramName + "_Value"] = string.Empty;
}
return parameters;
}
private static HashSet<string> DiscoverParameterNames(IEnumerable<string> files)
{
var names = new HashSet<string>(StringComparer.Ordinal);
var visitedDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var file in files)
{
var dir = Path.GetDirectoryName(file);
while (!string.IsNullOrEmpty(dir) && visitedDirs.Add(dir))
{
var candidate = Path.Combine(dir, DocsParamsFileName);
if (File.Exists(candidate))
{
AddNamesFromDocsParamsFile(candidate, names);
}
var parent = Path.GetDirectoryName(dir);
if (string.IsNullOrEmpty(parent) || parent == dir)
{
break;
}
dir = parent;
}
}
return names;
}
private static void AddNamesFromDocsParamsFile(string path, HashSet<string> sink)
{
// A malformed docs-params.json would silently shrink the injected
// variable set and turn parameter file regressions into hard-to-debug
// "variable not found" errors on otherwise-fine markdown. Surface the
// failure directly so the contributor fixes the JSON instead.
using var doc = JsonDocument.Parse(File.ReadAllText(path));
if (!doc.RootElement.TryGetProperty("parameters", out var parameters) ||
parameters.ValueKind != JsonValueKind.Array)
{
throw new InvalidDataException(
$"{path}: expected a top-level `parameters` array.");
}
foreach (var parameter in parameters.EnumerateArray())
{
if (parameter.TryGetProperty("name", out var nameElement) &&
nameElement.ValueKind == JsonValueKind.String)
{
var name = nameElement.GetString();
if (!string.IsNullOrWhiteSpace(name))
{
sink.Add(name);
}
}
}
}
private static string? TryFindRepoRoot(string startDir)
{
var current = new DirectoryInfo(startDir);
while (current != null)
{
var gitPath = Path.Combine(current.FullName, ".git");
if (Directory.Exists(gitPath) || File.Exists(gitPath))
{
return current.FullName;
}
current = current.Parent;
}
return null;
}
private static string EscapeAnnotation(string text)
{
// GitHub workflow command escaping. `%` must be encoded first so the
// sequences we introduce below are not double-escaped.
return text
.Replace("%", "%25")
.Replace("\r", "%0D")
.Replace("\n", "%0A")
.Replace(":", "%3A")
.Replace(",", "%2C");
}
private enum IssueSeverity
{
Warning,
Error
}
private readonly record struct Issue(
string Kind,
int Line,
int Column,
string Message,
IssueSeverity Severity);
}

221
.github/workflows/check-docs-syntax.yml

@ -0,0 +1,221 @@
# Validates Scriban template syntax in PR-changed Markdown files under docs/en/,
# so escape issues are caught before they reach the published documentation.
name: Check Docs Syntax
on:
pull_request:
paths:
- 'docs/en/**/*.md'
- 'docs/en/docs-params.json'
- '.github/scripts/CheckDocsSyntax/**'
- '.github/workflows/check-docs-syntax.yml'
permissions:
contents: read
pull-requests: write
jobs:
check-scriban-syntax:
name: Validate Scriban syntax in docs/en
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Build syntax checker
run: dotnet build .github/scripts/CheckDocsSyntax/CheckDocsSyntax.csproj -c Release --nologo -v minimal
- name: Get changed markdown files
id: changed
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const changed = [];
let paramsChanged = false;
let page = 1;
while (true) {
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
page,
});
const PARAMS_PATH = 'docs/en/docs-params.json';
for (const f of files) {
const isMutation =
f.status === 'added' || f.status === 'modified' || f.status === 'renamed';
if (!isMutation) continue;
// For renames, GitHub puts the new path in `filename` and the
// old one in `previous_filename`. Detect docs-params.json on
// either side so renames into / out of that path still trigger
// the parameter-file validation path.
if (f.filename === PARAMS_PATH || f.previous_filename === PARAMS_PATH) {
paramsChanged = true;
}
if (f.filename.startsWith('docs/en/') && f.filename.endsWith('.md')) {
changed.push(f.filename);
}
}
if (files.length < 100) break;
page++;
}
core.setOutput('files', changed.join('\n'));
core.setOutput('count', changed.length.toString());
core.setOutput('paramsChanged', paramsChanged ? 'true' : 'false');
core.info(`Markdown files to check: ${changed.length}`);
core.info(`docs-params.json changed: ${paramsChanged}`);
for (const f of changed) {
core.info(` - ${f}`);
}
- name: Run syntax checker
id: checker
if: steps.changed.outputs.count != '0' || steps.changed.outputs.paramsChanged == 'true'
env:
CHANGED_FILES: ${{ steps.changed.outputs.files }}
PARAMS_CHANGED: ${{ steps.changed.outputs.paramsChanged }}
run: |
mapfile -t files <<< "$CHANGED_FILES"
args=()
for f in "${files[@]}"; do
if [ -n "$f" ] && [ -f "$f" ]; then
args+=("$f")
fi
done
if [ ${#args[@]} -eq 0 ]; then
if [ "$PARAMS_CHANGED" = "true" ] && [ -f "docs/en/index.md" ]; then
# No markdown changed, but docs-params.json did. Run the checker
# against a single known-clean page so BuildRenderParameters /
# docs-params.json parsing actually executes and fails fast on a
# malformed parameter file.
echo "docs-params.json changed but no markdown changed; validating params via docs/en/index.md."
args+=("docs/en/index.md")
else
echo "No existing markdown files to check (all changes are deletions)."
exit 0
fi
fi
# Capture the checker's stdout so a follow-up step can post it as a PR
# comment when the run fails, while still streaming it to the job log.
set -o pipefail
dotnet run --project .github/scripts/CheckDocsSyntax/CheckDocsSyntax.csproj \
-c Release --no-build -- "${args[@]}" 2>&1 | tee checker-output.txt
- name: Upsert PR comment on failure
if: failure() && steps.checker.conclusion == 'failure'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const MARKER = '<!-- check-docs-syntax-bot -->';
const prNumber = context.payload.pull_request.number;
let report = '';
try {
report = fs.readFileSync('checker-output.txt', 'utf8').trim();
} catch (e) {
report = '(checker output was not captured)';
}
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = [
MARKER,
'### Docs syntax check failed',
'',
'The Scriban syntax checker reported issues in the Markdown files this PR changes. Wrap inline Scriban-looking text with `{%{{{ ... }}}%}` or wrap whole code blocks with `{%{` ... `}%}` to keep it from being parsed as a template.',
'',
'<details><summary>Checker output</summary>',
'',
'```',
report,
'```',
'',
'</details>',
'',
`[Full run log](${runUrl})`,
].join('\n');
// Find an existing bot comment to update (idempotent across re-runs).
let existing = null;
for (let page = 1; ; page++) {
const { data } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
page,
});
existing = data.find(c => c.body && c.body.startsWith(MARKER));
if (existing || data.length < 100) break;
}
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated existing bot comment (#${existing.id}).`);
} else {
const { data: created } = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
core.info(`Created bot comment (#${created.id}).`);
}
- name: Resolve previous failure comment on success
# Clear any stale failure comment whenever this workflow run is green,
# even if the syntax checker step was skipped (e.g. when a later
# commit reverts the earlier failure so no markdown files appear in
# the PR's net diff).
if: success()
uses: actions/github-script@v7
with:
script: |
const MARKER = '<!-- check-docs-syntax-bot -->';
const prNumber = context.payload.pull_request.number;
for (let page = 1; ; page++) {
const { data } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
page,
});
const existing = data.find(c => c.body && c.body.startsWith(MARKER));
if (existing) {
const body = [
MARKER,
'### Docs syntax check passed',
'',
'The previously reported issues are no longer present in this PR.',
].join('\n');
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Cleared bot comment (#${existing.id}).`);
break;
}
if (data.length < 100) break;
}

43
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -348,15 +348,58 @@
"CompanySize": "Company size",
"DetailTrialLicense": "Details",
"Requested": "Requested",
"Pending": "Pending",
"Running": "Running",
"Activated": "Activated",
"PurchasedToNormalLicense": "Purchased",
"Expired": "Expired",
"TrialLicenseDeletionWarningMessage": "Are you sure you want to delete the trial license? Trial license, organization, support accounts will be deleted!",
"LicenseCategoryFilter": "License category",
"Permission:SendWelcomeEmail": "Send Welcome Email",
"Permission:ProvisionExistingOrganizationsAi": "Provision Existing Organizations AI",
"Permission:AiProviderKeyLimit": "Manage AI Provider Key Limit",
"SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmailWarningMessage": "Are you sure you want to send welcome email to the organization members?",
"SendWelcomeEmailSuccessMessage": "Welcome email sent successfully!",
"ProvisionExistingOrganizationsAi": "Provision Existing Organizations AI",
"ProvisionExistingOrganizationsAiConfirmation": "This will enable AI assisted development for all active organizations, grant included AI credits, and provision provider keys in the background. Do you want to continue?",
"DeleteExistingOrganizationsAiCredentials": "Delete Existing Organizations AI Keys",
"DeleteExistingOrganizationsAiCredentialsConfirmation": "This will revoke existing OpenRouter keys referenced by organizations and remove stored AI credentials from the database so provisioning can be retried. Do you want to continue?",
"ExistingOrganizationsAiOperationAlreadyRunning": "Another existing organizations AI operation is already running.",
"ExistingOrganizationsAiBackfillAlreadyRunning": "An existing organizations AI provisioning job is already running.",
"ExistingOrganizationsAiBackfillMissingManagementApiKey": "OpenRouter management API key is not configured for the admin application. Configure AiAssistedDevelopment:Providers:OpenRouter:ManagementApiKey before starting this operation.",
"NoActiveOrganizationsFoundForAiBackfill": "No active organizations were found for AI provisioning.",
"NoOrganizationsFoundForAiCredentialCleanup": "No organizations with AI credentials were found for cleanup.",
"ExistingOrganizationsAiBackfillNotFound": "The existing organizations AI provisioning operation was not found.",
"ExistingOrganizationsAiBackfillCompleted": "Existing organizations AI provisioning completed successfully.",
"ExistingOrganizationsAiBackfillFailed": "Existing organizations AI provisioning failed.",
"ExistingOrganizationsAiCredentialCleanupCompleted": "Existing organizations AI credential cleanup completed successfully.",
"ExistingOrganizationsAiCredentialCleanupFailed": "Existing organizations AI credential cleanup failed.",
"CurrentOrganization": "Current organization",
"AiProviderKeyLimit": "ABP AI Agent Provider Key Limit",
"AiProviderKeyMissing": "No provider key exists for this organization.",
"AiProviderKeyMissingDescription": "Create a provider key before setting the ABP AI Agent usable limit.",
"CreateAiProviderKey": "Create provider key",
"ProviderUsableLimit": "Provider usable limit",
"CustomerVisibleCredits": "Customer visible credits",
"ProviderRemaining": "Provider remaining",
"ProviderUsed": "Provider used",
"GrossToUsableRatio": "Gross to usable ratio",
"LastSync": "Last sync",
"NeverSynced": "Never synced",
"NotSynced": "Not synced",
"NewProviderUsableLimit": "New provider usable limit",
"NewProviderUsableLimitDescription": "This is the absolute OpenRouter usable USD limit. Customer-visible credits are calculated from the configured gross-to-usable ratio.",
"CustomerVisibleGrossPreview": "Customer visible gross preview",
"AiProviderLimitMustBeNonNegative": "AI provider limit must be greater than or equal to 0.",
"AiProviderLimitCannotBeLowerThanUsage": "AI provider limit cannot be lower than current provider usage.",
"AiProviderKeyLimitUpdateConfirmation": "Set OpenRouter usable limit to ${0}? Customer-visible credits will become ${1}.",
"Processed": "Processed",
"Succeeded": "Succeeded",
"Failed": "Failed",
"Cancelled": "Cancelled",
"CompletedAt": "Completed at",
"LastError": "Last error",
"Activate": "Activate",
"ActivateTrialLicenseWarningMessage": " When you activate a trial license, a welcome e-mail will be sent to the user. Do you want to activate it?",
"ActivateTrialLicenseSuccessMessage": "Activated successfully and the welcome e-mail sent to the organization members.",

4
common.props

@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>10.4.0</Version>
<LeptonXVersion>5.4.0</LeptonXVersion>
<Version>10.5.0-preview</Version>
<LeptonXVersion>5.5.0-preview</LeptonXVersion>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

89
docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/POST.md

@ -0,0 +1,89 @@
# ABP.IO Platform 10.4 Final Has Been Released!
We are glad to announce that [ABP](https://abp.io/) 10.4 stable version has been released.
## What's New With Version 10.4?
All the new features were explained in detail in the [10.4 RC Announcement Post](https://abp.io/community/announcements/announcing-abp-10-4-release-candidate-7ukyudm0), so there is no need to review them again. You can check it out for more details.
## Getting Started with 10.4
### How to Upgrade an Existing Solution
You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained:
### Upgrading via ABP Studio
If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info.
After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Upgrade ABP Packages** action button to instantly upgrade your solution:
![](upgrade-abp-packages.png)
### Upgrading via ABP CLI
Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version.
If you haven't installed it yet, you can run the following command:
```bash
dotnet tool install -g Volo.Abp.Studio.Cli
```
Or to update the existing CLI, you can run the following command:
```bash
dotnet tool update -g Volo.Abp.Studio.Cli
```
After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows:
```bash
abp update
```
You can run this command in the root folder of your solution to update all ABP related packages.
## Migration Guides
There are no explicitly marked breaking changes in this version. However, there are still some important migration notes for specific scenarios. Please read the migration guide carefully, if you are upgrading from v10.3 or earlier versions: [ABP Version 10.4 Migration Guide](https://abp.io/docs/10.4/release-info/migration-guides/abp-10-4)
## Community News
### Highlights from the ABP Community
There have been some important announcements for the ABP community recently. Here are two highlights you may want to check out:
#### React UI for ABP Framework Is Finally Here
![React UI for ABP Framework Is Finally Here](https://abp.io/api/posts/cover-picture-source/3a2114d0-5518-38a8-d9b3-ab5100b587a4?v=20260508112328)
React support has been one of the most requested topics in the ABP community, and with ABP 10.4, it becomes a first-class UI option in the modern template system. The new React UI is designed for teams that want ABP on the backend and React on the frontend while keeping ABP's built-in application features such as authentication, authorization, localization, multi-tenancy, modularity, runtime configuration, and deployment.
Modern React solutions include your own React application as real source code in the solution, plus the ABP Admin Console for standard module administration screens. This means your product UI stays fully under your control, while ABP still provides a consistent and upgradeable administration experience.
The React stack is built with familiar modern tools, including Vite, TypeScript, TanStack Router, TanStack Query, Axios, Zod, React Hook Form, Tailwind CSS, shadcn/ui, and Vitest. You can create a React UI solution with the `--modern` flag or by selecting the modern template flow in ABP Studio. You can read the announcement here: [React UI for ABP Framework Is Finally Here](https://abp.io/community/announcements/react-ui-for-abp-framework-is-finally-here-7rfmgb2v).
#### Introducing ABP Studio AI Agent
![Introducing ABP Studio AI Agent](https://abp.io/api/posts/cover-picture-source/3a212ebc-06c1-e10f-f83c-a90079f988c1?v=20260508112328)
ABP Studio now introduces ABP Agent, a deeply integrated AI coding assistant that understands ABP solutions as complete systems, not just as files in folders. It is aware of ABP concepts such as modules, layers, aggregate roots, repositories, application services, DTOs, permissions, localization, event bus, distributed cache, background jobs, and module dependencies.
ABP Agent works in three modes: Agent mode for implementation, Plan mode for read-only investigation and planning, and Ask mode for Q&A and explanations. It can use ABP Studio's analysis engine to understand the solution structure, build affected projects, start or restart applications, run tasks, generate proxies, add migrations, and inspect runtime feedback such as exceptions, logs, HTTP requests, and distributed events.
The announcement also highlights the broader development loop around ABP Agent: solution runner integration, custom workflows, task runner support, Git and GitHub integration, AI-generated commit messages, and ABP-aware AI code review. You can read the announcement here: [Introducing ABP Studio AI Agent](https://abp.io/community/announcements/introducing-abp-studio-ai-agent-o1ni0toc).
### New ABP Community Articles
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:
- [ABP in the AI Era: Surviving, Evolving, and Staying Relevant](https://abp.io/community/articles/abp-in-the-ai-era-surviving-evolving-and-staying-relevant-6gyfjfpe) by [Engincan Veske](https://abp.io/community/members/EngincanV)
- [Stop Sprinkling [RequiresFeature] Everywhere — A Centralized Feature Gate for ABP.IO](https://abp.io/community/articles/stop-sprinkling-requiresfeature-everywhere-a-centralized-7znie818) by [Mohammad AlMohammad AlMahmoud](https://abp.io/community/members/Mohammad97Dev)
- [Top AI Coding Models in 2026: Which One Should Developers Actually Use?](https://abp.io/community/articles/top-ai-coding-models-in-2026-which-one-should-developers-use-rivh8x15) by [Alper Ebiçoğlu](https://abp.io/community/members/alper)
Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community.
## About the Next Version
The next feature version will be 10.5. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version.

BIN
docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
docs/en/Blog-Posts/2026-05-14 v10_4_Release_Stable/upgrade-abp-packages.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-ai-review.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-analyze-engine.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-agent-code-generation.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 KiB

244
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-studio-ai-announcement.md

@ -0,0 +1,244 @@
# Introducing ABP Studio AI Agent
The new ABP Studio release introduces a deeply integrated set of features designed around one idea: an AI coding agent that truly understands ABP solutions, sitting inside an IDE that already knows how to build, run, monitor, and iterate on them.
At the center is **ABP Agent**, our AI coding assistant. Around it are long-standing ABP Studio capabilities that have been brought together into a single development loop. Together they turn ABP Studio into a single place where you architect and code your ABP solutions.
![abp-studio-ui](studio-ai-coding-assistant.jpg)
---
## Meet ABP Agent
![agent-working](abp-agent-code-generation.gif)
ABP Agent is the AI coding assistant built into ABP Studio. It operates in three modes, each tuned for a different stage of work:
- **Agent mode**: the implementation mode. The agent reads the solution, writes and edits files, builds the affected projects, runs your apps, watches the runtime, and iterates until the change works end-to-end.
- **Plan mode**: read-only planning. The agent investigates the codebase, researches the official ABP documentation, and produces a structured implementation plan (Problem, Solution, Workflow Diagram, Files Affected, Expected Result). When you are happy with the plan, a single click promotes it into Agent mode and the implementation starts from the plan.
- **Ask mode**: read-only Q&A. The agent explains how something works, draws diagrams, and answers questions about your code without touching any files.
The agent is **ABP-aware by default**. It is instructed to prefer ABP base classes over plain POCOs, repositories over direct `DbContext` injection, `ApplicationService` over plain services, the ABP permission system over `[Authorize(Roles=…)]`, localized strings over hardcoded text, `BusinessException`/`UserFriendlyException` over plain `Exception`, the distributed cache abstraction over raw memory cache, and background job abstractions over hand-rolled hosted services. When it is unsure about an ABP feature, it consults the official ABP documentation as a primary source of truth, not random blog posts on the web.
---
## Why It Was Needed
General-purpose AI coding tools (Cursor, Claude Code, Windsurf, opencode and similar) are excellent for horizontal, file-shaped work. They read source files, edit them, and run shell commands. That works well for small scripts or front-end apps.
ABP solutions are different. A typical ABP solution is **system-shaped**, not just file-shaped: the important context is not only where files are located, but how modules, layers, permissions, contracts, localization, persistence, and UI pieces work together:
- It is split across multiple modules and layers (Domain, Application, EntityFrameworkCore, HttpApi, Web, etc.) with strict dependency rules.
- It is composed of many runnable units: HTTP services, gateways, identity servers, background workers, SPAs, mobile apps, plus the Docker containers they depend on.
- It follows a strong set of conventions: aggregate roots, repositories, application services, DTOs, permissions, localization, event bus, distributed cache, background jobs.
- It is a *living* system at design time. You don't just read code, you run it, watch logs, follow distributed events, hit HTTP endpoints, and iterate.
A generic agent has none of that vocabulary. It does not know what a module is, which project is the Domain layer, or that an `ApplicationService` should not depend on a `DbContext` directly. It cannot start "the gateway, the auth server, and the two microservices my React app talks to". It just runs `dotnet run` in some folder and hopes. It cannot tell you that the agent's latest edit caused a runtime exception in the Identity service or made the `OrderPlacedEto` event handler silently fail, because it has no concept of "running app".
ABP Agent and the surrounding ABP Studio features were built to close exactly that gap. The agent is born inside an IDE that already understands modules, run profiles, builds, migrations, proxies, Docker containers, Kubernetes services, and distributed runtime telemetry, and it uses every one of them.
---
## How ABP Agent Sees Your Application: The Analyze Engine
![abp-agent-analyze-engine](abp-agent-analyze-engine.png)
Before ABP Agent answers anything, it needs to *understand* your solution. This is where the **Analyze** feature does the heavy lifting.
When you open a solution, ABP Studio analyzes every package and builds a structural map: the application's **skeleton**. It identifies what each type actually is in ABP terms: an aggregate root, an entity, a value object, a repository interface or implementation, a domain service, an application service or interface, a DTO, an integration service, a controller, an HTTP API, a background job or worker, an event handler, an ETO, a SignalR hub, a DbContext (EF Core or MongoDB), a permission/feature/setting provider, a global feature, a Mapperly or AutoMapper profile, a fluent validator, a menu contributor, a data seed contributor, an options class, an ABP module with its `[DependsOn]` chain, and many more.
For each of these, the analyzer captures the structural information that actually matters: properties, method signatures with their line ranges, injected dependencies, base classes, the permission groups and items, the feature tree, the settings catalog, the database tables and collections, without parsing C# at runtime.
ABP Agent receives this analyzed skeleton at the start of every session. That means:
- The agent already knows what types exist in each project and what role each one plays, before you ask anything.
- When you say *"add a Category to my Catalog module"*, the agent already knows where the Domain project is, what the existing aggregate roots look like, which DbContext should get the new entity, where the permissions provider lives, and which application service to extend.
- When the agent needs to change an existing type, it can fetch a precise outline (base classes, properties, method signatures with exact line ranges) instead of reading thousands of lines of source. That is dramatically faster, dramatically cheaper, and dramatically more accurate.
- When you modify code, ABP Studio re-analyzes only the affected packages and refreshes the agent's understanding.
This is what we mean by **solution-aware AI**. The agent does not "look at folders and guess"; it works from a typed, ABP-shaped index of your application.
---
## Native .NET Awareness: No Shell Gymnastics
A common pattern in generic agentic IDEs is to do everything through the terminal. Building? Run `dotnet build` in a shell. Restarting an app? Spawn a process. Adding a migration? More shell. Checking whether the build succeeded? Parse terminal output and pray.
ABP Agent does not work through a terminal for these things. Building, running, restarting, adding migrations, generating proxies, installing client-side libraries: all of these are **first-class operations** the agent invokes directly. Build calls return structured results with errors and warnings the agent can act on immediately. Starting an application returns a structured outcome: which apps started, which failed, and a summarized error log for each failure. There is no string-scraping, no "did the spinner stop yet?" guessing.
The agent also scopes builds intelligently. It can build a single project, a single module, or the entire solution depending on what changed. After editing a few files in your Application layer, it builds just that module (not the whole solution), and only escalates the scope when needed.
---
## Solution Runner & Live Runtime Monitor
![solution-runner-and-monitor](studio-solution-runner-and-monitor.png)
This is the half of ABP Agent that no general-purpose AI IDE can replicate, because no general-purpose AI IDE has a first-class runner for distributed .NET solutions.
**Solution Runner** is the ABP Studio feature that knows about every runnable thing in your solution: web apps, microservices, gateways, identity servers, background workers, CLI applications, mobile and SPA front-ends, plus the Docker containers your stack depends on (databases, caches, message brokers). Apps are grouped into folders and can be launched as a coherent set under a named *run profile*. You start everything with one click; ABP Studio handles ports, dependencies, restart-on-failure, and embedded browser previews.
ABP Agent talks to the Solution Runner directly. It can:
- Start, stop, or restart a specific application, every application inside a folder ("Backend/API"), or all applications.
- Start or stop Docker containers separately from applications.
- Run tasks defined in your run profile (database migrations, npm scripts, custom scripts).
When the agent starts an application that crashes on startup, it does not loop forever restarting it. It captures the recent logs from that application, summarizes the failure, returns a structured report to itself, and uses that to **fix the underlying bug in the code** before trying again.
But starting apps is only half of it. The **runtime monitor** is the other half. When your applications run under ABP Studio, the IDE collects, in real time:
- **Exceptions**: type, message, full stack trace, inner exceptions, source application.
- **Logs**: timestamps, log level, application, message.
- **HTTP requests**: method, URL, status code, response time.
- **Distributed events**: name, source, direction (published/received), and payload.
ABP Agent can query all of this. The development loop becomes:
> Generate the code → Build the affected module → Restart the affected application → Hit the endpoint or perform the action → Ask the agent to inspect the last exceptions, the failing HTTP requests, and the distributed events → Fix the bug → Repeat.
That is the loop a generic IDE cannot do, because it cannot see your application after it starts. ABP Agent can.
---
## Custom Workflows & Task Runner Integration: Determinism When You Need It
LLMs are powerful but non-deterministic. In real teams, some steps must happen **every time** (before or after the agent works) and they must happen in a known order. That is what **Custom Workflows** are for.
A workflow is a named sequence of deterministic steps with a *Before* phase and an *After* phase. You can configure steps such as:
- Build (whole solution, specific modules, or specific packages)
- Start, stop, or restart applications (specific apps, an entire folder, or all)
- Start or stop containers
- Run a Task (any custom task defined in your run profile: `npm install`, custom scripts, code generators, database resets, anything your team already wired into ABP Studio's Task Runner)
- Add a database migration
- Install client-side libraries
- Generate C# or Angular client proxies for HTTP APIs
For example, you can configure a workflow that:
- **Before** the agent works: starts the required containers and runs a code-generation Task.
- **After** the agent works: builds the affected modules, regenerates client proxies, restarts the gateway and the SPA, and adds a database migration.
Workflows can be **personal** to you, or **shared with your team** through the solution's run profile file, so the deterministic pre/post pipeline travels with the repository and every developer gets the same behavior.
The result is a clean separation: the LLM handles the creative, ambiguous middle (designing the change and writing the code), while your workflow guarantees the boring, must-happen steps around it. The agent becomes more deterministic exactly where determinism matters, without losing flexibility where it doesn't.
The Task Runner integration is what makes the *After* phase especially valuable. You can run absolutely anything as a post-step: npm scripts, custom executables, code generators, integration test runners, lint passes, database refresh scripts. If your team already runs it as part of "I just changed some code, now do X", you can run it automatically after every agent turn.
---
## Git & GitHub: Reviewing and Committing Without Leaving the IDE
![ai-review](abp-agent-ai-review.gif)
ABP Studio now ships a full Git client with deep GitHub integration. The goal is simple: once the agent finishes a change, you should be able to review, commit, push, branch, and respond to review feedback **without ever leaving ABP Studio**.
The Git side covers everything you'd expect:
- Stage and commit changes with rich, package-grouped change views.
- Create, switch, and merge branches.
- Stash and restore work, including a clear flow for stashing before switching branches.
- View commit history and create branches from any commit.
- Resolve merge conflicts with a built-in conflict editor.
- Push, pull, fetch, with seamless OAuth-based GitHub authentication.
On the GitHub side:
- Browse and filter Issues, view their comments, and send an issue (or just its comments) directly to ABP Agent to start working on it.
- View existing Pull Requests, see their requested changes and comments inline, and send the requested-changes feedback straight into ABP Agent so it can address the reviewer's comments. You can send the feedback into the same session you used to write the change, or start a fresh agent session.
- A one-click "Create Pull Request" action opens the new-PR page on GitHub directly from the IDE, pre-targeted at the current branch.
- Jump to any file on GitHub directly from the IDE.
Two AI-assisted touches make this loop especially smooth:
- **AI-generated commit messages.** Click "Generate with AI" and ABP Agent writes a Conventional Commits-style message from the staged diff. Edit it if you want, then commit.
- **AI Code Review on the diff.** Select the files you want reviewed, run AI review, and inline suggestions stream into the IDE as the analysis runs. Crucially, this is not a generic code review; it is an **ABP-aware** review. The reviewer looks for ABP-specific pattern violations: plain POCOs where ABP base classes belong, direct `DbContext` injection where a repository should be used, hardcoded strings where localization should be used, plain exceptions where `BusinessException` belongs, role-based authorization where ABP Permissions are the right answer, and so on. When the reviewer is unsure, it consults the official ABP documentation before flagging an issue.
The result is a *closed loop*: the agent writes the change, you (or the AI reviewer agent) review the diff, you let the agent fix the comments, you commit with an AI-suggested message, you push, and you head to GitHub for the pull request, all from inside ABP Studio.
---
## The End-to-End Development Loop
Put the pieces together and ABP Studio becomes the single place where the whole development cycle happens:
1. Open an issue from GitHub, send it to ABP Agent.
2. Switch to Plan mode; ABP Agent investigates the analyzed solution, consults the ABP docs, produces a structured plan.
3. Promote the plan to Agent mode. Implementation starts from the plan.
4. The *Before* workflow runs (start containers, run preparation tasks).
5. ABP Agent writes the code, batching edits across the affected modules, and fixes any compile errors directly.
6. The *After* workflow runs (build the affected modules, install client-side libraries, generate proxies, add a database migration, restart the impacted applications).
7. ABP Agent inspects the runtime monitor (exceptions, logs, HTTP requests, distributed events) and fixes any bug it sees.
8. You (or the AI reviewer) review the diff. Comments are sent back into the same agent session, or into a new one.
9. ABP Agent generates a commit message; you commit and push.
10. Open the new-pull-request page on GitHub with one click from ABP Studio. When reviewers leave requested changes, send them into the same agent session or start a fresh one, and iterate.
You do not need to switch to a terminal to build. You do not need a separate tool to start your microservices. You do not need a different IDE to watch logs. You do not need a separate window for Git and GitHub. **ABP Studio is the only program you use during development, all in one.**
---
## Learning Over Time
ABP Agent also has a small but powerful feedback feature: when it makes a mistake and you (or the build, or the ABP docs) correct it, it can save that correction as a **lesson**. Lessons are short, verified notes that the agent carries forward into future turns and future sessions, so the same mistake does not happen twice. Over time, the agent gets better at the specific conventions and quirks of *your* solution, not just generic ABP.
---
## Honest Comparison With Generic AI IDEs
To be fair: Cursor, Claude Code, Windsurf and opencode are excellent products. They have semantic search, plan/agent modes, sub-agents, file editing, and shell access, and we wouldn't dispute any of that. But for ABP development specifically, here is where ABP Agent is genuinely different:
| Capability | ABP Agent | Generic AI IDEs |
| --- | --- | --- |
| Aware of ABP roles (aggregate root, app service, repository, DTO, ETO, event handler, etc.) | Yes, structurally indexed | No (flat file/text view) |
| Knows your solution's modules, layers and dependency rules | Yes | No |
| Builds with module/package scope as a first-class operation | Yes, structured build result | No, runs `dotnet build` in a shell and parses output |
| First-class Solution Runner for distributed .NET apps + containers | Yes | No, generic shell processes |
| Restarting an application, opening its URL, waiting for it to be ready | Yes, built in | No |
| Live runtime telemetry as a tool: exceptions, logs, HTTP requests, distributed events | Yes | No |
| ABP-specific patterns checked during AI code review | Yes | Generic patterns only |
| Pre/post workflows with deterministic build / start / migrate / generate-proxies steps | Yes | No first-class concept |
| Task Runner integration for arbitrary pre/post steps owned by your team | Yes | No |
| Adding EF Core migrations as a first-class agent action | Yes | Shell only |
| Generating C#/Angular client proxies as a first-class agent action | Yes | Shell only |
| Native ABP documentation as authoritative source for the agent's decisions | Yes | Generic web search |
| Integrated Git + GitHub (issues, PR viewing, AI review) inside the same window | Yes | Partial / external |
The pattern is consistent: **anything ABP-specific or runtime-specific belongs to ABP Agent and ABP Studio. Anything general-purpose is something everyone has.** That is the line we drew on purpose.
---
## What This Means For You
If you build with ABP Framework, this release changes how you work in three concrete ways:
1. **You stop describing your solution to the AI.** ABP Agent already knows it.
2. **You stop bouncing between tools.** Editor, runner, runtime monitor, Git, GitHub, AI review, they all live in one window with one feedback loop.
3. **You stop accepting non-deterministic side effects.** Custom workflows + Task Runner integration make the boring steps boring again, while the agent focuses on the creative ones.
This is the first release that brings AI assistance to ABP Studio, and we built it so that it is designed *around* the agent rather than bolted on. We think this is the natural shape of AI-assisted enterprise .NET development.
---
## Short-Term Roadmap
This release is the first step. The features below are already on our short-term roadmap and will land in upcoming releases:
- **Debug Mode**: a dedicated mode where you and ABP Agent co-operate when debugging the solution. The agent can follow breakpoints, inspect state, propose fixes mid-session, and re-run after each change.
- **Browser control for the agent**: ABP Agent will be able to drive the embedded browser, browse pages, fill forms, and click buttons, so it can verify a UI flow end-to-end on its own and report what it observed.
- **Custom Workflows improvements**: more step types, richer conditions, finer-grained targets, and better visibility into which workflow ran for which agent turn.
- **GitHub integration improvements**: in-IDE pull request creation (no more jumping to the GitHub website), richer review handling, and more first-class issue and PR actions for the agent.
- **Git integration improvements**: more advanced day-to-day Git operations available without leaving the IDE.
- **Design helper**: ABP Agent will generate images.
- **Create a new project with the agent**: an AI-driven solution creation flow where you describe the application you want and ABP Agent helps choose the right template, modules, and configuration.
- **ABP Suite integration**: ABP Agent will be able to invoke ABP Suite for CRUD-page generation. Instead of asking the LLM to write the full set of layers for a CRUD page (which spends a lot of tokens and time), the agent will hand the task to ABP Suite, get a deterministic, production-quality result back, and continue with the parts that actually need the AI.
## Live Demo
We have previewed the ABP Agent in our latest community talk. Click to watch it:
[![ABP Agent community talk demo video cover image](community-talk-cover-image.png)](https://www.youtube.com/watch?v=GYVFn2lRuWw)
### Also see
* https://abp.io/studio/ai-agent

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/abp-studio-new-design.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/community-talk-cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/studio-ai-coding-assistant.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
docs/en/Community-Articles/2026-05-12-Introducing-Abp-Studio-Ai-Agent/studio-solution-runner-and-monitor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

4
docs/en/docs-nav.json

@ -1935,6 +1935,10 @@
"text": "Overview",
"path": "framework/ui/react-native",
"isIndex": true
},
{
"text": "Styling with NativeWind",
"path": "framework/ui/react-native/styling-with-nativewind.md"
}
]
},

72
docs/en/framework/ui/react-native/index.md

@ -18,7 +18,7 @@
The ABP platform provides a basic [React Native](https://reactnative.dev/) startup template to develop mobile applications **integrated with your ABP-based backends**.
React Native gif
> The startup template UI is built with **[NativeWind v4](https://www.nativewind.dev/)** (Tailwind CSS for React Native) on top of a shadcn-inspired neutral palette, with full **light/dark mode** support. See [Styling with NativeWind](styling-with-nativewind.md) for the styling system reference.
## How to Prepare Development Environment
@ -128,19 +128,85 @@ In the image above, you can start the application on an Android emulator, an iOS
### Expo
React Native login screen on iPhone 16
Press **i** to open the iOS simulator, or scan the QR code from the Expo CLI with your phone to run on a physical device.
### Android Studio
1. Start the emulator in **Android Studio** before running the `yarn start` or `npm start` command.
2. Press **a** to open in Android Studio.
React Native login screen on Android Device
<img width="360" src="../../../images/rn-login-iphone.png" alt="React Native login screen" />
Enter **admin** as the username and **1q2w3E** as the password to log in to the application.
The application is up and running. You can continue to develop your application based on this startup template.
## Navigation
The startup template ships with **two navigation styles**, switchable when the project is created:
- **Bottom Tab** — *the default* — three tabs at the bottom of the screen: **Home**, **Settings** and **Account**.
- **Drawer** — a side menu (hamburger) with two items: **Home** and **Settings**.
<img width="600" src="../../../images/rn-nav-comparison.png" alt="Bottom Tab vs Drawer navigation comparison" />
Every main tab or drawer item is wired to **its own** native stack (`@react-navigation/native-stack`). Pushing more screens stays on that branch: the Back stack belongs to that tab or drawer route and does not mix with others. Bottom Tab and Drawer use the **same screen components**; they differ in how those screens are grouped and opened from the outer shell (and where the sign‑in/sign‑up flow lives in Bottom Tab versus Drawer).
> **How to choose:** The mode is selected in **ABP Studio** during the *Mobile Framework* step. Switching modes after the project is generated is not a one-line change — you would need to add the missing navigator (and its `@react-navigation/drawer` or `@react-navigation/bottom-tabs` dependency) manually, then update `src/AppContainer.tsx` and `src/navigators/types.ts` to match. Pick the mode upfront when possible.
### Bottom Tab Navigation (default)
The root navigator is `BottomTabNavigator` (`src/navigators/BottomTabNavigator.tsx`) with three stacks:
- **HomeTab** → `HomeNavigator` → `HomeScreen` (hero greeting + feature cards).
- **SettingsTab** → `SettingsNavigator` → `SettingsScreen` (language, theme, profile/password shortcuts).
- **AccountTab** → `AccountNavigator` — *conditional stack* based on the authentication state read from Redux:
- **Authenticated:** `AccountScreen` → `ChangePasswordScreen`, `ProfilePictureScreen`.
- **Guest:** `LoginScreen` → `RegisterScreen`, `ForgotPasswordScreen`, `ResetPasswordScreen`.
Tab bar colors (active/inactive tint, background, border) are sourced from the `useThemeColors` hook so the bar follows the active light/dark theme.
#### The Account Screen
`AccountScreen` (`src/screens/Account/AccountScreen.tsx`) is the home of the AccountTab when the user is signed in. Its layout follows an iOS-style grouped pattern:
1. **Profile header** — circular avatar (profile picture or first-letter fallback), full name and email, centered at the top.
2. **Account actions card** — a single rounded card containing two rows with leading icon chips:
- **Profile Picture** → navigates to `ProfilePictureScreen`.
- **Change Password** → navigates to `ChangePasswordScreen`.
3. **Destructive logout button** — an outlined `destructive`-colored button that calls the `useLogout` hook.
### Drawer Navigation (alternative)
When the drawer mode is selected, `DrawerNavigator` (`src/navigators/DrawerNavigator.tsx`) replaces the bottom tabs. It exposes two drawer items:
- **HomeStack** → `HomeNavigator` → `HomeScreen`, plus the auth flow (`LoginScreen`, `RegisterScreen`, `ForgotPasswordScreen`, `ResetPasswordScreen`).
- **SettingsStack** → `SettingsNavigator` → `SettingsScreen`, `ChangePasswordScreen`, `ProfilePictureScreen`.
Note that there is **no `AccountTab` / `AccountScreen` in drawer mode** — auth lives in the Home stack and profile/password actions live in the Settings stack. The drawer side panel itself is fully custom.
#### The Drawer Content
`DrawerContent` (`src/components/DrawerContent/DrawerContent.tsx`) is the custom side panel rendered by `DrawerNavigator` via the `drawerContent` prop. From top to bottom:
1. **User header** — circular avatar (image or first-letter fallback) + full name + email when authenticated.
2. **Divider**.
3. **Navigation items** — Home and Settings rows with leading Ionicons; tapping navigates and closes the drawer.
4. **Auth row** — when authenticated, a **Logout** row that calls `useLogout`; when guest, a **Login** row that navigates to the Login screen inside `HomeStack`.
The whole panel uses NativeWind classes with `dark:` variants, so it follows the active theme automatically.
### Adding a New Screen
To add a screen to either navigation mode:
1. Create the screen component under `src/screens/<FeatureName>/<FeatureName>Screen.tsx` and export it from `src/screens/index.ts`.
2. Register it as a `Stack.Screen` inside the appropriate navigator (e.g. `HomeNavigator`, `SettingsNavigator`, or `AccountNavigator`).
3. Add the route to the matching `*ParamList` in `src/navigators/types.ts` so the screen props stay typed.
If the new screen needs to appear at the *root* level (a new tab or drawer item rather than a child of an existing stack), edit `BottomTabNavigator.tsx` or `DrawerNavigator.tsx` and update the corresponding `BottomTabParamList` / `RootDrawerParamList` type.
## How to Configure & Run the Backend (Required for Emulator/Simulator Testing)
> React Native application does not trust the auto-generated .NET HTTPS certificate. You should use **HTTP** during the development.

159
docs/en/framework/ui/react-native/styling-with-nativewind.md

@ -0,0 +1,159 @@
```json
//[doc-seo]
{
"Description": "Learn how the ABP React Native startup template is styled with NativeWind v4 (Tailwind CSS for React Native), including the semantic color tokens, dark mode and theme customization."
}
```
# Styling with NativeWind
The ABP React Native startup template uses **[NativeWind v4](https://www.nativewind.dev/)** — Tailwind CSS for React Native. Most components are styled through utility `className` props that are compiled at build time, so there is almost no styling runtime cost. The design system is based on the **shadcn/ui** neutral palette and supports **light and dark themes** out of the box.
> This page documents the styling system that ships with the template. For the general getting-started flow (installing tools, running the app, configuring the backend), see [Getting Started with React Native](index.md).
<img width="360" src="../../../images/rn-home-screen.png" alt="HomeScreen built with NativeWind" />
---
## 1. Project Files
NativeWind is wired in through a small set of configuration files at the root of the React Native project:
| File | Purpose |
|------|---------|
| `tailwind.config.js` | Defines the design tokens (colors, spacing, border radius), enables `darkMode: 'class'`, and registers the NativeWind preset. This is the source of truth for the theme. |
| `global.css` | Tailwind entry point with the three base directives (`@tailwind base; @tailwind components; @tailwind utilities;`). |
| `metro.config.js` | Wraps the default Expo Metro config with `withNativeWind(...)` and points it at `global.css`. |
| `babel.config.js` | Adds the `nativewind/babel` preset and sets `jsxImportSource: 'nativewind'` on `babel-preset-expo` so JSX understands the `className` prop. |
| `nativewind-env.d.ts` | TypeScript triple-slash reference (`/// <reference types="nativewind/types" />`) that adds `className` typings to React Native components. |
All of these files come pre-configured in the template. You normally only need to edit `tailwind.config.js` to customize the theme.
---
## 2. Semantic Color Tokens
Instead of raw color names like `bg-zinc-900`, the template exposes a small set of **semantic tokens** that follow the shadcn/ui convention. Each token has a default (light) value and a `dark` variant, and many also carry a paired `foreground` color for text/icons placed on top.
| Token | Use it for |
|-------|-----------|
| `background` / `foreground` | Screen background and primary text. |
| `card` / `card-border` | Surface containers (settings rows, feature cards) and their 1px border. |
| `primary` / `primary-foreground` | Filled call-to-action buttons. |
| `secondary` / `secondary-foreground` | Subtle filled elements such as icon chips inside cards. |
| `muted` / `muted-foreground` | Backgrounds and text for secondary information (subtitles, helper text). |
| `accent` / `accent-foreground` | Highlighted/active state — also used as the active tint of the bottom tab bar. |
| `destructive` / `destructive-foreground` | Delete/danger actions and error states. |
| `border` | Generic separators and outline borders. |
| `input` | Border color for text inputs (Paper `TextInput` outline). |
| `ring` | Focus ring/outline color. |
All token values live in `tailwind.config.js` under `theme.extend.colors`. Refer to that file for the exact hex values.
The template also extends Tailwind's spacing and border-radius scales so paddings and corners stay consistent across screens:
* **Spacing:** `xs` (4px), `sm` (8px), `md` (16px), `lg` (24px), `xl` (32px) — usable as `p-md`, `mt-lg`, `gap-sm`, etc.
* **Border radius:** `rounded-sm` (4px), `rounded-md` (8px), `rounded-lg` (12px), `rounded-xl` (16px), `rounded-2xl` (20px).
---
## 3. Dark Mode
Dark mode is enabled with `darkMode: 'class'` in `tailwind.config.js` and is driven by the device color scheme via NativeWind's `useColorScheme()` hook. Each color token has a sibling `*-dark` (and where applicable `*-dark-foreground`) variant, and components opt into it with the `dark:` prefix on their classes:
```tsx
<View className="flex-1 bg-background dark:bg-background-dark">
<Text className="text-foreground dark:text-foreground-dark">
Hello
</Text>
</View>
```
> **Pattern note.** The token shape used in this template is `{ DEFAULT, dark }` (and `{ foreground, 'dark-foreground' }` where applicable), so the dark-mode class is `dark:bg-background-dark` — not `dark:bg-background`. Follow this convention when you add new screens so the theme switch behaves consistently across the app.
The user can also switch themes manually from the **Settings** screen (system / light / dark), and the choice is persisted via Redux.
<img width="360" src="../../../images/rn-home-screen.png" alt="HomeScreen with light and dark theme" />
---
## 4. The `useThemeColors` Hook
A handful of APIs in the template do not accept a `className` — they need a *color value*. Examples:
* `react-native-paper`'s `TextInput` (which the template still uses for outlined inputs and validation styling).
* React Navigation's `screenOptions` (header background, tab bar tint, etc.).
* The native status bar.
For these cases the template ships a `useThemeColors` hook at `src/hooks/UseThemeColors.ts`. It returns theme-aware values that mirror the Tailwind tokens:
```ts
const {
primaryContainer, // Paper TextInput surface
headerBg, // Navigator headers
headerText,
iconColor, // Inactive icon tint
accentColor, // Active icon tint / focused tab
destructiveColor,
inputBorderColor,
} = useThemeColors();
```
**Rule of thumb:** prefer `className` with `dark:` variants whenever the component supports it; reach for `useThemeColors` only for the components listed above.
---
## 5. React Native Paper
`react-native-paper` is still in the template's `package.json`, but its usage has been narrowed down to a single component: **`TextInput`** in outlined mode (used for forms because of its strong validation/error UI). Buttons, lists, modals, drawers, and icons are all NativeWind + `@expo/vector-icons` (Ionicons) now.
If you add new forms, follow the same split:
* Use Paper's `TextInput` (with values from `useThemeColors`) for text fields.
* Use plain `View`/`Text`/`Pressable` with NativeWind classes for everything else.
---
## 6. Customizing the Theme
Most customization happens in `tailwind.config.js`. To introduce a brand color, extend the `colors` map and reuse the `{ DEFAULT, dark }` shape so the `dark:` variants keep working:
```js
// tailwind.config.js
module.exports = {
// ...
theme: {
extend: {
colors: {
// ...
brand: {
DEFAULT: '#2563eb',
dark: '#3b82f6',
foreground: '#ffffff',
'dark-foreground': '#ffffff',
},
},
},
},
};
```
You can then use it from any component:
```tsx
<Pressable className="bg-brand dark:bg-brand-dark rounded-lg px-md py-sm">
<Text className="text-brand-foreground dark:text-brand-dark-foreground font-semibold">
Get started
</Text>
</Pressable>
```
If the new color also needs to be reachable from non-`className` APIs (Paper, navigators, status bar), add a matching entry to `useThemeColors` so the rest of the template stays consistent.
---
## 7. Going Further
* [NativeWind documentation](https://www.nativewind.dev/) — class reference, advanced features (variants, animations).
* [Tailwind CSS documentation](https://tailwindcss.com/docs) — utility classes, responsive design, theming.
* [shadcn/ui](https://ui.shadcn.com/) — the design system the color tokens are inspired by.

BIN
docs/en/get-started/images/abp-studio-mobile-sample.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
docs/en/images/rn-home-screen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/en/images/rn-login-iphone.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/en/images/rn-nav-comparison.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

12
docs/en/modules/openiddict.md

@ -303,6 +303,18 @@ PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
- `UpdateAbpClaimTypes(default: true)`: Updates `AbpClaimTypes` to be compatible with the Openiddict claims.
- `AddDevelopmentEncryptionAndSigningCertificate(default: true)`: Registers (and generates if necessary) a user-specific development encryption/development signing certificate. This is a certificate used for signing and encrypting the tokens and for **development environment only**. You must set it to **false** for non-development environments.
- `UseDefaultScopesForClientCredentials(default: false)`: When set to `true`, the access token issued for the `client_credentials` grant automatically grants the scopes configured on the client application (permissions prefixed with `oi_scp:`) when the client does not explicitly request any scope.
- `UseDefaultScopesForPassword(default: false)`: When set to `true`, the token response for the `password` grant automatically grants the scopes configured on the client application when the client does not explicitly request any scope. If the configured scopes include `openid`/`profile`/`email`/`roles`, the corresponding `id_token` and claim destinations are affected as well.
- `UseDefaultScopesForTokenExchange(default: false)`: When set to `true`, the token response for the `urn:ietf:params:oauth:grant-type:token-exchange` grant automatically grants the scopes configured on the client application when the client does not explicitly request any scope. If the configured scopes include `openid`/`profile`/`email`/`roles`, the corresponding `id_token` and claim destinations are affected as well.
Example to enable the default-scope fallback for the `client_credentials` grant:
```csharp
PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
{
options.UseDefaultScopesForClientCredentials = true;
});
```
> `AddDevelopmentEncryptionAndSigningCertificate` cannot be used in applications deployed on IIS or Azure App Service: trying to use them on IIS or Azure App Service will result in an exception being thrown at runtime (unless the application pool is configured to load a user profile). To avoid that, consider creating self-signed certificates and storing them in the X.509 certificates store of the host machine(s). Please refer to: https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html#registering-a-development-certificate

12
docs/en/solution-templates/layered-web-application/mobile-applications.md

@ -100,6 +100,8 @@ You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/
This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option.
The UI is built with **[NativeWind v4](https://www.nativewind.dev/)** (Tailwind CSS for React Native) on top of a shadcn-inspired neutral palette, with full **light/dark mode** support. See [Styling with NativeWind](../../framework/ui/react-native/styling-with-nativewind.md) for the styling system reference.
#### Project Structure
- **Environment.ts**: file using for provide application level variables like `apiUrl`, `oAuthConfig` and etc.
@ -110,16 +112,20 @@ This is the mobile application that is built based on Facebook's [React Native f
- **contexts**: `contexts` folder contains [react context](https://react.dev/reference/react/createContext). You can expots your contexts in this folder. `Localization context provided in here`
- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After create new *FeatureName*Navigator we need to provide in `DrawerNavigator.tsx` file as `Drawer.Screen`
- **hooks**: custom [React hooks](https://react.dev/reference/react/hooks) for shared UI and app logic. For example, `useThemeColors` returns light/dark palette values when a component needs explicit colors instead of NativeWind `className`, and `useLogout` handles signing out—plus other hooks bundled with the template.
- **navigators**: folder contains [React Navigation](https://reactnavigation.org/) stacks. The template includes `BottomTabNavigator`, `DrawerNavigator`, `HomeNavigator`, `SettingsNavigator` and `AccountNavigator`. After creating a new *FeatureName*Navigator, register it in the appropriate parent navigator (e.g. `DrawerNavigator.tsx` or `BottomTabNavigator.tsx`).
- **screens**: is the content of navigated page. We'll pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/)
- **screens**: contains the content of each navigated page. The template ships with screens for `Home`, `Account`, `Settings`, `Login`, `Register`, `ForgotPassword`, `ResetPassword`, `ChangePassword` and `ProfilePicture`. Each screen is wired to a navigator as a [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) component.
- **store**: folder manages state-management operations. We will define `actions`, `listeners`, `reducers`, and `selectors` here.
- **styles**: folder contains app styles. `system-style.ts` comes built in template we can also add new styles.
- **theme**: folder exposes Paper-only theme colors used by `react-native-paper`'s `TextInput`. Most theming now lives in `tailwind.config.js` (see below).
- **utils**: folder contains helper functions that we can use in application
In addition to `src/`, the project root hosts the NativeWind setup. `tailwind.config.js` defines design tokens, the color palette, and dark mode. `global.css` contains Tailwind's layer directives. `metro.config.js` and `babel.config.js` configure Metro and Babel so NativeWind can transform your styles. `nativewind-env.d.ts` adds TypeScript typings for the `className` prop on components.
#### Running the Application
React Native applications can't be run with the solution runner. You need to run them with the React Native CLI. You can check the [React Native documentation](https://reactnative.dev/docs/environment-setup) to learn how to setup the environment for React Native development.

12
docs/en/solution-templates/microservice/mobile-applications.md

@ -145,6 +145,8 @@ You can follow [Mobile Application Development Tutorial - MAUI](../../tutorials/
This is the mobile application that is built based on Facebook's [React Native framework](https://reactnative.dev/) and [Expo](https://expo.dev/). It will be in the solution only if you've selected React Native as your mobile application option.
The UI is built with **[NativeWind v4](https://www.nativewind.dev/)** (Tailwind CSS for React Native) on top of a shadcn-inspired neutral palette, with full **light/dark mode** support. See [Styling with NativeWind](../../framework/ui/react-native/styling-with-nativewind.md) for the styling system reference.
#### Project Structure
- **Environment.ts**: file using for providing application level variables like `apiUrl`, `oAuthConfig` and etc.
@ -157,20 +159,22 @@ This is the mobile application that is built based on Facebook's [React Native f
- **hocs**: this folder is added to contain higher order components. The purpose is to wrap components with additional features or properties. It initially has a `PermissionHoc.tsx` that wraps a component to check the permission grant status.
- **hooks**: covers the react native hooks where you can get a reference from [the official documentation](https://react.dev/reference/react/hooks).
- **hooks**: Custom [React hooks](https://react.dev/reference/react/hooks) for shared UI and app logic. For example, `useThemeColors` returns light/dark palette values when a component needs explicit colors instead of NativeWind `className`, and `useLogout` handles signing out—plus other hooks bundled with the template.
- **interceptors**: initializes a file called `APIInterceptor.ts` that has a function to manage the http operations in a better way.
- **navigators**: folder contains [react-native stacks](https://reactnavigation.org/docs/stack-navigator/). After creating a new *FeatureName*Navigator we need to provide in `DrawerNavigator.ts` file as `Drawer.Screen`
- **navigators**: folder contains [React Navigation](https://reactnavigation.org/) stacks. The template includes `BottomTabNavigator`, `DrawerNavigator`, `HomeNavigator`, `SettingsNavigator` and `AccountNavigator`. After creating a new *FeatureName*Navigator, register it in the appropriate parent navigator (e.g. `DrawerNavigator.tsx` or `BottomTabNavigator.tsx`).
- **screens**: folder has the content of navigated page. We will pass as component property to [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/)
- **screens**: contains the content of each navigated page. The template ships with screens for `Home`, `Account`, `Settings`, `Login`, `Register`, `ForgotPassword`, `ResetPassword`, `ChangePassword` and `ProfilePicture`. Each screen is wired to a navigator as a [Stack.Screen](https://reactnavigation.org/docs/native-stack-navigator/) component.
- **store**: folder manages state-management operations. We will define `actions`, `listeners`, `reducers`, and `selectors` here.
- **styles**: folder contains app styles. `system-style.ts` comes built in template we can also add new styles.
- **theme**: folder exposes Paper-only theme colors used by `react-native-paper`'s `TextInput`. Most theming now lives in `tailwind.config.js` (see below).
- **utils**: folder contains helper functions that we can use in application
In addition to `src/`, the project root hosts the NativeWind setup. `tailwind.config.js` defines design tokens, the color palette, and dark mode. `global.css` contains Tailwind's layer directives. `metro.config.js` and `babel.config.js` configure Metro and Babel so NativeWind can transform your styles. `nativewind-env.d.ts` adds TypeScript typings for the `className` prop on components.
#### Running the Application
React Native applications can't be run with the solution runner. You need to run them with the React Native CLI. You can check the [React Native documentation](https://reactnative.dev/docs/environment-setup) to learn how to setup the environment for React Native development.

6
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs

@ -13,7 +13,11 @@ using Volo.Abp.Hangfire;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
[Dependency(ReplaceServices = true)]
public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
public class HangfireDynamicBackgroundWorkerManager :
IDynamicBackgroundWorkerManager,
ISupportsRuntimeRegistration,
ISupportsCronScheduling,
ISingletonDependency
{
protected IServiceProvider ServiceProvider { get; }
protected IDynamicBackgroundWorkerHandlerRegistry HandlerRegistry { get; }

6
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs

@ -10,7 +10,11 @@ using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundWorkers.Quartz;
[Dependency(ReplaceServices = true)]
public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
public class QuartzDynamicBackgroundWorkerManager :
IDynamicBackgroundWorkerManager,
ISupportsRuntimeRegistration,
ISupportsCronScheduling,
ISingletonDependency
{
public const string DynamicWorkerNameKey = "AbpDynamicWorkerName";

17
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/DefaultDynamicBackgroundWorkerManager.cs

@ -10,7 +10,10 @@ using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers;
public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerManager, ISingletonDependency
public class DefaultDynamicBackgroundWorkerManager :
IDynamicBackgroundWorkerManager,
ISupportsRuntimeRegistration,
ISingletonDependency
{
protected IServiceProvider ServiceProvider { get; }
public ILogger<DefaultDynamicBackgroundWorkerManager> Logger { get; set; }
@ -39,11 +42,11 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan
schedule.Validate();
if (schedule.Period == null)
if (!schedule.CronExpression.IsNullOrWhiteSpace())
{
throw new AbpException(
$"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " +
"Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ).");
$"The default in-memory background worker manager does not support CronExpression for dynamic worker '{workerName}'. " +
"Please clear CronExpression and use Period-based scheduling, or use a scheduler-backed provider (Hangfire or Quartz).");
}
await _semaphore.WaitAsync(cancellationToken);
@ -102,11 +105,11 @@ public class DefaultDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan
schedule.Validate();
if (schedule.Period == null)
if (!schedule.CronExpression.IsNullOrWhiteSpace())
{
throw new AbpException(
$"The default in-memory background worker manager does not support CronExpression without Period for dynamic worker '{workerName}'. " +
"Please set Period, or use a scheduler-backed provider (Hangfire, Quartz, TickerQ).");
$"The default in-memory background worker manager does not support CronExpression for dynamic worker '{workerName}'. " +
"Please clear CronExpression and use Period-based scheduling, or use a scheduler-backed provider (Hangfire or Quartz).");
}
await _semaphore.WaitAsync(cancellationToken);

6
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/IDynamicBackgroundWorkerManager.cs

@ -6,6 +6,12 @@ namespace Volo.Abp.BackgroundWorkers;
/// <summary>
/// Manages dynamic background workers that are registered at runtime
/// without requiring a strongly-typed worker class.
/// <para>
/// Implementations may differ in capabilities. Check <see cref="ISupportsRuntimeRegistration"/>
/// before calling <see cref="AddAsync"/> / <see cref="RemoveAsync"/> / <see cref="UpdateScheduleAsync"/>,
/// and <see cref="ISupportsCronScheduling"/> before passing
/// <see cref="DynamicBackgroundWorkerSchedule.CronExpression"/>.
/// </para>
/// </summary>
public interface IDynamicBackgroundWorkerManager
{

8
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/ISupportsCronScheduling.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.BackgroundWorkers;
/// <summary>
/// Marks a dynamic background worker manager that supports cron-based scheduling.
/// </summary>
public interface ISupportsCronScheduling
{
}

8
framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/ISupportsRuntimeRegistration.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.BackgroundWorkers;
/// <summary>
/// Marks a dynamic background worker manager that supports registering workers at runtime.
/// </summary>
public interface ISupportsRuntimeRegistration
{
}

42
framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/ar.json

@ -1,31 +1,31 @@
{
"culture": "ar",
"texts": {
"InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!",
"ValidationErrorMessage": "طلبك غير صحيح!",
"ValidationNarrativeErrorMessageTitle": "تم الكشف عن الأخطاء التالية أثناء التحقق .",
"DefaultErrorMessage": "حدث خطأ!",
"DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ بواسطة الخادم.",
"DefaultErrorMessage401": "أنت غير مصدق!",
"DefaultErrorMessage401Detail": "يجب عليك تسجيل الدخول لأداء هذه العملية.",
"DefaultErrorMessage403": "أنك غير مخول!",
"DefaultErrorMessage403Detail": "لا يسمح لك بإجراء هذه العملية!",
"DefaultErrorMessage404": "المورد غير موجود!",
"DefaultErrorMessage404Detail": "لم يتم العثور على المورد المطلوب على الخادم!",
"EntityNotFoundErrorMessage": "لا يوجد كيان {0} بالمعرف = {1}!",
"EntityNotFoundErrorMessageWithoutId": "لا يوجد كيان {0}!",
"AbpDbConcurrencyErrorMessage": "تم تغيير البيانات التي قدمتها بالفعل من قبل مستخدم/عميل آخر. يرجى تجاهل التغييرات التي قمت بها والمحاولة من البداية.",
"InternalServerErrorMessage": "حدث خطأ داخلي أثناء تنفيذ طلبك.",
"ValidationErrorMessage": "البيانات المُدخلة غير صحيحة.",
"ValidationNarrativeErrorMessageTitle": "تم العثور على الأخطاء التالية أثناء التحقق:",
"DefaultErrorMessage": "حدث خطأ.",
"DefaultErrorMessageDetail": "لم يتم إرسال تفاصيل الخطأ من الخادم.",
"DefaultErrorMessage401": "لم يتم تسجيل الدخول.",
"DefaultErrorMessage401Detail": "يجب تسجيل الدخول لإتمام هذه العملية.",
"DefaultErrorMessage403": "ليس لديك صلاحية.",
"DefaultErrorMessage403Detail": "غير مسموح لك بتنفيذ هذه العملية.",
"DefaultErrorMessage404": "المورد غير موجود.",
"DefaultErrorMessage404Detail": "تعذر العثور على المورد المطلوب.",
"EntityNotFoundErrorMessage": "لا يوجد {0} بالمعرف = {1}.",
"EntityNotFoundErrorMessageWithoutId": "العنصر {0} غير موجود.",
"AbpDbConcurrencyErrorMessage": "تم تغيير البيانات التي أرسلتها بواسطة مستخدم آخر. يرجى تجاهل التغييرات التي أجريتها والمحاولة مرة أخرى.",
"Error": "خطأ",
"UnhandledException": "استثناء غير معالج!",
"Authorizing": "التحقق من الصلاحية…",
"UnhandledException": "حدث استثناء غير متوقع.",
"Authorizing": "جارٍ التحقق من الصلاحيات…",
"401Message": "غير مصرح",
"403Message": "ممنوع",
"404Message": "الصفحة غير موجودة",
"500Message": "خطأ في الخادم الداخلي",
"403MessageDetail": "أنت غير مصرح لك لإجراء هذه العملية!",
"404MessageDetail": "عذرا ، لا يوجد شيء في هذا العنوان.",
"500Message": "خطأ داخلي في الخادم",
"403MessageDetail": "ليس لديك صلاحية لتنفيذ هذه العملية.",
"404MessageDetail": "عذرًا، الصفحة المطلوبة غير موجودة.",
"Unauthorized": "غير مصرح",
"invalid_token": "الرمز غير صالح",
"SessionExpired": "انتهت جلستك. يرجى تسجيل الدخول مرة أخرى لمتابعة التطبيق."
"invalid_token": "الرمز المميز غير صالح",
"SessionExpired": "انتهت الجلسة. يرجى تسجيل الدخول مرة أخرى للمتابعة."
}
}

51
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundWorkers/DynamicBackgroundWorkerManager_Tests.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.BackgroundWorkers;
using Xunit;
@ -20,6 +21,13 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
_dynamicWorkerManager = GetRequiredService<IDynamicBackgroundWorkerManager>();
}
[Fact]
public void Should_Report_Provider_Capabilities_Using_Marker_Interfaces()
{
(_dynamicWorkerManager is ISupportsRuntimeRegistration).ShouldBeTrue();
(_dynamicWorkerManager is ISupportsCronScheduling).ShouldBeFalse();
}
[Fact]
public async Task Should_Register_Dynamic_Worker()
{
@ -235,6 +243,49 @@ public class DynamicBackgroundWorkerManager_Tests : BackgroundJobsTestBase
});
}
[Fact]
public async Task Should_Throw_When_CronExpression_Is_Set()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await Assert.ThrowsAsync<AbpException>(async () =>
{
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = 1000,
CronExpression = "0 */5 * * * *"
},
(_, _) => Task.CompletedTask
);
});
}
[Fact]
public async Task Should_Throw_When_CronExpression_Is_Set_On_UpdateSchedule()
{
var workerName = "dynamic-worker-" + Guid.NewGuid();
await _dynamicWorkerManager.AddAsync(
workerName,
new DynamicBackgroundWorkerSchedule { Period = 1000 },
(_, _) => Task.CompletedTask
);
await Assert.ThrowsAsync<AbpException>(async () =>
{
await _dynamicWorkerManager.UpdateScheduleAsync(
workerName,
new DynamicBackgroundWorkerSchedule
{
Period = 1000,
CronExpression = "0 */5 * * * *"
}
);
});
}
[Fact]
public async Task Should_Continue_Running_After_Handler_Throws_Exception()
{

9
latest-versions.json

@ -1,4 +1,13 @@
[
{
"version": "10.4.0",
"releaseDate": "",
"type": "stable",
"message": "",
"leptonx": {
"version": "5.4.0"
}
},
{
"version": "10.3.0",
"releaseDate": "",

4
modules/openiddict/app/OpenIddict.Demo.Server/OpenIddict.Demo.Server.csproj

@ -68,6 +68,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

7
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs

@ -25,9 +25,16 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule
Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
{
options.ClaimsPrincipalHandlers.Add<AbpDefaultScopesHandler>();
options.ClaimsPrincipalHandlers.Add<AbpDefaultOpenIddictClaimsPrincipalHandler>();
});
var preActions = context.Services.GetPreConfigureActions<AbpOpenIddictAspNetCoreOptions>();
Configure<AbpOpenIddictAspNetCoreOptions>(options =>
{
preActions.Configure(options);
});
Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationFormats.Add("/Volo/Abp/OpenIddict/Views/{1}/{0}.cshtml");

29
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictOptions.cs

@ -25,4 +25,33 @@ public class AbpOpenIddictAspNetCoreOptions
/// Set the url of the select account page.
/// </summary>
public string SelectAccountPage { get; set; } = "~/Account/SelectAccount";
/// <summary>
/// When set to <c>true</c>, the access token issued for the <c>client_credentials</c> grant
/// automatically includes the scopes configured on the client application (permissions
/// prefixed with <c>oi_scp:</c>) when the client does not explicitly request any scope.
/// Default: false.
/// </summary>
public bool UseDefaultScopesForClientCredentials { get; set; }
/// <summary>
/// When set to <c>true</c>, the token response for the <c>password</c> grant automatically
/// grants the scopes configured on the client application (permissions prefixed with
/// <c>oi_scp:</c>) when the client does not explicitly request any scope. If the configured
/// scopes include <c>openid</c>/<c>profile</c>/<c>email</c>/<c>roles</c>, the corresponding
/// id_token and claim destinations are affected as well.
/// Default: false.
/// </summary>
public bool UseDefaultScopesForPassword { get; set; }
/// <summary>
/// When set to <c>true</c>, the token response for the
/// <c>urn:ietf:params:oauth:grant-type:token-exchange</c> grant automatically grants the
/// scopes configured on the client application (permissions prefixed with <c>oi_scp:</c>)
/// when the client does not explicitly request any scope. If the configured scopes include
/// <c>openid</c>/<c>profile</c>/<c>email</c>/<c>roles</c>, the corresponding id_token and
/// claim destinations are affected as well.
/// Default: false.
/// </summary>
public bool UseDefaultScopesForTokenExchange { get; set; }
}

92
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Claims/AbpDefaultScopesHandler.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.OpenIddict;
public class AbpDefaultScopesHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
{
public ILogger<AbpDefaultScopesHandler> Logger { get; set; }
= NullLogger<AbpDefaultScopesHandler>.Instance;
public virtual async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
{
var options = context.ScopeServiceProvider
.GetRequiredService<IOptions<AbpOpenIddictAspNetCoreOptions>>().Value;
var request = context.OpenIddictRequest;
if (!IsDefaultScopesEnabled(request, options))
{
return;
}
if (!context.Principal.GetScopes().IsDefaultOrEmpty)
{
return;
}
var clientId = request.ClientId;
if (string.IsNullOrEmpty(clientId))
{
return;
}
var applicationManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
var scopeManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictScopeManager>();
var application = await applicationManager.FindByClientIdAsync(clientId);
if (application == null)
{
return;
}
var permissions = await applicationManager.GetPermissionsAsync(application);
var prefix = OpenIddictConstants.Permissions.Prefixes.Scope;
var scopes = permissions
.Where(p => p.StartsWith(prefix, StringComparison.Ordinal))
.Select(p => p[prefix.Length..])
.ToImmutableArray();
if (scopes.IsDefaultOrEmpty)
{
return;
}
Logger.LogDebug(
"Injecting default scopes for client {ClientId} (grant_type {GrantType}): {Scopes}",
clientId,
request.GrantType,
string.Join(", ", scopes));
context.Principal.SetScopes(scopes);
context.Principal.SetResources(await scopeManager.ListResourcesAsync(scopes).ToListAsync());
}
protected virtual bool IsDefaultScopesEnabled(OpenIddictRequest request, AbpOpenIddictAspNetCoreOptions options)
{
if (request.IsClientCredentialsGrantType())
{
return options.UseDefaultScopesForClientCredentials;
}
if (request.IsPasswordGrantType())
{
return options.UseDefaultScopesForPassword;
}
if (request.IsTokenExchangeGrantType())
{
return options.UseDefaultScopesForTokenExchange;
}
return false;
}
}
Loading…
Cancel
Save