Browse Source

Merge branch 'dev' into Blazorise2.0

pull/24906/head
Ma Liming 1 month ago
committed by GitHub
parent
commit
8eee86e0a0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      Directory.Packages.props
  2. 82
      docs/en/Blog-Posts/2026-02-23 v10_1_Release_Stable/POST.md
  3. BIN
      docs/en/Blog-Posts/2026-02-23 v10_1_Release_Stable/cover-image.png
  4. BIN
      docs/en/Blog-Posts/2026-02-23 v10_1_Release_Stable/upgrade-abp-packages.png
  5. 4
      docs/en/docs-nav.json
  6. 28
      docs/en/framework/infrastructure/background-jobs/tickerq.md
  7. 23
      docs/en/framework/infrastructure/background-workers/tickerq.md
  8. 162
      docs/en/framework/ui/angular/hybrid-localization.md
  9. 5
      docs/en/package-version-changes.md
  10. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.cs
  11. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpTenantClientProxy.cs
  12. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientCommonModule.cs
  13. 4
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs
  14. 24
      framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs
  15. 38
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/ApplicationInitializationContextExtensions.cs
  16. 5
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs
  17. 2
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo.Abp.BackgroundJobs.TickerQ.csproj
  18. 2
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs
  19. 5
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs
  20. 13
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpTickerQBackgroundJobManager.cs
  21. 2
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.TickerQ.csproj
  22. 6
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs
  23. 2
      framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQPeriodicBackgroundWorkerInvoker.cs
  24. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs
  25. 4
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  26. 11
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs
  27. 7
      framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenServiceCollectionExtensions.cs
  28. 7
      framework/src/Volo.Abp.TickerQ/Microsoft/Extensions/Hosting/AbpTickerQApplicationBuilderExtensions.cs
  29. 2
      framework/src/Volo.Abp.TickerQ/Volo.Abp.TickerQ.csproj
  30. 5
      framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQModule.cs
  31. 91
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs
  32. 84
      framework/test/Volo.Abp.ObjectMapping.Tests/Volo/Abp/ObjectMapping/ObjectMappingHelper_Tests.cs
  33. 2
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/CleanupJobs.cs
  34. 29
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs
  35. 2
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

8
Directory.Packages.props

@ -183,10 +183,10 @@
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="TencentCloudSDK.Sms" Version="3.0.1273" />
<PackageVersion Include="TimeZoneConverter" Version="7.2.0" />
<PackageVersion Include="TickerQ" Version="2.5.3" />
<PackageVersion Include="TickerQ.Dashboard" Version="2.5.3" />
<PackageVersion Include="TickerQ.Utilities" Version="2.5.3" />
<PackageVersion Include="TickerQ.EntityFrameworkCore" Version="2.5.3" />
<PackageVersion Include="TickerQ" Version="10.1.1" />
<PackageVersion Include="TickerQ.Dashboard" Version="10.1.1" />
<PackageVersion Include="TickerQ.Utilities" Version="10.1.1" />
<PackageVersion Include="TickerQ.EntityFrameworkCore" Version="10.1.1" />
<PackageVersion Include="Unidecode.NET" Version="2.1.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.9.3" />

82
docs/en/Blog-Posts/2026-02-23 v10_1_Release_Stable/POST.md

@ -0,0 +1,82 @@
# ABP.IO Platform 10.1 Final Has Been Released!
We are glad to announce that [ABP](https://abp.io/) 10.1 stable version has been released.
## What's New With Version 10.1?
All the new features were explained in detail in the [10.1 RC Announcement Post](https://abp.io/community/announcements/announcing-abp-10-1-release-candidate-cyqui19d), so there is no need to review them again. You can check it out for more details.
## Getting Started with 10.1
### 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 a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.0 or earlier versions: [ABP Version 10.1 Migration Guide](https://abp.io/docs/latest/release-info/migration-guides/abp-10-1)
## Community News
### New ABP Community Articles
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:
* [Enis Necipoğlu](https://abp.io/community/members/enisn):
* [ABP Framework's Hidden Magic: Things That Just Work Without You Knowing](https://abp.io/community/articles/hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt)
* [Implementing Multiple Global Query Filters with Entity Framework Core](https://abp.io/community/articles/implementing-multiple-global-query-filters-with-entity-ugnsmf6i)
* [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa):
* [.NET 11 Preview 1 Highlights: Faster Runtime, Smarter JIT, and AI-Ready Improvements](https://abp.io/community/articles/dotnet-11-preview-1-highlights-hspp3o5x)
* [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd)
* [Fahri Gedik](https://abp.io/community/members/fahrigedik):
* [Building a Multi-Agent AI System with A2A, MCP, and ADK in .NET](https://abp.io/community/articles/building-a-multiagent-ai-system-with-a2a-mcp-iefdehyx)
* [Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems](https://abp.io/community/articles/async-chain-of-persistence-pattern-wzjuy4gl)
* [Alper Ebiçoğlu](https://abp.io/community/members/alper):
* [NDC London 2026: From a Developer's Perspective and My Personal Notes about AI](https://abp.io/community/articles/ndc-london-2026-a-.net-conf-from-a-developers-perspective-07wp50yl)
* [Which Open-Source PDF Libraries Are Recently Popular? A Data-Driven Look At PDF Topic](https://abp.io/community/articles/which-opensource-pdf-libraries-are-recently-popular-a-g68q78it)
* [Engincan Veske](https://abp.io/community/members/EngincanV):
* [Stop Spam and Toxic Users in Your App with AI](https://abp.io/community/articles/stop-spam-and-toxic-users-in-your-app-with-ai-3i0xxh0y)
* [Liming Ma](https://abp.io/community/members/maliming):
* [How AI Is Changing Developers](https://abp.io/community/articles/how-ai-is-changing-developers-e8y4a85f)
* [Tarık Özdemir](https://abp.io/community/members/mtozdemir):
* [JetBrains State of Developer Ecosystem Report 2025 — Key Insights](https://abp.io/community/articles/jetbrains-state-of-developer-ecosystem-report-2025-key-z0638q5e)
* [Adnan Ali](https://abp.io/community/members/adnanaldaim):
* [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0)
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.2. 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-02-23 v10_1_Release_Stable/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

BIN
docs/en/Blog-Posts/2026-02-23 v10_1_Release_Stable/upgrade-abp-packages.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

4
docs/en/docs-nav.json

@ -1627,6 +1627,10 @@
"text": "Localization",
"path": "framework/ui/angular/localization.md"
},
{
"text": "Hybrid Localization",
"path": "framework/ui/angular/hybrid-localization.md"
},
{
"text": "Form Validation",
"path": "framework/ui/angular/form-validation.md"

28
docs/en/framework/infrastructure/background-jobs/tickerq.md

@ -38,16 +38,19 @@ public override void ConfigureServices(ServiceConfigurationContext context)
### UseAbpTickerQ
You need to call the `UseAbpTickerQ` extension method instead of `AddTickerQ` in the `OnApplicationInitialization` method of your module:
You need to call the `UseAbpTickerQ` extension method in the `OnApplicationInitialization` method of your module:
```csharp
// (default: TickerQStartMode.Immediate)
app.UseAbpTickerQ(startMode: ...);
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
// (default: TickerQStartMode.Immediate)
context.GetHost().UseAbpTickerQ(qStartMode: ...);
}
```
### AbpBackgroundJobsTickerQOptions
You can configure the `TimeTicker` properties for specific jobs. For example, you can change `Priority`, `Retries` and `RetryIntervals` properties as shown below:
You can configure the `TimeTickerEntity` properties for specific jobs. For example, you can change `Priority`, `Retries` and `RetryIntervals` properties as shown below:
```csharp
Configure<AbpBackgroundJobsTickerQOptions>(options =>
@ -56,11 +59,10 @@ Configure<AbpBackgroundJobsTickerQOptions>(options =>
{
Retries = 3,
RetryIntervals = new[] {30, 60, 120}, // Retry after 30s, 60s, then 2min
Priority = TickerTaskPriority.High
Priority = TickerTaskPriority.High,
// Optional batching
//BatchParent = Guid.Parse("...."),
//BatchRunCondition = BatchRunCondition.OnSuccess
// Optional: run condition for chained jobs
//RunCondition = RunCondition.OnSuccess
});
options.AddJobConfiguration<MyBackgroundJob2>(new AbpBackgroundJobsTimeTickerConfiguration()
@ -74,7 +76,7 @@ Configure<AbpBackgroundJobsTickerQOptions>(options =>
### Add your own TickerQ Background Jobs Definitions
ABP will handle the TickerQ job definitions by `AbpTickerQFunctionProvider` service. You shouldn't use `TickerFunction` to add your own job definitions. You can inject and use the `AbpTickerQFunctionProvider` to add your own definitions and use `ITimeTickerManager<TimeTicker>` or `ICronTickerManager<CronTicker>` to manage the jobs.
ABP will handle the TickerQ job definitions by `AbpTickerQFunctionProvider` service. You shouldn't use `TickerFunction` to add your own job definitions. You can inject and use the `AbpTickerQFunctionProvider` to add your own definitions and use `ITimeTickerManager<TimeTickerEntity>` or `ICronTickerManager<CronTickerEntity>` to manage the jobs.
For example, you can add a `CleanupJobs` job definition in the `OnPreApplicationInitializationAsync` method of your module:
@ -96,7 +98,7 @@ public override Task OnPreApplicationInitializationAsync(ApplicationInitializati
abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) =>
{
var service = new CleanupJobs(); // Or get it from the serviceProvider
var request = await TickerRequestProvider.GetRequestAsync<string>(serviceProvider, tickerFunctionContext.Id, tickerFunctionContext.Type);
var request = await TickerRequestProvider.GetRequestAsync<string>(tickerFunctionContext, cancellationToken);
var genericContext = new TickerFunctionContext<string>(tickerFunctionContext, request);
await service.CleanupLogsAsync(genericContext, cancellationToken);
})));
@ -105,11 +107,11 @@ public override Task OnPreApplicationInitializationAsync(ApplicationInitializati
}
```
And then you can add a job by using the `ITimeTickerManager<TimeTicker>`:
And then you can add a job by using the `ITimeTickerManager<TimeTickerEntity>`:
```csharp
var timeTickerManager = context.ServiceProvider.GetRequiredService<ITimeTickerManager<TimeTicker>>();
await timeTickerManager.AddAsync(new TimeTicker
var timeTickerManager = context.ServiceProvider.GetRequiredService<ITimeTickerManager<TimeTickerEntity>>();
await timeTickerManager.AddAsync(new TimeTickerEntity
{
Function = nameof(CleanupJobs),
ExecutionTime = DateTime.UtcNow.AddSeconds(5),

23
docs/en/framework/infrastructure/background-workers/tickerq.md

@ -36,11 +36,14 @@ public override void ConfigureServices(ServiceConfigurationContext context)
### UseAbpTickerQ
You need to call the `UseAbpTickerQ` extension method instead of `AddTickerQ` in the `OnApplicationInitialization` method of your module:
You need to call the `UseAbpTickerQ` extension method in the `OnApplicationInitialization` method of your module:
```csharp
// (default: TickerQStartMode.Immediate)
app.UseAbpTickerQ(startMode: ...);
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
// (default: TickerQStartMode.Immediate)
context.GetHost().UseAbpTickerQ(qStartMode: ...);
}
```
### AbpBackgroundWorkersTickerQOptions
@ -61,7 +64,7 @@ Configure<AbpBackgroundWorkersTickerQOptions>(options =>
### Add your own TickerQ Background Worker Definitions
ABP will handle the TickerQ job definitions by `AbpTickerQFunctionProvider` service. You shouldn't use `TickerFunction` to add your own job definitions. You can inject and use the `AbpTickerQFunctionProvider` to add your own definitions and use `ITimeTickerManager<TimeTicker>` or `ICronTickerManager<CronTicker>` to manage the jobs.
ABP will handle the TickerQ job definitions by `AbpTickerQFunctionProvider` service. You shouldn't use `TickerFunction` to add your own job definitions. You can inject and use the `AbpTickerQFunctionProvider` to add your own definitions and use `ITimeTickerManager<TimeTickerEntity>` or `ICronTickerManager<CronTickerEntity>` to manage the jobs.
For example, you can add a `CleanupJobs` job definition in the `OnPreApplicationInitializationAsync` method of your module:
@ -83,7 +86,7 @@ public override Task OnPreApplicationInitializationAsync(ApplicationInitializati
abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) =>
{
var service = new CleanupJobs(); // Or get it from the serviceProvider
var request = await TickerRequestProvider.GetRequestAsync<string>(serviceProvider, tickerFunctionContext.Id, tickerFunctionContext.Type);
var request = await TickerRequestProvider.GetRequestAsync<string>(tickerFunctionContext, cancellationToken);
var genericContext = new TickerFunctionContext<string>(tickerFunctionContext, request);
await service.CleanupLogsAsync(genericContext, cancellationToken);
})));
@ -92,11 +95,11 @@ public override Task OnPreApplicationInitializationAsync(ApplicationInitializati
}
```
And then you can add a job by using the `ICronTickerManager<CronTicker>`:
And then you can add a job by using the `ICronTickerManager<CronTickerEntity>`:
```csharp
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTicker>>();
await cronTickerManager.AddAsync(new CronTicker
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTickerEntity>>();
await cronTickerManager.AddAsync(new CronTickerEntity
{
Function = nameof(CleanupJobs),
Expression = "0 */6 * * *", // Every 6 hours
@ -106,13 +109,13 @@ await cronTickerManager.AddAsync(new CronTicker
});
```
You can specify a cron expression instead of use `ICronTickerManager<CronTicker>` to add a worker:
You can specify a cron expression instead of using `ICronTickerManager<CronTickerEntity>` to add a worker:
```csharp
abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) =>
{
var service = new CleanupJobs();
var request = await TickerRequestProvider.GetRequestAsync<string>(serviceProvider, tickerFunctionContext.Id, tickerFunctionContext.Type);
var request = await TickerRequestProvider.GetRequestAsync<string>(tickerFunctionContext, cancellationToken);
var genericContext = new TickerFunctionContext<string>(tickerFunctionContext, request);
await service.CleanupLogsAsync(genericContext, cancellationToken);
})));

162
docs/en/framework/ui/angular/hybrid-localization.md

@ -0,0 +1,162 @@
```json
//[doc-seo]
{
"Description": "Combine backend and UI localizations in Angular: use JSON files to override or extend server-sidtexts with the same key format and abpLocalization pipe."
}
```
# Hybrid Localization
Hybrid localization lets you combine **backend localizations** (from the ABP server) with **UI localizations** (JSON files in your Angular app). UI values take priority over backend values for the same key, so you can override or extend server-side texts without changing the backend.
## How It Works
- **Backend localizations**: Loaded from the server (e.g. `ApplicationLocalizationResourceDto`). Keys use the format `ResourceName::Key`.
- **UI localizations**: Loaded from static JSON files under your app's assets (e.g. `/assets/localization/en.json`). The same key format `ResourceName::Key` is used.
- **Priority**: When a key exists in both backend and UI, the **UI value is used** (UI overrides backend).
The existing `abpLocalization` pipe and localization APIs work unchanged; they resolve keys from the merged set (backend + UI), with UI winning on conflicts.
## Configuration
Enable hybrid localization in your app config via `provideAbpCore` and `withOptions`:
```typescript
// app.config.ts
import { provideAbpCore, withOptions } from "@abp/ng.core";
export const appConfig: ApplicationConfig = {
providers: [
provideAbpCore(
withOptions({
// ...other options
uiLocalization: {
enabled: true,
basePath: "/assets/localization", // optional; default is '/assets/localization'
},
}),
),
// ...
],
};
```
| Option | Description | Default |
| ---------- | ---------------------------------------------------------------------------- | ------------------------ |
| `enabled` | Turn on UI localization loading from `{basePath}/{culture}.json`. | — |
| `basePath` | Base path for JSON files. Files are loaded from `{basePath}/{culture}.json`. | `'/assets/localization'` |
When `enabled` is `true`, the app loads a JSON file for the current language (e.g. `en`, `tr`) whenever the user changes language. Loaded data is merged with backend localizations (UI overrides backend for the same key).
## UI Localization File Format
Place one JSON file per culture under your `basePath`. File name must be `{culture}.json` (e.g. `en.json`, `tr.json`).
Structure: **resource name → key → value**.
```json
{
"MyProjectName": {
"Welcome": "Welcome from UI (en.json)",
"CustomKey": "This is a UI-only localization",
"TestMessage": "UI localization is working!"
},
"AbpAccount": {
"Login": "Sign In (UI Override)"
}
}
```
- Top-level keys are **resource names** (e.g. `MyProjectName`, `AbpAccount`).
- Nested keys are **localization keys**; values are the display strings for that culture.
In templates you keep using the same key format: `ResourceName::Key`.
## Using in Templates
Use the `abpLocalization` pipe as usual. Keys can come from backend only, UI only, or both (UI wins):
```html
<!-- Backend (if available) or UI -->
<p>{%{{ 'MyProjectName::Welcome' | abpLocalization }}%}</p>
<!-- UI-only key (from /assets/localization/{{ culture }}.json) -->
<p>{%{{ 'MyProjectName::CustomKey' | abpLocalization }}%}</p>
<!-- Backend key overridden by UI -->
<p>{%{{ 'AbpAccount::Login' | abpLocalization }}%}</p>
```
No template changes are needed; only the configuration and the JSON files.
## UILocalizationService
The `UILocalizationService` (`@abp/ng.core`) manages UI localizations and merges them with backend data.
### Get loaded UI data
To inspect what was loaded from the UI JSON files (e.g. for debugging or display):
```typescript
import { UILocalizationService, SessionStateService } from "@abp/ng.core";
export class MyComponent {
private uiLocalizationService = inject(UILocalizationService);
private sessionState = inject(SessionStateService);
currentLanguage$ = this.sessionState.getLanguage$();
ngOnInit() {
// All loaded UI resources for current language
const loaded = this.uiLocalizationService.getLoadedLocalizations();
// Or for a specific culture
const loadedEn = this.uiLocalizationService.getLoadedLocalizations("en");
}
}
```
`getLoadedLocalizations(culture?: string)` returns an object of the form `{ [resourceName: string]: Record<string, string> }` for the given culture (or current language if omitted).
### Add translations at runtime
You can also add or merge UI translations programmatically (e.g. from another source or lazy-loaded module):
```typescript
this.uiLocalizationService.addAngularLocalizeLocalization(
'en', // culture
'MyProjectName', // resource name
{ MyKey: 'My value' }, // key-value map
);
```
This merges into the existing UI localizations and is taken into account by the `abpLocalization` pipe with the same UI-over-backend priority.
## Example: Dev App
The ABP dev app demonstrates hybrid localization:
1. **Config** (`app.config.ts`):
```typescript
uiLocalization: {
enabled: true,
basePath: '/assets/localization',
},
```
2. **Files**: `src/assets/localization/en.json` and `src/assets/localization/tr.json` with the structure shown above.
3. **Component** (`localization-test.component.ts`): Uses `abpLocalization` for backend keys, UI-only keys, and overrides; and uses `UILocalizationService.getLoadedLocalizations()` to show loaded UI data.
See `apps/dev-app/src/app/localization-test/localization-test.component.ts` and `apps/dev-app/src/assets/localization/*.json` in the repository for the full example.
## Summary
| Topic | Description |
|------------------|-------------|
| **Purpose** | Combine backend and UI localizations; UI overrides backend for the same key. |
| **Config** | `provideAbpCore(withOptions({ uiLocalization: { enabled: true, basePath?: string } }))`. |
| **File location**| `{basePath}/{culture}.json` (e.g. `/assets/localization/en.json`). |
| **JSON format** | `{ "ResourceName": { "Key": "Value", ... }, ... }`. |
| **Template usage** | Same as before: `{%{{ 'ResourceName::Key' \| abpLocalization }}%}`. |
| **API** | `UILocalizationService`: `getLoadedLocalizations(culture?)`, `addAngularLocalizeLocalization(culture, resourceName, translations)`. |

5
docs/en/package-version-changes.md

@ -8,6 +8,10 @@
| Blazorise.Components | 1.8.8 | 2.0.0 | #24906 |
| Blazorise.DataGrid | 1.8.8 | 2.0.0 | #24906 |
| Blazorise.Snackbar | 1.8.8 | 2.0.0 | #24906 |
| TickerQ | 2.5.3 | 10.1.1 | #24916 |
| TickerQ.Dashboard | 2.5.3 | 10.1.1 | #24916 |
| TickerQ.EntityFrameworkCore | 2.5.3 | 10.1.1 | #24916 |
| TickerQ.Utilities | 2.5.3 | 10.1.1 | #24916 |
## 10.1.0
@ -15,4 +19,3 @@
|---------|-------------|-------------|-----|
| Microsoft.SemanticKernel | 1.67.1 | 1.71.0 | #24891 |
| Microsoft.SemanticKernel.Abstractions | 1.67.1 | 1.71.0 | #24891 |

3
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpApplicationConfigurationClientProxy.cs

@ -1,12 +1,9 @@
// This file is part of AbpApplicationConfigurationClientProxy, you can customize it here
// ReSharper disable once CheckNamespace
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies;
[RemoteService(false)]
[DisableConventionalRegistration]
public partial class AbpApplicationConfigurationClientProxy
{
}

3
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/ClientProxies/AbpTenantClientProxy.cs

@ -2,13 +2,10 @@
// ReSharper disable once CheckNamespace
using Volo.Abp;
using Volo.Abp.DependencyInjection;
namespace Pages.Abp.MultiTenancy.ClientProxies;
[RemoteService(false)]
[DisableConventionalRegistration]
public partial class AbpTenantClientProxy
{
}

5
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/AbpAspNetCoreMvcClientCommonModule.cs

@ -1,6 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Pages.Abp.MultiTenancy.ClientProxies;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ClientProxies;
using Volo.Abp.Authorization;
using Volo.Abp.Caching;
using Volo.Abp.Features;
@ -39,9 +37,6 @@ public class AbpAspNetCoreMvcClientCommonModule : AbpModule
options.GlobalContributors.Add<RemoteLocalizationContributor>();
});
context.Services.AddTransient<AbpApplicationConfigurationClientProxy>();
context.Services.AddTransient<AbpTenantClientProxy>();
var abpClaimsPrincipalFactoryOptions = context.Services.ExecutePreConfiguredActions<AbpClaimsPrincipalFactoryOptions>();
if (abpClaimsPrincipalFactoryOptions.IsRemoteRefreshEnabled)
{

4
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationScriptController.cs

@ -17,14 +17,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
[ApiExplorerSettings(IgnoreApi = true)]
public class AbpApplicationConfigurationScriptController : AbpController
{
protected readonly AbpApplicationConfigurationAppService ConfigurationAppService;
protected readonly IAbpApplicationConfigurationAppService ConfigurationAppService;
protected readonly IJsonSerializer JsonSerializer;
protected readonly AbpAspNetCoreMvcOptions Options;
protected readonly IJavascriptMinifier JavascriptMinifier;
protected readonly IAbpAntiForgeryManager AntiForgeryManager;
public AbpApplicationConfigurationScriptController(
AbpApplicationConfigurationAppService configurationAppService,
IAbpApplicationConfigurationAppService configurationAppService,
IJsonSerializer jsonSerializer,
IOptions<AbpAspNetCoreMvcOptions> options,
IJavascriptMinifier javascriptMinifier,

24
framework/src/Volo.Abp.AspNetCore/Microsoft/AspNetCore/Builder/AbpApplicationBuilderExtensions.cs

@ -38,6 +38,18 @@ public static class AbpApplicationBuilderExtensions
Check.NotNull(app, nameof(app));
app.ApplicationServices.GetRequiredService<ObjectAccessor<IApplicationBuilder>>().Value = app;
if (app is WebApplication webApplication)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<WebApplication>>().Value = webApplication;
}
if (app is IHost host)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<IHost>>().Value = host;
}
if (app is IEndpointRouteBuilder endpointRouteBuilder)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<IEndpointRouteBuilder>>().Value = endpointRouteBuilder;
}
var application = app.ApplicationServices.GetRequiredService<IAbpApplicationWithExternalServiceProvider>();
var applicationLifetime = app.ApplicationServices.GetRequiredService<IHostApplicationLifetime>();
@ -59,6 +71,18 @@ public static class AbpApplicationBuilderExtensions
Check.NotNull(app, nameof(app));
app.ApplicationServices.GetRequiredService<ObjectAccessor<IApplicationBuilder>>().Value = app;
if (app is WebApplication webApplication)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<WebApplication>>().Value = webApplication;
}
if (app is IHost host)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<IHost>>().Value = host;
}
if (app is IEndpointRouteBuilder endpointRouteBuilder)
{
app.ApplicationServices.GetRequiredService<ObjectAccessor<IEndpointRouteBuilder>>().Value = endpointRouteBuilder;
}
var application = app.ApplicationServices.GetRequiredService<IAbpApplicationWithExternalServiceProvider>();
var applicationLifetime = app.ApplicationServices.GetRequiredService<IHostApplicationLifetime>();

38
framework/src/Volo.Abp.AspNetCore/Volo/Abp/ApplicationInitializationContextExtensions.cs

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
@ -21,6 +23,42 @@ public static class ApplicationInitializationContextExtensions
return context.ServiceProvider.GetRequiredService<IObjectAccessor<IApplicationBuilder>>().Value;
}
public static IHost GetHost(this ApplicationInitializationContext context)
{
var host = context.ServiceProvider.GetRequiredService<IObjectAccessor<IHost>>().Value;
Check.NotNull(host, nameof(host));
return host;
}
public static IHost? GetHostOrNull(this ApplicationInitializationContext context)
{
return context.ServiceProvider.GetRequiredService<IObjectAccessor<IHost>>().Value;
}
public static IEndpointRouteBuilder GetEndpointRouteBuilder(this ApplicationInitializationContext context)
{
var endpointRouteBuilder = context.ServiceProvider.GetRequiredService<IObjectAccessor<IEndpointRouteBuilder>>().Value;
Check.NotNull(endpointRouteBuilder, nameof(endpointRouteBuilder));
return endpointRouteBuilder;
}
public static IEndpointRouteBuilder? GetEndpointRouteBuilderOrNull(this ApplicationInitializationContext context)
{
return context.ServiceProvider.GetRequiredService<IObjectAccessor<IEndpointRouteBuilder>>().Value;
}
public static WebApplication GetWebApplication(this ApplicationInitializationContext context)
{
var webApplication = context.ServiceProvider.GetRequiredService<IObjectAccessor<WebApplication>>().Value;
Check.NotNull(webApplication, nameof(webApplication));
return webApplication;
}
public static WebApplication? GetWebApplicationOrNull(this ApplicationInitializationContext context)
{
return context.ServiceProvider.GetRequiredService<IObjectAccessor<WebApplication>>().Value;
}
public static IWebHostEnvironment GetEnvironment(this ApplicationInitializationContext context)
{
return context.ServiceProvider.GetRequiredService<IWebHostEnvironment>();

5
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/AbpAspNetCoreModule.cs

@ -2,7 +2,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.AspNetCore.RequestLocalization;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.FileProviders;
using MyCSharp.HttpUserAgentParser.DependencyInjection;
using Volo.Abp.AspNetCore.Auditing;
@ -57,6 +59,9 @@ public class AbpAspNetCoreModule : AbpModule
AddAspNetServices(context.Services);
context.Services.AddObjectAccessor<IApplicationBuilder>();
context.Services.AddObjectAccessor<WebApplication>();
context.Services.AddObjectAccessor<IHost>();
context.Services.AddObjectAccessor<IEndpointRouteBuilder>();
context.Services.AddAbpDynamicOptions<RequestLocalizationOptions, AbpRequestLocalizationOptionsManager>();
StaticWebAssetsLoader.UseStaticWebAssets(context.Services.GetHostingEnvironment(), context.Services.GetConfiguration());

2
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo.Abp.BackgroundJobs.TickerQ.csproj

@ -4,7 +4,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<AssemblyName>Volo.Abp.BackgroundJobs.TickerQ</AssemblyName>

2
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs

@ -65,7 +65,7 @@ public class AbpBackgroundJobsTickerQModule : AbpModule
using (var scope = serviceProvider.CreateScope())
{
var jobExecuter = serviceProvider.GetRequiredService<IBackgroundJobExecuter>();
var args = await TickerRequestProvider.GetRequestAsync<TArgs>(serviceProvider, context.Id, context.Type);
var args = await TickerRequestProvider.GetRequestAsync<TArgs>(context, cancellationToken);
var jobType = options.GetJob(typeof(TArgs)).JobType;
var jobExecutionContext = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: cancellationToken);
await jobExecuter.ExecuteAsync(jobExecutionContext);

5
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTimeTickerConfiguration.cs

@ -1,4 +1,3 @@
using System;
using TickerQ.Utilities.Enums;
namespace Volo.Abp.BackgroundJobs.TickerQ;
@ -11,7 +10,5 @@ public class AbpBackgroundJobsTimeTickerConfiguration
public TickerTaskPriority? Priority { get; set; }
public Guid? BatchParent { get; set; }
public BatchRunCondition? BatchRunCondition { get; set; }
public RunCondition? RunCondition { get; set; }
}

13
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpTickerQBackgroundJobManager.cs

@ -2,8 +2,8 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using TickerQ.Utilities;
using TickerQ.Utilities.Entities;
using TickerQ.Utilities.Interfaces.Managers;
using TickerQ.Utilities.Models.Ticker;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundJobs.TickerQ;
@ -11,12 +11,12 @@ namespace Volo.Abp.BackgroundJobs.TickerQ;
[Dependency(ReplaceServices = true)]
public class AbpTickerQBackgroundJobManager : IBackgroundJobManager, ITransientDependency
{
protected ITimeTickerManager<TimeTicker> TimeTickerManager { get; }
protected ITimeTickerManager<TimeTickerEntity> TimeTickerManager { get; }
protected AbpBackgroundJobOptions Options { get; }
protected AbpBackgroundJobsTickerQOptions TickerQOptions { get; }
public AbpTickerQBackgroundJobManager(
ITimeTickerManager<TimeTicker> timeTickerManager,
ITimeTickerManager<TimeTickerEntity> timeTickerManager,
IOptions<AbpBackgroundJobOptions> options,
IOptions<AbpBackgroundJobsTickerQOptions> tickerQOptions)
{
@ -28,7 +28,7 @@ public class AbpTickerQBackgroundJobManager : IBackgroundJobManager, ITransientD
public virtual async Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
var job = Options.GetJob(typeof(TArgs));
var timeTicker = new TimeTicker
var timeTicker = new TimeTickerEntity
{
Id = Guid.NewGuid(),
Function = job.JobName,
@ -41,11 +41,10 @@ public class AbpTickerQBackgroundJobManager : IBackgroundJobManager, ITransientD
{
timeTicker.Retries = config.Retries ?? timeTicker.Retries;
timeTicker.RetryIntervals = config.RetryIntervals ?? timeTicker.RetryIntervals;
timeTicker.BatchParent = config.BatchParent ?? timeTicker.BatchParent;
timeTicker.BatchRunCondition = config.BatchRunCondition ?? timeTicker.BatchRunCondition;
timeTicker.RunCondition = config.RunCondition ?? timeTicker.RunCondition;
}
var result = await TimeTickerManager.AddAsync(timeTicker);
return !result.IsSucceded ? timeTicker.Id.ToString() : result.Result.Id.ToString();
return !result.IsSucceeded ? timeTicker.Id.ToString() : result.Result.Id.ToString();
}
}

2
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo.Abp.BackgroundWorkers.TickerQ.csproj

@ -4,7 +4,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<AssemblyName>Volo.Abp.BackgroundWorkers.TickerQ</AssemblyName>

6
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQModule.cs

@ -1,8 +1,8 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using TickerQ.Utilities.Entities;
using TickerQ.Utilities.Interfaces.Managers;
using TickerQ.Utilities.Models.Ticker;
using Volo.Abp.Modularity;
using Volo.Abp.TickerQ;
@ -14,11 +14,11 @@ public class AbpBackgroundWorkersTickerQModule : AbpModule
public override async Task OnPostApplicationInitializationAsync(ApplicationInitializationContext context)
{
var abpTickerQBackgroundWorkersProvider = context.ServiceProvider.GetRequiredService<AbpTickerQBackgroundWorkersProvider>();
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTicker>>();
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTickerEntity>>();
var abpBackgroundWorkersTickerQOptions = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundWorkersTickerQOptions>>().Value;
foreach (var backgroundWorker in abpTickerQBackgroundWorkersProvider.BackgroundWorkers)
{
var cronTicker = new CronTicker
var cronTicker = new CronTickerEntity
{
Function = backgroundWorker.Value.Function,
Expression = backgroundWorker.Value.CronExpression

2
framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpTickerQPeriodicBackgroundWorkerInvoker.cs

@ -3,7 +3,7 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using TickerQ.Utilities.Models;
using TickerQ.Utilities.Base;
namespace Volo.Abp.BackgroundWorkers.TickerQ;

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs

@ -49,6 +49,6 @@ public static class EfCoreRepositoryExtensions
public static IQueryable<TEntity> AsNoTrackingIf<TEntity>(this IQueryable<TEntity> queryable, bool condition)
where TEntity : class, IEntity
{
return condition ? queryable.AsNoTracking() : queryable;
return condition ? queryable.AsNoTracking() : queryable.AsTracking();
}
}

4
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -478,9 +478,7 @@ public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbC
{
return includeDetails
? await (await WithDetailsAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken))
: !ShouldTrackingEntityChange()
? await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken))
: await (await GetDbSetAsync()).FindAsync(new object[] { id! }, GetCancellationToken(cancellationToken));
: await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken));
}
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)

11
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs

@ -55,10 +55,17 @@ public static class ObjectMappingHelper
return true;
}
if (type.IsGenericType &&
supportedCollectionTypes.Contains(type.GetGenericTypeDefinition()) ||
if ((type.IsGenericType &&
supportedCollectionTypes.Contains(type.GetGenericTypeDefinition())) ||
type.GetInterfaces().Any(i => i.IsGenericType && supportedCollectionTypes.Contains(i.GetGenericTypeDefinition())))
{
if (!type.IsGenericType)
{
elementType = null!;
definitionGenericType = null!;
return false;
}
elementType = type.GetGenericArguments()[0];
definitionGenericType = type.GetGenericTypeDefinition();
if (definitionGenericType == typeof(IEnumerable<>) ||

7
framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenServiceCollectionExtensions.cs

@ -77,7 +77,8 @@ public static class AbpSwaggerGenServiceCollectionExtensions
string[]? scopes = null,
string[]? flows = null,
string? discoveryEndpoint = null,
Action<SwaggerGenOptions>? setupAction = null)
Action<SwaggerGenOptions>? setupAction = null,
string oidcAuthenticationScheme = "oidc")
{
var discoveryUrl = discoveryEndpoint != null ?
$"{discoveryEndpoint.TrimEnd('/')}/.well-known/openid-configuration":
@ -96,7 +97,7 @@ public static class AbpSwaggerGenServiceCollectionExtensions
.AddSwaggerGen(
options =>
{
options.AddSecurityDefinition("oidc", new OpenApiSecurityScheme
options.AddSecurityDefinition(oidcAuthenticationScheme, new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OpenIdConnect,
OpenIdConnectUrl = new Uri(RemoveTenantPlaceholders(discoveryUrl))
@ -104,7 +105,7 @@ public static class AbpSwaggerGenServiceCollectionExtensions
options.AddSecurityRequirement(document => new OpenApiSecurityRequirement()
{
[new OpenApiSecuritySchemeReference("oauth2", document)] = []
[new OpenApiSecuritySchemeReference(oidcAuthenticationScheme, document)] = []
});
setupAction?.Invoke(options);

7
framework/src/Volo.Abp.TickerQ/Microsoft/AspNetCore/Builder/AbpTickerQApplicationBuilderExtensions.cs → framework/src/Volo.Abp.TickerQ/Microsoft/Extensions/Hosting/AbpTickerQApplicationBuilderExtensions.cs

@ -1,16 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TickerQ.DependencyInjection;
using TickerQ.Utilities;
using TickerQ.Utilities.Enums;
using Volo.Abp.TickerQ;
namespace Microsoft.AspNetCore.Builder;
namespace Microsoft.Extensions.Hosting;
public static class AbpTickerQApplicationBuilderExtensions
{
public static IApplicationBuilder UseAbpTickerQ(this IApplicationBuilder app, TickerQStartMode qStartMode = TickerQStartMode.Immediate)
public static IHost UseAbpTickerQ(this IHost app, TickerQStartMode qStartMode = TickerQStartMode.Immediate)
{
var abpTickerQFunctionProvider = app.ApplicationServices.GetRequiredService<AbpTickerQFunctionProvider>();
var abpTickerQFunctionProvider = app.Services.GetRequiredService<AbpTickerQFunctionProvider>();
TickerFunctionProvider.RegisterFunctions(abpTickerQFunctionProvider.Functions);
TickerFunctionProvider.RegisterRequestType(abpTickerQFunctionProvider.RequestTypes);

2
framework/src/Volo.Abp.TickerQ/Volo.Abp.TickerQ.csproj

@ -4,7 +4,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<AssemblyName>Volo.Abp.TickerQ</AssemblyName>

5
framework/src/Volo.Abp.TickerQ/Volo/Abp/TickerQ/AbpTickerQModule.cs

@ -10,7 +10,10 @@ public class AbpTickerQModule : AbpModule
{
context.Services.AddTickerQ(options =>
{
options.SetInstanceIdentifier(context.Services.GetApplicationName());
options.ConfigureScheduler(scheduler =>
{
scheduler.NodeIdentifier = context.Services.GetApplicationName();
});
});
}
}

91
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.ChangeTracking;
@ -83,6 +84,96 @@ public class ChangeTrackingInterceptor_Tests : TestAppTestBase<AbpEntityFramewor
});
}
[Fact]
public async Task Repository_Should_Override_Global_NoTracking_When_EntityChangeTracking_Is_Enabled()
{
await AddSomePeopleAsync();
var repository = GetRequiredService<IRepository<Person, Guid>>();
Guid personId = default;
await WithUnitOfWorkAsync(async () =>
{
var p = await repository.FindAsync(x => x.Name == "people1");
p.ShouldNotBeNull();
personId = p.Id;
});
// Simulate global NoTracking configured on DbContext (e.g. optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking))
await WithUnitOfWorkAsync(async () =>
{
var db = await repository.GetDbContextAsync();
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
db.ChangeTracker.Entries().Count().ShouldBe(0);
// FindAsync(id): ShouldTrackingEntityChange()=true, GetQueryableAsync() uses AsTracking() to override global NoTracking
var person = await repository.FindAsync(personId, includeDetails: false);
person.ShouldNotBeNull();
db.ChangeTracker.Entries<Person>().Count().ShouldBe(1);
db.Entry(person).State.ShouldBe(EntityState.Unchanged);
});
await WithUnitOfWorkAsync(async () =>
{
var db = await repository.GetDbContextAsync();
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
db.ChangeTracker.Entries().Count().ShouldBe(0);
// FindAsync(predicate): same - AsTracking() overrides global NoTracking
var person = await repository.FindAsync(x => x.Name == "people1");
person.ShouldNotBeNull();
db.ChangeTracker.Entries<Person>().Count().ShouldBe(1);
db.Entry(person).State.ShouldBe(EntityState.Unchanged);
});
await WithUnitOfWorkAsync(async () =>
{
var db = await repository.GetDbContextAsync();
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
db.ChangeTracker.Entries().Count().ShouldBe(0);
// GetListAsync: same - AsTracking() overrides global NoTracking
var list = await repository.GetListAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries<Person>().Count().ShouldBe(list.Count);
});
}
[Fact]
public async Task Repository_Should_Respect_NoTracking_When_EntityChangeTracking_Is_Disabled_With_Global_NoTracking()
{
await AddSomePeopleAsync();
var repository = GetRequiredService<IRepository<Person, Guid>>();
Guid personId = default;
await WithUnitOfWorkAsync(async () =>
{
var p = await repository.FindAsync(x => x.Name == "people1");
p.ShouldNotBeNull();
personId = p.Id;
});
await WithUnitOfWorkAsync(async () =>
{
var db = await repository.GetDbContextAsync();
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
db.ChangeTracker.Entries().Count().ShouldBe(0);
// When tracking is explicitly disabled, entity should NOT be tracked regardless of global setting
using (repository.DisableTracking())
{
var person = await repository.FindAsync(personId, includeDetails: false);
person.ShouldNotBeNull();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await repository.GetListAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(0);
}
});
}
private async Task AddSomePeopleAsync()
{
var repository = GetRequiredService<IRepository<Person, Guid>>();

84
framework/test/Volo.Abp.ObjectMapping.Tests/Volo/Abp/ObjectMapping/ObjectMappingHelper_Tests.cs

@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Shouldly;
using Xunit;
namespace Volo.Abp.ObjectMapping;
public class ObjectMappingHelper_Tests
{
[Fact]
public void IsCollectionGenericType_Should_Return_True_For_Standard_GenericCollection()
{
var result = ObjectMappingHelper.IsCollectionGenericType<List<MappingTestSource>, List<MappingTestDestination>>(
out var sourceArg, out var destArg, out var defGenericType);
result.ShouldBeTrue();
sourceArg.ShouldBe(typeof(MappingTestSource));
destArg.ShouldBe(typeof(MappingTestDestination));
defGenericType.ShouldBe(typeof(List<>));
}
[Fact]
public void IsCollectionGenericType_Should_Return_True_For_Array()
{
var result = ObjectMappingHelper.IsCollectionGenericType<MappingTestSource[], MappingTestDestination[]>(
out var sourceArg, out var destArg, out _);
result.ShouldBeTrue();
sourceArg.ShouldBe(typeof(MappingTestSource));
destArg.ShouldBe(typeof(MappingTestDestination));
}
[Fact]
public void IsCollectionGenericType_Should_Normalize_IEnumerable_To_List()
{
var result = ObjectMappingHelper.IsCollectionGenericType<IEnumerable<MappingTestSource>, IEnumerable<MappingTestDestination>>(
out _, out _, out var defGenericType);
result.ShouldBeTrue();
defGenericType.ShouldBe(typeof(List<>));
}
[Fact]
public void IsCollectionGenericType_Should_Normalize_ICollection_To_Collection()
{
var result = ObjectMappingHelper.IsCollectionGenericType<ICollection<MappingTestSource>, ICollection<MappingTestDestination>>(
out _, out _, out var defGenericType);
result.ShouldBeTrue();
defGenericType.ShouldBe(typeof(Collection<>));
}
[Fact]
public void IsCollectionGenericType_Should_Return_False_For_NonCollection()
{
var result = ObjectMappingHelper.IsCollectionGenericType<MappingTestSource, MappingTestDestination>(
out _, out _, out _);
result.ShouldBeFalse();
}
[Fact]
public void IsCollectionGenericType_Should_Return_False_For_NonGeneric_DerivedCollection()
{
var result = ObjectMappingHelper.IsCollectionGenericType<List<MappingTestSource>, MappingTestDestinationList>(
out _, out _, out _);
result.ShouldBeFalse();
}
}
public class MappingTestSource
{
public string Value { get; set; } = "";
}
public class MappingTestDestination
{
public string Value { get; set; } = "";
}
public class MappingTestDestinationList : List<MappingTestDestination>
{
}

2
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/CleanupJobs.cs

@ -1,7 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using TickerQ.Utilities.Models;
using TickerQ.Utilities.Base;
namespace Volo.Abp.BackgroundJobs.DemoApp.TickerQ;

29
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.TickerQ/DemoAppTickerQModule.cs

@ -3,13 +3,14 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using TickerQ.Dashboard.DependencyInjection;
using TickerQ.DependencyInjection;
using TickerQ.Utilities;
using TickerQ.Utilities.Enums;
using TickerQ.Utilities.Interfaces.Managers;
using TickerQ.Utilities.Models;
using TickerQ.Utilities.Models.Ticker;
using TickerQ.Utilities.Base;
using TickerQ.Utilities.Entities;
using Volo.Abp.AspNetCore;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundJobs.DemoApp.Shared;
@ -35,13 +36,14 @@ public class DemoAppTickerQModule : AbpModule
{
context.Services.AddTickerQ(options =>
{
options.UpdateMissedJobCheckDelay(TimeSpan.FromSeconds(30));
options.ConfigureScheduler(scheduler =>
{
scheduler.FallbackIntervalChecker = TimeSpan.FromSeconds(30);
});
options.AddDashboard(x =>
{
x.BasePath = "/tickerq-dashboard";
x.UseHostAuthentication = true;
x.SetBasePath("/tickerq-dashboard");
});
});
@ -78,7 +80,7 @@ public class DemoAppTickerQModule : AbpModule
abpTickerQFunctionProvider.Functions.TryAdd(nameof(CleanupJobs), (string.Empty, TickerTaskPriority.Normal, new TickerFunctionDelegate(async (cancellationToken, serviceProvider, tickerFunctionContext) =>
{
var service = new CleanupJobs();
var request = await TickerRequestProvider.GetRequestAsync<string>(serviceProvider, tickerFunctionContext.Id, tickerFunctionContext.Type);
var request = await TickerRequestProvider.GetRequestAsync<string>(tickerFunctionContext, cancellationToken);
var genericContext = new TickerFunctionContext<string>(tickerFunctionContext, request);
await service.CleanupLogsAsync(genericContext, cancellationToken);
})));
@ -92,10 +94,11 @@ public class DemoAppTickerQModule : AbpModule
await backgroundWorkerManager.AddAsync(context.ServiceProvider.GetRequiredService<MyBackgroundWorker>());
var app = context.GetApplicationBuilder();
app.UseAbpTickerQ();
var timeTickerManager = context.ServiceProvider.GetRequiredService<ITimeTickerManager<TimeTicker>>();
await timeTickerManager.AddAsync(new TimeTicker
context.GetHost().UseAbpTickerQ();
var timeTickerManager = context.ServiceProvider.GetRequiredService<ITimeTickerManager<TimeTickerEntity>>();
await timeTickerManager.AddAsync(new TimeTickerEntity
{
Function = nameof(CleanupJobs),
ExecutionTime = DateTime.UtcNow.AddSeconds(5),
@ -104,8 +107,8 @@ public class DemoAppTickerQModule : AbpModule
RetryIntervals = new[] { 30, 60, 120 }, // Retry after 30s, 60s, then 2min
});
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTicker>>();
await cronTickerManager.AddAsync(new CronTicker
var cronTickerManager = context.ServiceProvider.GetRequiredService<ICronTickerManager<CronTickerEntity>>();
await cronTickerManager.AddAsync(new CronTickerEntity
{
Function = nameof(CleanupJobs),
Expression = "* * * * *", // Every minute
@ -134,7 +137,7 @@ public class DemoAppTickerQModule : AbpModule
await Task.Delay(1000);
var timeTickerManager = serviceProvider.GetRequiredService<ITimeTickerManager<TimeTicker>>();
var timeTickerManager = serviceProvider.GetRequiredService<ITimeTickerManager<TimeTickerEntity>>();
var result = await timeTickerManager.DeleteAsync(Guid.Parse(jobId));
}
}

2
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -1,3 +1,4 @@
import { NgStyle } from '@angular/common';
import { ConfigStateService, CurrentUserDto, LocalizationPipe } from '@abp/ng.core';
import {
ButtonComponent,
@ -99,6 +100,7 @@ type PermissionWithGroupName = PermissionGrantInfoDto & {
],
imports: [
FormsModule,
NgStyle,
ModalComponent,
LocalizationPipe,
ButtonComponent,

Loading…
Cancel
Save