Browse Source

Merge branch 'dev' into feature/ssr-support

# Conflicts:
#	npm/ng-packs/apps/dev-app/project.json
pull/23416/head
erdemcaygor 1 year ago
parent
commit
c14629dd77
  1. 1
      Directory.Packages.props
  2. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  3. 2
      docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md
  4. 546
      docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md
  5. 5
      docs/en/framework/architecture/multi-tenancy/index.md
  6. 16
      docs/en/framework/fundamentals/object-extensions.md
  7. 129
      docs/en/framework/infrastructure/object-to-object-mapping.md
  8. 184
      docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md
  9. 27
      docs/en/studio/release-notes.md
  10. 2
      docs/en/studio/version-mapping.md
  11. 2
      docs/en/tutorials/modular-crm/part-05.md
  12. 114
      docs/en/tutorials/todo/layered/index.md
  13. 14
      framework/Volo.Abp.sln
  14. 10
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs
  15. 8
      framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.cshtml
  16. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs
  17. 4
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs
  18. 22
      framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs
  19. 29
      framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj
  20. 27
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs
  21. 25
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs
  22. 23
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs
  23. 14
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs
  24. 32
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs
  25. 281
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
  26. 2
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs
  27. 11
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs
  28. 135
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs
  29. 81
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs
  30. 82
      framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs
  31. 17
      framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj
  32. 59
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs
  33. 114
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs
  34. 191
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs
  35. 68
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs
  36. 86
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
  37. 13
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs
  38. 44
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs
  39. 10
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs
  40. 10
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs
  41. 10
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs
  42. 43
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs
  43. 39
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs
  44. 8
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs
  45. 9
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs
  46. 10
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs
  47. 1
      modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj
  48. 21
      modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs
  49. 10
      modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs
  50. 15
      modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs
  51. 20
      modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs
  52. 27
      modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs
  53. 15
      modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs
  54. 2
      modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj
  55. 15
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs
  56. 13
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs
  57. 14
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs
  58. 2
      modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj
  59. 46
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json
  60. 2
      modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj
  61. 19
      modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs
  62. 11
      modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs
  63. 15
      modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs
  64. 18
      modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs
  65. 25
      modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs
  66. 13
      modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs
  67. 2
      modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj
  68. 2
      modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj
  69. 11
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs
  70. 33
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs
  71. 15
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs
  72. 57
      modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs
  73. 94
      modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs
  74. 11
      modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs
  75. 2
      modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj
  76. 29
      npm/ng-packs/apps/dev-app/project.json
  77. 2
      npm/ng-packs/packages/account-core/tsconfig.lib.json
  78. 2
      npm/ng-packs/packages/account/tsconfig.lib.json
  79. 3
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  80. 2
      npm/ng-packs/packages/components/tsconfig.lib.json
  81. 6
      npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts
  82. 10
      npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts
  83. 9
      npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts
  84. 2
      npm/ng-packs/packages/core/tsconfig.lib.json
  85. 2
      npm/ng-packs/packages/feature-management/tsconfig.lib.json
  86. 2
      npm/ng-packs/packages/identity/tsconfig.lib.json
  87. 37
      npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts
  88. 7
      npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts
  89. 2
      npm/ng-packs/packages/oauth/tsconfig.lib.json
  90. 2
      npm/ng-packs/packages/permission-management/tsconfig.lib.json
  91. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template
  92. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template
  93. 2
      npm/ng-packs/packages/schematics/tsconfig.json
  94. 2
      npm/ng-packs/packages/setting-management/tsconfig.lib.json
  95. 2
      npm/ng-packs/packages/tenant-management/tsconfig.lib.json
  96. 2
      npm/ng-packs/packages/theme-basic/tsconfig.lib.json
  97. 2
      npm/ng-packs/packages/theme-shared/tsconfig.lib.json
  98. 9
      npm/ng-packs/scripts/publish.ts
  99. 2
      npm/ng-packs/source-code-requirements/tsconfig.lib.json
  100. 3
      npm/ng-packs/tsconfig.base.json

1
Directory.Packages.props

@ -145,6 +145,7 @@
<PackageVersion Include="RabbitMQ.Client" Version="7.1.2" />
<PackageVersion Include="Rebus" Version="8.8.0" />
<PackageVersion Include="Rebus.ServiceProvider" Version="10.3.0" />
<PackageVersion Include="Riok.Mapperly" Version="4.2.1" />
<PackageVersion Include="Scriban" Version="6.2.1" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />

3
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -431,6 +431,9 @@
"WhoWeAre_Expert": "About Me",
"CreateSolutionFolder": "Create Solution Folder",
"CreateSolutionFolderOption": "Specifies if the project will be in a new folder in the output folder or directly the output folder.",
"CreateCrudPage": "Create CRUD Page",
"CreateCrudPageOption": "Generates a sample CRUD page with a Book entity to demonstrate basic operations (Create, Read, Update, Delete).",
"ConnectionString": "Connection string",
"BooksPageTitle": "ABP Books",
"BooksPageDescription": "Explore ABP books to deepen your understanding and mastery of ABP.",
"PackageDetailPage_NuGetPackageInstallationOptions": "There are three ways to install <code>{0}</code> NuGet package to your project",

2
docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md

@ -22,7 +22,7 @@ In the shared database model, all the application data stored in a single physic
![single-shared-database](single-shared-database.png)
This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for must applications.
This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for most applications.
In this model, a single database table may contain data of multiple tenants. Each row in these tables have a `TenantId` field which is used to distinguish the tenant data and isolate a tenant's data from other tenant users. To make your entities multi-tenant aware, all you have to do is to implement the `IMultiTenant` interface provided by the ABP Framework.

546
docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md

@ -0,0 +1,546 @@
# How to Build Persistent Background Jobs with ABP Framework and Quartz
## Introduction
In modern SaaS applications, automated background processing is essential for delivering reliable user experiences. Whether you're sending subscription reminders, processing payments, or generating reports, background jobs ensure critical tasks happen on schedule without blocking your main application flow.
### What is `Quartz.NET`?
`Quartz.NET` is a powerful, open-source job scheduling library for .NET applications that provides cron-based scheduling for complex time patterns, job persistence across application restarts, clustering support for high-availability scenarios, flexible trigger types, and the ability to pass parameters to jobs through job data maps. It's the de facto standard for enterprise-grade job scheduling in the .NET ecosystem.
### Quartz Storage Options: In-Memory vs Persistent
When configuring **Quartz**, you have two primary storage options, each with significant implications for how your application behaves:
### 🧠 In-Memory Storage (`RAMJobStore`)
- Keeps all job information in application memory.
- **Very fast** – no database overhead.
- **Volatile** – all jobs, triggers, and schedules are lost when the application stops or restarts.
- Best suited for:
- Development environments.
- Scenarios where job loss is acceptable.
### 🗃️ Persistent Storage (`JobStoreTX` or similar)
- Stores all job information in a database.
- **Reliable** – schedules persist across:
- Application restarts
- Server crashes
- Deployments
- **Supports horizontal scaling** – multiple application instances can share the same job queue.
- **Slight performance overhead** due to database I/O.
- Best choice for:
- Production systems.
- Any scenario where **business continuity and reliability** are critical.
### How ABP Simplifies Quartz Integration
ABP handles Quartz configuration, dependency injection, and lifecycle management automatically. Developers define jobs using `QuartzBackgroundWorkerBase` and access services via `ICachedServiceProvider`, following ABP's standard conventions and leveraging optimal service caching for background job scenarios.
### Benefits of the Integration
- Full support for ABP’s cross-cutting concerns (e.g., multi-tenancy, localization)
- Robust scheduling powered by Quartz
- Built-in logging, error handling, and performance monitoring
- Scales easily without modifying business logic
### Real-World Use Case: Subscription Reminders
In this tutorial, we'll build a subscription reminder system that monitors client subscriptions, identifies those nearing expiration, sends professional email reminders seven days before expiration, tracks reminder history to prevent duplicates, and runs automatically every day at 9:00 AM using Quartz scheduling with PostgreSQL persistence. This system demonstrates how ABP and Quartz work together to solve real business problems with clean, maintainable code that follows enterprise-grade patterns.
## Installing and Configuring Quartz
Getting Quartz up and running in an ABP application is straightforward thanks to ABP's dedicated integration package. We'll replace the default background job system with Quartz for persistent job storage and robust scheduling capabilities.
### Adding the Quartz Package
The easiest way to add Quartz support to your ABP application is using the ABP CLI. Open a terminal in your project directory and run:
```bash
abp add-package Volo.Abp.BackgroundWorkers.Quartz
```
This command automatically adds the necessary NuGet package reference and updates your module dependencies. The ABP CLI handles all the heavy lifting, ensuring you get the correct version that matches your ABP Framework version.
### Configuring Quartz for Persistent Storage
Once the package is installed, you need to configure Quartz to use your database (in my case it is PostgreSQL) for job persistence. This configuration goes in your main module's `PreConfigureServices` method:
```csharp
[DependsOn(
// ... other dependencies
typeof(AbpBackgroundJobsQuartzModule),
typeof(AbpBackgroundWorkersQuartzModule)
)]
public class MySaaSApplicationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
ConfigureAuthentication(context, configuration);
ConfigureUrls(configuration);
ConfigureImpersonation(context, configuration);
ConfigureQuartz(); // Add this line
}
private void ConfigureQuartz()
{
PreConfigure<AbpQuartzOptions>(options =>
{
options.Properties = new NameValueCollection
{
["quartz.scheduler.instanceName"] = "QuartzScheduler",
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.tablePrefix"] = "qrtz_",
["quartz.jobStore.dataSource"] = "myDS",
["quartz.dataSource.myDS.connectionString"] = _configuration.GetConnectionString("Default"),
["quartz.dataSource.myDS.provider"] = "Npgsql",
["quartz.serializer.type"] = "json"
};
});
}
}
```
This configuration tells Quartz to store all job information in your PostgreSQL database using tables prefixed with "qrtz_". The key points are:
- **Job Store Type**: Uses ADO.NET with transaction support for reliable job persistence
- **Connection String**: Shares your application's existing database connection
- **Table Prefix**: Keeps Quartz tables separate with the "qrtz_" prefix
- **JSON Serialization**: Makes job data readable and debuggable
- **PostgreSQL Provider**: Uses Npgsql for optimal PostgreSQL integration
When your application starts, ABP automatically initializes the Quartz scheduler with these settings. Any background workers you create will be registered and scheduled automatically, with their state persisted to the database for reliability across application restarts.
For detailed installation options and advanced configuration scenarios, check the official [ABP documentation.](https://abp.io/docs/latest/framework/infrastructure/background-workers/quartz)
## Database Setup for Quartz
With Quartz configured for persistent storage, we need to create the necessary database tables where Quartz will store job definitions, triggers, and execution history. Rather than running SQL scripts directly against the database, we'll use Entity Framework migrations to maintain consistency with ABP's database management approach.
### Creating an Empty Migration for Quartz Tables
Instead of executing raw SQL scripts against the database, we created an empty Entity Framework migration and populated it with the required Quartz table definitions. This approach keeps all database changes within the migration system, ensuring they're version-controlled, repeatable, and consistent across different environments.
To create the empty migration, we used the standard Entity Framework CLI command:
```bash
dotnet ef migrations add AddQuartzTables
```
This generates a new migration file with empty `Up` and `Down` methods that we can populate with the Quartz table creation scripts.
### Adding Quartz SQL Schema to Migration
Once the empty migration was created, we populated it with the PostgreSQL-specific SQL needed to create all Quartz tables. The SQL scripts were obtained from the official Quartz repository, which provides database schema scripts for various database providers:
```csharp
public partial class AddQuartzTables : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE TABLE qrtz_job_details (
sched_name VARCHAR(120) NOT NULL,
job_name VARCHAR(200) NOT NULL,
job_group VARCHAR(200) NOT NULL,
description VARCHAR(250) NULL,
job_class_name VARCHAR(250) NOT NULL,
is_durable BOOLEAN NOT NULL,
is_nonconcurrent BOOLEAN NOT NULL,
is_update_data BOOLEAN NOT NULL,
requests_recovery BOOLEAN NOT NULL,
job_data BYTEA NULL,
PRIMARY KEY (sched_name, job_name, job_group)
);
CREATE TABLE qrtz_triggers (
sched_name VARCHAR(120) NOT NULL,
trigger_name VARCHAR(200) NOT NULL,
trigger_group VARCHAR(200) NOT NULL,
job_name VARCHAR(200) NOT NULL,
job_group VARCHAR(200) NOT NULL,
-- ... additional columns and constraints
PRIMARY KEY (sched_name, trigger_name, trigger_group),
FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details(sched_name, job_name, job_group)
);
-- Additional tables: qrtz_simple_triggers, qrtz_cron_triggers,
-- qrtz_simprop_triggers, qrtz_blob_triggers, qrtz_calendars,
-- qrtz_paused_trigger_grps, qrtz_fired_triggers, qrtz_scheduler_state, qrtz_locks
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
DROP TABLE IF EXISTS qrtz_locks;
DROP TABLE IF EXISTS qrtz_scheduler_state;
-- ... drop all other Quartz tables in reverse order
DROP TABLE IF EXISTS qrtz_triggers;
DROP TABLE IF EXISTS qrtz_job_details;
");
}
}
```
The complete SQL scripts for all supported database providers, including PostgreSQL, MySQL, SQL Server, and others, can be found in the official `Quartz.NET` repository. You should use the script that matches your specific database provider and version requirements.
### Why Use Migrations Instead of Direct SQL Scripts?
This migration-based approach offers several important advantages over running SQL scripts directly:
**Version Control Integration**: The migration becomes part of your codebase, tracked in source control alongside your application changes. This means every developer and deployment environment gets the exact same database schema.
**Rollback Capability**: The `Down` method provides a clean way to remove Quartz tables if needed, something that's much harder to manage with standalone SQL scripts.
**Environment Consistency**: Whether you're setting up a development machine, staging server, or production deployment, running DBMigrator or `dotnet ef database update` command ensures the same schema is created everywhere.
**Integration with ABP's Database Management**: This approach aligns perfectly with how ABP manages all other database changes, keeping your database evolution strategy consistent.
The Quartz tables created by this migration handle all aspects of job persistence - from storing job definitions and triggers to tracking execution history and managing scheduler state. With these tables in place, your Quartz scheduler can reliably persist jobs across application restarts and coordinate work across multiple application instances if needed.
After creating this migration, running DBMigrator `dotnet ef database update` will create all the necessary Quartz infrastructure in your PostgreSQL database, ready to store and manage your background jobs.
For complete SQL scripts for your specific database provider, visit the official [Quartz documentation.](https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html#creating-and-initializing-database)
## Building the Business Logic
Before implementing our Quartz background job, we needed to create the essential business entities and services that our subscription reminder system would work with. Since this article focuses on Quartz integration rather than general ABP development patterns, we'll keep this section brief and move quickly to the background job implementation.
### Core Entities and Services
For our subscription reminder system, we created the following core components:
**Entities:**
- **`Client`**: Represents customers with subscription information (Name, Email, SubscriptionEnd, IsActive)
- **`ReminderLog`**: Tracks when reminder emails have been sent to prevent duplicates
**Application Services:**
- **`ClientAppService`**: Handles CRUD operations and provides methods to find clients with expiring subscriptions
- **`ReminderLogAppService`**: Manages reminder history and prevents duplicate notifications
- **`EmailService`**: Sends professional HTML reminder emails via SMTP
**Data Transfer Objects (DTOs):**
- Complete set of DTOs for both entities following ABP conventions
- Input/output DTOs for all service operations
### Business Logic Overview
The system follows standard ABP patterns with entities inheriting from `FullAuditedAggregateRoot`, services implementing `ICrudAppService` interfaces, and proper AutoMapper configurations for entity-DTO mapping. We also included a data seeder to create sample clients for testing purposes.
The key business methods our background job will use are:
- `GetExpiringClientsAsync()` - Finds clients whose subscriptions expire in the next 7 days
- `CreateAsync()` - Logs when a reminder has been sent
- `SendSubscriptionExpiryReminderAsync()` - Sends professional email reminders
### Focus on Background Operations
Rather than diving deep into ABP entity creation, repository patterns, or service layer implementation details, we'll move directly to the heart of this article: implementing robust background jobs with Quartz. The entities and services we created simply provide the business context for our background job to operate within.
The real value of this tutorial lies in showing how ABP's `QuartzBackgroundWorkerBase` integrates seamlessly with your business logic to create reliable, persistent background operations that survive application restarts and scale across multiple instances.
Let's now implement the background job that ties everything together and demonstrates the power of ABP + Quartz integration.
## Implementing the Background Job (The ABP Way)
This is where the magic happens. ABP's integration with Quartz provides a clean, powerful way to create background jobs that follow framework conventions while leveraging Quartz's robust scheduling capabilities. Let's dive into how we implemented our subscription reminder job and explore the advanced features ABP provides.
### Creating a QuartzBackgroundWorkerBase Job
Instead of implementing Quartz's raw `IJob` interface, ABP provides `QuartzBackgroundWorkerBase`, which integrates seamlessly with ABP's dependency injection, logging, and lifecycle management systems:
```csharp
public class SubscriptionExpiryNotifierJob : QuartzBackgroundWorkerBase
{
public SubscriptionExpiryNotifierJob()
{
// Configure the job to run daily at 9:00 AM
JobDetail = JobBuilder.Create<SubscriptionExpiryNotifierJob>()
.WithIdentity(nameof(SubscriptionExpiryNotifierJob))
.Build();
Trigger = TriggerBuilder.Create()
.WithIdentity(nameof(SubscriptionExpiryNotifierJob))
.WithCronSchedule("0 0 9 * * ?") // Every day at 9:00 AM
.Build();
ScheduleJob = async scheduler =>
{
if (!await scheduler.CheckExists(JobDetail.Key))
{
await scheduler.ScheduleJob(JobDetail, Trigger);
}
};
}
public override async Task Execute(IJobExecutionContext context)
{
// Use ICachedServiceProvider for better performance and proper scoping
var serviceProvider = ServiceProvider.GetRequiredService<ICachedServiceProvider>();
// These services will be cached and reused throughout the job execution
var clientAppService = serviceProvider.GetRequiredService<IClientAppService>();
var reminderLogAppService = serviceProvider.GetRequiredService<IReminderLogAppService>();
var emailService = serviceProvider.GetRequiredService<IEmailService>();
Logger.LogInformation("🔄 Starting subscription expiry notification job...");
// 1. Get clients expiring in 7 days
var expiringClients = await clientAppService.GetExpiringClientsAsync(7);
Logger.LogInformation("📋 Found {Count} clients with expiring subscriptions", expiringClients.Count);
// 2. Process each client
foreach (var client in expiringClients)
{
await ProcessClientAsync(client, emailService, reminderLogAppService);
}
Logger.LogInformation("✅ Job completed successfully");
}
}
```
### Key Implementation Features
**Constructor-Based Configuration**: Unlike traditional Quartz jobs that require external scheduling code, ABP's approach lets you define both the job and its schedule directly in the constructor. This keeps related configuration together and makes the job self-contained.
**ABP Service Integration**: The `ICachedServiceProvider` gives you access to any service in ABP's dependency injection container, enabling you to use application services, repositories, domain services, or any other ABP component with optimized caching and proper scoping.
**Built-in Logging**: The `Logger` property provides access to ABP's logging infrastructure, automatically including context like correlation IDs and tenant information in multi-tenant applications.
**Custom Scheduling Logic**: The `ScheduleJob` property allows you to customize how the job gets registered with Quartz. In our example, we check if the job already exists before scheduling it, preventing duplicate registrations during application restarts.
### Understanding Quartz Trigger Types
Quartz provides several trigger types to handle different scheduling requirements. Choosing the right trigger type is crucial for your job's behavior and performance.
#### CronTrigger - Complex Time-Based Scheduling
CronTrigger uses cron expressions for sophisticated scheduling patterns. This is what we used for our daily subscription reminders:
```csharp
// Daily at 9:00 AM
Trigger = TriggerBuilder.Create()
.WithIdentity("DailyReminder")
.WithCronSchedule("0 0 9 * * ?")
.Build();
// Every weekday at 2:30 PM
Trigger = TriggerBuilder.Create()
.WithIdentity("WeekdayReport")
.WithCronSchedule("0 30 14 ? * MON-FRI")
.Build();
// First day of every month at midnight
Trigger = TriggerBuilder.Create()
.WithIdentity("MonthlyCleanup")
.WithCronSchedule("0 0 0 1 * ?")
.Build();
```
**Cron Expression Format**: `Seconds Minutes Hours Day-of-Month Month Day-of-Week Year(optional)`
- `0 0 9 * * ?` = 9:00 AM every day
- `0 */15 * * * ?` = Every 15 minutes
- `0 0 12 ? * SUN` = Every Sunday at noon
#### SimpleTrigger - Interval-Based Scheduling
SimpleTrigger is perfect for jobs that need to run at regular intervals or a specific number of times:
```csharp
// Run every 30 seconds indefinitely
Trigger = TriggerBuilder.Create()
.WithIdentity("HealthCheck")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInSeconds(30)
.RepeatForever())
.Build();
// Run every 5 minutes, but only 10 times
Trigger = TriggerBuilder.Create()
.WithIdentity("LimitedRetry")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInMinutes(5)
.WithRepeatCount(9)) // 0-based, so 9 = 10 executions
.Build();
// One-time execution after 1 hour delay
Trigger = TriggerBuilder.Create()
.WithIdentity("DelayedCleanup")
.StartAt(DateTimeOffset.UtcNow.AddHours(1))
.Build();
```
#### CalendarIntervalTrigger - Calendar-Aware Intervals
CalendarIntervalTrigger handles intervals that need to respect calendar boundaries:
```csharp
// Every month on the same day (handles varying month lengths)
Trigger = TriggerBuilder.Create()
.WithIdentity("MonthlyBilling")
.WithCalendarIntervalSchedule(x => x
.WithIntervalInMonths(1))
.Build();
// Every week, starting Monday
Trigger = TriggerBuilder.Create()
.WithIdentity("WeeklyReport")
.WithCalendarIntervalSchedule(x => x
.WithIntervalInWeeks(1))
.Build();
```
#### DailyTimeIntervalTrigger - Time Windows
DailyTimeIntervalTrigger runs jobs within specific time windows on certain days:
```csharp
// Every 2 hours between 8 AM and 6 PM, Monday through Friday
Trigger = TriggerBuilder.Create()
.WithIdentity("BusinessHoursSync")
.WithDailyTimeIntervalSchedule(x => x
.OnMondayThroughFriday()
.StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(8, 0))
.EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(18, 0))
.WithIntervalInHours(2))
.Build();
```
### Choosing the Right Trigger Type
For different scenarios, you'd choose different trigger types:
- **Daily/Weekly/Monthly Operations**: Use **CronTrigger** for maximum flexibility
- **High-Frequency Tasks**: Use **SimpleTrigger** for performance (every few seconds/minutes)
- **Business Calendar Operations**: Use **CalendarIntervalTrigger** for month-end reports, quarterly tasks
- **Business Hours Operations**: Use **DailyTimeIntervalTrigger** for operations that should only run during specific hours
### Automatic Job Registration
One of ABP's most powerful features is automatic job discovery and registration. When your application starts, ABP automatically:
1. **Scans for Background Workers**: ABP discovers all classes inheriting from `QuartzBackgroundWorkerBase`
2. **Registers with DI Container**: Each job is registered as a service in the dependency injection container
3. **Schedules with Quartz**: ABP calls the `ScheduleJob` delegate to register the job with the Quartz scheduler
4. **Handles Lifecycle**: ABP manages starting and stopping jobs with the application lifecycle
This means you simply create your job class, and ABP handles everything else. No manual registration, no startup code, no configuration files - it just works.
### Understanding Misfire Handling
Misfires occur when a scheduled job cannot execute at its intended time, typically due to system downtime, resource constraints, or the scheduler being paused. Quartz provides several misfire instructions to handle these scenarios:
#### CronTrigger Misfire Instructions
For cron-based schedules like our daily reminder job, Quartz offers these misfire behaviors:
**`MisfireInstruction.DoNothing`** (Default):
```csharp
Trigger = TriggerBuilder.Create()
.WithIdentity(nameof(SubscriptionExpiryNotifierJob))
.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionDoNothing())
.Build();
```
- Skips all missed executions
- Waits for the next naturally scheduled time
- Best for jobs where missing executions is acceptable
**`MisfireInstruction.FireOnceNow`**:
```csharp
.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionFireAndProceed())
```
- Immediately executes one missed job upon recovery
- Then continues with the normal schedule
- Useful when you need to catch up on missed work
**`MisfireInstruction.IgnoreMisfires`**:
```csharp
.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionIgnoreMisfires())
```
- Executes all missed jobs immediately upon recovery
- Can cause a burst of executions after extended downtime
- Use carefully to avoid overwhelming the system
#### SimpleTrigger Misfire Instructions
Simple triggers have their own set of misfire behaviors:
**`MisfireInstruction.FireNow`**: Execute immediately when recovered
**`MisfireInstruction.RescheduleNowWithExistingRepeatCount`**: Start over with remaining repeat count
**`MisfireInstruction.RescheduleNowWithRemainingRepeatCount`**: Continue as if no misfire occurred
**`MisfireInstruction.RescheduleNextWithExistingCount`**: Wait for next interval, keep original repeat count
### Real-World Misfire Considerations
For our subscription reminder system, we chose the default `DoNothing` behavior because:
- **Business Logic**: Sending yesterday's reminder today might confuse customers
- **Duplicate Prevention**: Our job checks for existing reminders, so running late won't cause duplicate emails
- **Resource Management**: We avoid overwhelming the email system after extended downtime
However, for other scenarios you might choose differently:
- **Financial reporting**: Use `FireOnceNow` to ensure reports are always generated
- **Data synchronization**: Use `IgnoreMisfires` to process all missed sync operations
- **Cache warming**: Use `DoNothing` since stale cache warming provides no value
### Advanced Job Features
**Error Handling and Resilience**: Our job implementation includes comprehensive error handling for individual client processing, ensuring one failed email doesn't stop the entire batch:
```csharp
try
{
await emailService.SendSubscriptionExpiryReminderAsync(/*...*/);
await LogReminderAsync(client.Id, client.SubscriptionEnd, "Email sent successfully", reminderLogAppService);
}
catch (Exception ex)
{
Logger.LogError(ex, "❌ Failed to send reminder to {ClientName}", client.Name);
await LogReminderAsync(client.Id, client.SubscriptionEnd, $"Failed: {ex.Message}", reminderLogAppService);
}
```
**Duplicate Prevention**: The job checks for existing reminders to prevent sending multiple emails on the same day, even if the job runs multiple times:
```csharp
private async Task<bool> AlreadySentTodayAsync(Guid clientId, IReminderLogAppService reminderLogAppService)
{
var todayReminders = await reminderLogAppService.GetByClientIdAsync(clientId);
var today = DateTime.UtcNow.Date;
return todayReminders.Any(r => r.ReminderDate.Date == today);
}
```
This implementation demonstrates how ABP's `QuartzBackgroundWorkerBase` provides a clean, powerful foundation for building robust background jobs that integrate seamlessly with your business logic while leveraging Quartz's enterprise-grade scheduling capabilities.
## Conclusion
You've successfully built a production-ready subscription reminder system that demonstrates the powerful synergy between ABP Framework and `Quartz.NET`. This isn't just a tutorial example - it's a robust, enterprise-grade solution that handles real business requirements.
### What We Accomplished
**✅ Enterprise-Grade Reliability**: PostgreSQL persistence ensures jobs survive restarts and deployments
**✅ ABP Best Practices**: Used `QuartzBackgroundWorkerBase`, `ICachedServiceProvider`, and ABP's logging infrastructure
**✅ Real Business Value**: Automated subscription reminders with duplicate prevention and audit logging
**✅ Flexible Scheduling**: Explored cron expressions, trigger types, and misfire handling strategies
### The Power of ABP + Quartz Integration
The combination delivers exceptional value through automatic job discovery, persistent scheduling, built-in dependency injection, and seamless framework integration. You get enterprise reliability with developer-friendly simplicity.
### Final Thoughts
Complex background processing doesn't have to be complicated to implement. ABP's thoughtful abstractions combined with Quartz's proven engine create a development experience that's both powerful and enjoyable.
Whether you're building subscription management, financial reporting, or data synchronization, these patterns provide a solid foundation for reliable, maintainable solutions.
**Happy coding, and may your background jobs never miss a beat!** 🚀

5
docs/en/framework/architecture/multi-tenancy/index.md

@ -42,6 +42,8 @@ ABP supports all the following approaches to store the tenant data in the databa
[Saas module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches.
> You can see the community article *[Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)* for more details about different database architectures with practical implementation details.
## Usage
Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible.
@ -456,8 +458,9 @@ The [Tenant Management module](../../../modules/tenant-management.md) provides a
### A note about separate database per tenant approach in open source version
While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [Saas module (PRO)](../../../modules/saas.md).
Alternatively you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly.
Alternatively, you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly.
## See Also
* [Features](../../infrastructure/features.md)
* [Article: Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)

16
docs/en/framework/fundamentals/object-extensions.md

@ -394,6 +394,22 @@ public class MyProfile : Profile
It has the same parameters with the `MapExtraPropertiesTo` method.
#### Mapperly Integration
If you're using the [Mapperly](https://github.com/riok/mapperly) library, the ABP also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above.
You can use the `MapExtraProperties` attribute to Mapperly class:
````csharp
[Mapper]
[MapExtraProperties]
public partial class IdentityUserToProfileDtoMapper : MapperBase<IdentityUser, IdentityUserDto>
{
public override partial IdentityUserDto Map(IdentityUser source);
public override partial void Map(IdentityUser source, IdentityUserDto destination);
}
````
## Entity Framework Core Database Mapping
If you're using the EF Core, you can map an extra property to a table field in the database. Example:

129
docs/en/framework/infrastructure/object-to-object-mapping.md

@ -84,7 +84,7 @@ public class UserAppService : ApplicationService
}
````
You should have defined the mappings before to be able to map objects. See the AutoMapper integration section to learn how to define mappings.
You should have defined the mappings before to be able to map objects. See the AutoMapper/Mapperly integration section to learn how to define mappings.
## AutoMapper Integration
@ -217,13 +217,120 @@ public class MyProfile : Profile
}
````
## Mapperly Integration
[Mapperly](https://github.com/riok/mapperly) is a .NET source generator for generating object mappings. [Volo.Abp.Mapperly](https://www.nuget.org/packages/Volo.Abp.Mapperly) package defines the Mapperly integration for the `IObjectMapper`.
Once you define mappings class as below, you can use the `IObjectMapper` interface just like explained before.
### Define Mapping Classes
You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process:
````csharp
[Mapper]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
````
If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase<User, UserDto>` class:
````csharp
[Mapper]
public partial class UserToUserDtoMapper : TwoWayMapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
public override partial User ReverseMap(UserDto destination);
public override partial void ReverseMap(UserDto destination, User source);
}
````
### Before and After Mapping Methods
The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping:
````csharp
[Mapper]
public partial class UserToUserDtoMapper : TwoWayMapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
public override partial void BeforeMap(User source)
{
//TODO: Perform actions before the mapping
}
public override partial void AfterMap(User source, UserDto destination)
{
//TODO: Perform actions after the mapping
}
public override partial User ReverseMap(UserDto destination);
public override partial void ReverseMap(UserDto destination, User source);
public override partial void BeforeReverseMap(UserDto destination)
{
//TODO: Perform actions before the reverse mapping
}
public override partial void AfterReverseMap(UserDto destination, User source)
{
//TODO: Perform actions after the reverse mapping
}
}
````
### Mapping the Object Extensions
[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects:
````csharp
[Mapper]
[MapExtraProperties]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
````
It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more.
### Lists and Arrays Support
ABP Mapperly integration also supports mapping lists and arrays as explained in the [IObjectMapper<TSource, TDestination> Interface](#iobjectmappertsource-tdestination-interface) section.
**Example**:
````csharp
[Mapper]
public partial class UserToUserDtoMapper : MapperBase<User, UserDto>
{
public override partial UserDto Map(User source);
public override partial void Map(User source, UserDto destination);
}
var users = await _userRepository.GetListAsync(); // returns List<User>
var dtos = ObjectMapper.Map<List<User>, List<UserDto>>(users); // creates List<UserDto>
````
### More Mapperly Features
Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details.
## Advanced Topics
### IObjectMapper<TContext> Interface
Assume that you have created a **reusable module** which defines AutoMapper profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md).
Assume that you have created a **reusable module** which defines AutoMapper/Mapperly profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md).
`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper even if the final application uses another default object mapping library.
`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper/Mapperly library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper/Mapperly even if the final application uses another default object mapping library.
`IObjectMapper<TContext>` is used to contextualize the object mapper, so you can use different libraries for different modules/contexts.
@ -281,6 +388,8 @@ public class UserAppService : ApplicationService
While using the contextualized object mapper is same as the normal object mapper, you should register the contextualized mapper in your module's `ConfigureServices` method:
When using AutoMapper:
````csharp
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
@ -298,6 +407,20 @@ public class MyModule : AbpModule
}
````
When using Mapperly:
````csharp
[DependsOn(typeof(AbpMapperlyModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//Use Mapperly for MyModule
context.Services.AddMapperlyObjectMapper<MyModule>();
}
}
````
`IObjectMapper<MyModule>` is an essential feature for a reusable module where it can be used in multiple applications each may use a different library for object to object mapping. All pre-built ABP modules are using it. But, for the final application, you can ignore this interface and always use the default `IObjectMapper` interface.
### IObjectMapper<TSource, TDestination> Interface

184
docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md

@ -0,0 +1,184 @@
# Migrating from AutoMapper to Mapperly
## Introduction
The AutoMapper library is **no longer free for commercial use**. For more details, you can refer to [this announcement post](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/).
ABP Framework provides both AutoMapper and Mapperly integrations. If your project currently uses AutoMapper and you don't have a commercial license, you can switch to Mapperly by following the steps outlined below.
## Migration Steps
Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations:
1. Replace `Volo.Abp.AutoMapper` with `Volo.Abp.Mapperly` in all `*.csproj` files.
2. Replace `using Volo.Abp.AutoMapper;` with `using Volo.Abp.Mapperly;` in all `*.cs` files.
3. Replace `AbpAutoMapperModule` with `AbpMapperlyModule` in all `*.cs` files.
4. Replace `AddAutoMapperObjectMapper` with `AddMapperlyObjectMapper` in all `*.cs` files.
5. Remove any code sections that configure `Configure<AbpAutoMapperOptions>`.
6. Review any existing AutoMapper `Profile` classes and ensure all newly created Mapperly mapping classes are registered appropriately. (You can refer to the example below for guidance)
**Example:**
Here is an AutoMapper's `Profile` class:
```csharp
public class ExampleAutoMapper : Profile
{
public AbpIdentityApplicationModuleAutoMapperProfile()
{
CreateMap<IdentityUser, IdentityUserDto>()
.MapExtraProperties()
.Ignore(x => x.IsLockedOut)
.Ignore(x => x.SupportTwoFactor)
.Ignore(x => x.RoleNames);
CreateMap<IdentityUserClaim, IdentityUserClaimDto>();
CreateMap<OrganizationUnit, OrganizationUnitDto>()
.MapExtraProperties();
CreateMap<OrganizationUnitRole, OrganizationUnitRoleDto>()
.ReverseMap();
CreateMap<IdentityRole, OrganizationUnitRoleDto>()
.ForMember(dest => dest.RoleId, src => src.MapFrom(r => r.Id));
CreateMap<IdentityUser, IdentityUserExportDto>()
.ForMember(dest => dest.Active, src => src.MapFrom(r => r.IsActive ? "Yes" : "No"))
.ForMember(dest => dest.EmailConfirmed, src => src.MapFrom(r => r.EmailConfirmed ? "Yes" : "No"))
.ForMember(dest => dest.TwoFactorEnabled, src => src.MapFrom(r => r.TwoFactorEnabled ? "Yes" : "No"))
.ForMember(dest => dest.AccountLookout, src => src.MapFrom(r => r.LockoutEnd != null && r.LockoutEnd > DateTime.UtcNow ? "Yes" : "No"))
.Ignore(x => x.Roles);
}
}
```
And the Mapperly mapping class:
```csharp
[Mapper]
[MapExtraProperties]
public partial class IdentityUserToIdentityUserDtoMapper : MapperBase<IdentityUser, IdentityUserDto>
{
[MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))]
[MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))]
[MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))]
public override partial IdentityUserDto Map(IdentityUser source);
[MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))]
[MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))]
[MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))]
public override partial void Map(IdentityUser source, IdentityUserDto destination);
}
[Mapper]
public partial class IdentityUserClaimToIdentityUserClaimDtoMapper : MapperBase<IdentityUserClaim, IdentityUserClaimDto>
{
public override partial IdentityUserClaimDto Map(IdentityUserClaim source);
public override partial void Map(IdentityUserClaim source, IdentityUserClaimDto destination);
}
[Mapper]
[MapExtraProperties]
public partial class OrganizationUnitToOrganizationUnitDtoMapper : MapperBase<OrganizationUnit, OrganizationUnitDto>
{
public override partial OrganizationUnitDto Map(OrganizationUnit source);
public override partial void Map(OrganizationUnit source, OrganizationUnitDto destination);
}
[Mapper]
public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase<OrganizationUnitRole, OrganizationUnitRoleDto>
{
public override partial OrganizationUnitRoleDto Map(OrganizationUnitRole source);
public override partial void Map(OrganizationUnitRole source, OrganizationUnitRoleDto destination);
public override partial OrganizationUnitRole ReverseMap(OrganizationUnitRoleDto destination);
public override partial void ReverseMap(OrganizationUnitRoleDto destination, OrganizationUnitRole source);
}
[Mapper]
[MapExtraProperties]
public partial class OrganizationUnitToOrganizationUnitWithDetailsDtoMapper : MapperBase<OrganizationUnit, OrganizationUnitWithDetailsDto>
{
[MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))]
[MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))]
public override partial OrganizationUnitWithDetailsDto Map(OrganizationUnit source);
[MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))]
[MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))]
public override partial void Map(OrganizationUnit source, OrganizationUnitWithDetailsDto destination);
}
[Mapper]
public partial class IdentityRoleToOrganizationUnitRoleDtoMapper : MapperBase<IdentityRole, OrganizationUnitRoleDto>
{
[MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))]
public override partial OrganizationUnitRoleDto Map(IdentityRole source);
[MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))]
public override partial void Map(IdentityRole source, OrganizationUnitRoleDto destination);
}
[Mapper]
public partial class IdentityUserToIdentityUserExportDtoMapper : MapperBase<IdentityUser, IdentityUserExportDto>
{
[MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))]
public override partial IdentityUserExportDto Map(IdentityUser source);
[MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))]
public override partial void Map(IdentityUser source, IdentityUserExportDto destination);
public override void AfterMap(IdentityUser source, IdentityUserExportDto destination)
{
destination.Active = source.IsActive ? "Yes" : "No";
destination.EmailConfirmed = source.EmailConfirmed ? "Yes" : "No";
destination.TwoFactorEnabled = source.TwoFactorEnabled ? "Yes" : "No";
destination.AccountLookout = source.LockoutEnd != null && source.LockoutEnd > DateTime.UtcNow ? "Yes" : "No";
}
}
```
## Mapperly Mapping Class
To use Mapperly, you'll need to create a dedicated mapping class for each source and destination types.
* Use the `[Mapper]` attribute to designate the class as a Mapperly mapper.
* Replace AutoMapper's `Ignore()` method with the `[MapperIgnoreTarget]` attribute.
* Replace the `MapExtraProperties()` method with the `[MapExtraProperties]` attribute.
* Use the `TwoWayMapperBase` class as an alternative to AutoMapper’s `ReverseMap()` functionality.
* Implement the `AfterMap()` method to execute logic after the mapping is completed.
### Dependency Injection in Mapper Class
All Mapperly mapping classes automatically registered in the the [dependency injection (DI)](../../framework/fundamentals/dependency-injection.md) container. To use a service within a Mapper class, simply add it to the constructor, Mapperly will inject it automatically.
**Example:**
```csharp
public partial class IdentityUserToIdentityUserDtoMapper : MapperBase<IdentityUser, IdentityUserDto>
{
public IdentityUserToIdentityUserDtoMapper(MyService myService)
{
_myService = myService;
}
public override partial IdentityUserDto Map(IdentityUser source);
public override partial void Map(IdentityUser source, IdentityUserDto destination);
public override void AfterMap(IdentityUser source, IdentityUserDto destination)
{
destination.MyProperty = _myService.GetMyProperty(source.MyProperty);
}
}
```
## Mapperly Documentation
Please refer to the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details.
**Key points:**
- [Mapperly Configuration](https://mapperly.riok.app/docs/configuration/mapper/)
- [Mapperly Enums](https://mapperly.riok.app/docs/configuration/enum/)
- [Mapperly Flattening and unflattening](https://mapperly.riok.app/docs/configuration/flattening/)

27
docs/en/studio/release-notes.md

@ -2,6 +2,33 @@
This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version.
## 1.1.2 (2025-07-31)
* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.3`)
* Configured LeptonX Lite logos in the templates.
* Added browser tab memory feature to remember previously selected tabs.
* Enhanced tools section with default credentials display for first-time tool usage.
* Improved module and package loading with better error handling.
## 1.1.1 (2025-07-22)
* Enhanced tools section with clear cookies option.
* Fixed language management module name display for imported modules.
* Improved update window messaging with "Skip this version" option.
* Fixed Docker Compose file issues in microservice template.
* Resolved RabbitMQ tool cookie problems.
## 1.1.0 (2025-07-16)
* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.2`)
* Enhanced UI scaling for all windows and improved user experience.
* Added tools section in solution runner main area with basic Grafana dashboard for microservice template.
* Improved container management during application building.
* Enhanced background task exception handling.
* Added public account module package reference to Blazor WebApp client.
* Fixed tenant database context updating errors.
* Improved optional module selection UI with better documentation integration.
## 1.0.2 (2025-06-24)
* Enhanced the ABP NuGet package installation experience.

2
docs/en/studio/version-mapping.md

@ -4,6 +4,8 @@ This document provides a general overview of the relationship between various ve
| **ABP Studio Version** | **ABP Version of Startup Template** |
|------------------------|---------------------------|
| 1.1.2 | 9.2.3 |
| 1.1.0 - 1.1.1 | 9.2.2 |
| 1.0.2 | 9.2.1 |
| 1.0.1 | 9.2.0 |
| 1.0.0 | 9.2.0 |

2
docs/en/tutorials/modular-crm/part-05.md

@ -167,7 +167,7 @@ The `ReplaceDbContext` attribute allows the use of the `ModularCrmDbContext` cla
````csharp
public class ModularCrmDbContext :
AbpDbContext<ModularCrmDbContext>,
IProductsDbContext,
ICatalogDbContext,
IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE
{
public DbSet<Product> Products { get; set; }

114
docs/en/tutorials/todo/layered/index.md

@ -171,13 +171,13 @@ This application has a single [entity](../../../framework/architecture/domain-dr
using System;
using Volo.Abp.Domain.Entities;
namespace TodoApp
namespace TodoApp;
public class TodoItem : BasicAggregateRoot<Guid>
{
public class TodoItem : BasicAggregateRoot<Guid>
{
public string Text { get; set; } = string.Empty;
}
public string Text { get; set; } = string.Empty;
}
````
`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here.
@ -281,15 +281,15 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace TodoApp
namespace TodoApp;
public interface ITodoAppService : IApplicationService
{
public interface ITodoAppService : IApplicationService
{
Task<List<TodoItemDto>> GetListAsync();
Task<TodoItemDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
Task<List<TodoItemDto>> GetListAsync();
Task<TodoItemDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
````
### Data Transfer Object
@ -299,14 +299,14 @@ namespace TodoApp
````csharp
using System;
namespace TodoApp
namespace TodoApp;
public class TodoItemDto
{
public class TodoItemDto
{
public Guid Id { get; set; }
public string Text { get; set; } = string.Empty;
}
public Guid Id { get; set; }
public string Text { get; set; } = string.Empty;
}
````
This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`.
@ -323,19 +323,18 @@ using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace TodoApp
namespace TodoApp;
public class TodoAppService : ApplicationService, ITodoAppService
{
public class TodoAppService : ApplicationService, ITodoAppService
{
private readonly IRepository<TodoItem, Guid> _todoItemRepository;
private readonly IRepository<TodoItem, Guid> _todoItemRepository;
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
// TODO: Implement the methods here...
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
// TODO: Implement the methods here...
}
````
@ -412,8 +411,8 @@ Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* proje
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Web.Pages
{
namespace TodoApp.Web.Pages;
public class IndexModel : TodoAppPageModel
{
public List<TodoItemDto> TodoItems { get; set; }
@ -430,7 +429,7 @@ namespace TodoApp.Web.Pages
TodoItems = await _todoAppService.GetListAsync();
}
}
}
````
This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page.
@ -578,34 +577,39 @@ using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Blazor.Pages
{{if UI=="Blazor" || UI=="BlazorWebApp"}}
namespace TodoApp.Blazor.Client.Pages;
{{else if UI=="BlazorServer"}}
namespace TodoApp.Blazor.Pages;
{{else if UI=="MAUIBlazor"}}
namespace TodoApp.MauiBlazor.Pages;
{{end}}
public partial class Index
{
public partial class Index
{
[Inject]
private ITodoAppService TodoAppService { get; set; }
[Inject]
private ITodoAppService TodoAppService { get; set; }
private List<TodoItemDto> TodoItems { get; set; } = new List<TodoItemDto>();
private string NewTodoText { get; set; } = string.Empty;
private List<TodoItemDto> TodoItems { get; set; } = new List<TodoItemDto>();
private string NewTodoText { get; set; } = string.Empty;
protected override async Task OnInitializedAsync()
{
TodoItems = await TodoAppService.GetListAsync();
}
private async Task Create()
{
var result = await TodoAppService.CreateAsync(NewTodoText);
TodoItems.Add(result);
NewTodoText = null;
}
protected override async Task OnInitializedAsync()
{
TodoItems = await TodoAppService.GetListAsync();
}
private async Task Create()
{
var result = await TodoAppService.CreateAsync(NewTodoText);
TodoItems.Add(result);
NewTodoText = null;
}
private async Task Delete(TodoItemDto todoItem)
{
await TodoAppService.DeleteAsync(todoItem.Id);
await Notify.Info("Deleted the todo item.");
TodoItems.Remove(todoItem);
}
private async Task Delete(TodoItemDto todoItem)
{
await TodoAppService.DeleteAsync(todoItem.Id);
await Notify.Info("Deleted the todo item.");
TodoItems.Remove(todoItem);
}
}
```

14
framework/Volo.Abp.sln

@ -491,6 +491,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Timing.Tests", "test\Volo.Abp.Timing.Tests\Volo.Abp.Timing.Tests.csproj", "{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly", "src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj", "{AF556046-54CD-48BC-9740-3E926DB8B510}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly.Tests", "test\Volo.Abp.Mapperly.Tests\Volo.Abp.Mapperly.Tests.csproj", "{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.MySQL.Pomelo", "src\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo.csproj", "{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}"
EndProject
Global
@ -1467,6 +1471,14 @@ Global
{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.Build.0 = Release|Any CPU
{AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.Build.0 = Release|Any CPU
{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.Build.0 = Release|Any CPU
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1718,6 +1730,8 @@ Global
{1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{AF556046-54CD-48BC-9740-3E926DB8B510} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

10
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs

@ -42,10 +42,10 @@ using Volo.Abp.AspNetCore.RazorViews;
#nullable disable
WriteLiteral("\n");
WriteLiteral("\n<html");
BeginWriteAttribute("lang", " lang=\"", 453, "\"", 512, 1);
BeginWriteAttribute("lang", " lang=\"", 453, "\"", 492, 1);
#nullable restore
#line 19 "MultiTenancyMiddlewareErrorPage.cshtml"
WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name), 460, 52, false);
WriteAttributeValue("", 460, CultureInfo.CurrentCulture.Name, 460, 32, false);
#line default
#line hidden
@ -54,7 +54,7 @@ WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)
WriteLiteral(">\n <head>\n <meta charset=\"utf-8\" />\n <title>");
#nullable restore
#line 22 "MultiTenancyMiddlewareErrorPage.cshtml"
Write(HtmlEncoder.Encode(Model.Message));
Write(Model.Message);
#line default
#line hidden
@ -62,7 +62,7 @@ WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)
WriteLiteral("</title>\n </head>\n <body>\n <h3>");
#nullable restore
#line 25 "MultiTenancyMiddlewareErrorPage.cshtml"
Write(HtmlEncoder.Encode(Model.Message));
Write(Model.Message);
#line default
#line hidden
@ -70,7 +70,7 @@ WriteAttributeValue("", 460, HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)
WriteLiteral("</h3>\n <p>");
#nullable restore
#line 26 "MultiTenancyMiddlewareErrorPage.cshtml"
Write(HtmlEncoder.Encode(Model.Details));
Write(Model.Details);
#line default
#line hidden

8
framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.cshtml

@ -16,13 +16,13 @@
public MultiTenancyMiddlewareErrorPageModel Model { get; set; }
}
<html lang="@HtmlEncoder.Encode(CultureInfo.CurrentCulture.Name)">
<html lang="@CultureInfo.CurrentCulture.Name">
<head>
<meta charset="utf-8" />
<title>@HtmlEncoder.Encode(Model.Message)</title>
<title>@Model.Message</title>
</head>
<body>
<h3>@HtmlEncoder.Encode(Model.Message)</h3>
<p>@HtmlEncoder.Encode(Model.Details)<p/>
<h3>@Model.Message</h3>
<p>@Model.Details<p/>
</body>
</html>

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs

@ -30,6 +30,8 @@ public class WebRequestResources : IWebRequestResources, IScopedDependency
}
Resources.Add(path, resources);
return resources;
// Create a new list as return value to avoid any changes to the list will affect the value in Resources dictionary.
return resources.ToList();
}
}

4
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs

@ -224,7 +224,7 @@ public class ClientProxyUrlBuilder : ITransientDependency
return true;
}
protected virtual Task<string> ConvertValueToStringAsync(object value)
protected virtual Task<string> ConvertValueToStringAsync(object? value)
{
if (value is DateTime dateTimeValue)
{
@ -236,6 +236,6 @@ public class ClientProxyUrlBuilder : ITransientDependency
return Task.FromResult(dateTimeValue.ToString("yyyy-MM-ddTHH:mm:ss.fffffff").TrimEnd('0').TrimEnd('.'));
}
return Task.FromResult(value.ToString()!);
return Task.FromResult(value?.ToString() ?? string.Empty);
}
}

22
framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs

@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Mapperly;
using Volo.Abp.ObjectMapping;
namespace Microsoft.Extensions.DependencyInjection;
public static class AbpAutoMapperServiceCollectionExtensions
{
public static IServiceCollection AddMapperlyObjectMapper(this IServiceCollection services)
{
return services.Replace(
ServiceDescriptor.Transient<IAutoObjectMappingProvider, MapperlyAutoObjectMappingProvider>()
);
}
public static IServiceCollection AddMapperlyObjectMapper<TContext>(this IServiceCollection services)
{
return services.Replace(
ServiceDescriptor.Transient<IAutoObjectMappingProvider<TContext>, MapperlyAutoObjectMappingProvider<TContext>>()
);
}
}

29
framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<AssemblyName>Volo.Abp.Mapperly</AssemblyName>
<PackageId>Volo.Abp.Mapperly</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Auditing\Volo.Abp.Auditing.csproj" />
<ProjectReference Include="..\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj" />
<ProjectReference Include="..\Volo.Abp.ObjectMapping\Volo.Abp.ObjectMapping.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Riok.Mapperly" />
</ItemGroup>
</Project>

27
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Mapperly;
public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar
{
protected override bool IsConventionalRegistrationDisabled(Type type)
{
return !type.GetInterfaces().Any(x => x.IsGenericType && typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition()) ||
base.IsConventionalRegistrationDisabled(type);
}
protected override List<Type> GetExposedServiceTypes(Type type)
{
var exposedServiceTypes = base.GetExposedServiceTypes(type);
var mapperlyInterfaces = type.GetInterfaces().Where(x =>
x.IsGenericType && (typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition() ||
typeof(IAbpReverseMapperlyMapper<,>) == x.GetGenericTypeDefinition()));
return exposedServiceTypes
.Union(mapperlyInterfaces)
.Distinct()
.ToList();
}
}

25
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs

@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Auditing;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.Mapperly;
[DependsOn(
typeof(AbpObjectMappingModule),
typeof(AbpObjectExtendingModule),
typeof(AbpAuditingModule)
)]
public class AbpMapperlyModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddConventionalRegistrar(new AbpMapperlyConventionalRegistrar());
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddMapperlyObjectMapper();
}
}

23
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs

@ -0,0 +1,23 @@
namespace Volo.Abp.Mapperly;
public interface IAbpMapperlyMapper<in TSource, TDestination>
{
TDestination Map(TSource source);
void Map(TSource source, TDestination destination);
void BeforeMap(TSource source);
void AfterMap(TSource source, TDestination destination);
}
public interface IAbpReverseMapperlyMapper<TSource, TDestination> : IAbpMapperlyMapper<TSource, TDestination>
{
TSource ReverseMap(TDestination destination);
void ReverseMap(TDestination destination, TSource source);
void BeforeReverseMap(TDestination destination);
void AfterReverseMap(TDestination destination, TSource source);
}

14
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs

@ -0,0 +1,14 @@
using System;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Mapperly;
[AttributeUsage(AttributeTargets.Class)]
public class MapExtraPropertiesAttribute : Attribute
{
public MappingPropertyDefinitionChecks DefinitionChecks { get; set; } = MappingPropertyDefinitionChecks.Null;
public string[]? IgnoredProperties { get; set; }
public bool MapToRegularProperties { get; set; }
}

32
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs

@ -0,0 +1,32 @@
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Mapperly;
public abstract class MapperBase<TSource, TDestination> : IAbpMapperlyMapper<TSource, TDestination>, ITransientDependency
{
public abstract TDestination Map(TSource source);
public abstract void Map(TSource source, TDestination destination);
public virtual void BeforeMap(TSource source)
{
}
public virtual void AfterMap(TSource source, TDestination destination)
{
}
}
public abstract class TwoWayMapperBase<TSource, TDestination> : MapperBase<TSource, TDestination>, IAbpReverseMapperlyMapper<TSource, TDestination>
{
public abstract TSource ReverseMap(TDestination destination);
public abstract void ReverseMap(TDestination destination, TSource source);
public virtual void BeforeReverseMap(TDestination destination)
{
}
public virtual void AfterReverseMap(TDestination destination, TSource source)
{
}
}

281
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs

@ -0,0 +1,281 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Reflection;
namespace Volo.Abp.Mapperly;
public class MapperlyAutoObjectMappingProvider<TContext> : MapperlyAutoObjectMappingProvider, IAutoObjectMappingProvider<TContext>
{
public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
}
public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
{
protected static readonly ConcurrentDictionary<string, Func<object, object, object, object?>> MapCache = new();
protected IServiceProvider ServiceProvider { get; }
public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public virtual TDestination Map<TSource, TDestination>(object source)
{
if (TryToMapCollection<TSource, TDestination>((TSource)source, default, out var collectionResult))
{
return collectionResult;
}
var mapper = ServiceProvider.GetService<IAbpMapperlyMapper<TSource, TDestination>>();
if (mapper != null)
{
mapper.BeforeMap((TSource)source);
var destination = mapper.Map((TSource)source);
TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull<MapExtraPropertiesAttribute>(), (TSource)source, destination, new ExtraPropertyDictionary());
mapper.AfterMap((TSource)source, destination);
return destination;
}
var reverseMapper = ServiceProvider.GetService<IAbpReverseMapperlyMapper<TDestination, TSource>>();
if (reverseMapper != null)
{
reverseMapper.BeforeReverseMap((TSource)source);
var destination = reverseMapper.ReverseMap((TSource)source);
TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull<MapExtraPropertiesAttribute>(), (TSource)source, destination, GetExtraProperties(destination));
reverseMapper.AfterReverseMap((TSource)source, destination);
return destination;
}
throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperlyMapper<TSource, TDestination>))} or" +
$" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper<TSource, TDestination>))} was found");
}
public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
{
if (TryToMapCollection<TSource, TDestination>(source, destination, out var collectionResult))
{
return collectionResult;
}
var mapper = ServiceProvider.GetService<IAbpMapperlyMapper<TSource, TDestination>>();
if (mapper != null)
{
mapper.BeforeMap(source);
var destinationExtraProperties = GetExtraProperties(destination);
mapper.Map(source, destination);
TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull<MapExtraPropertiesAttribute>(), source, destination, destinationExtraProperties);
mapper.AfterMap(source, destination);
return destination;
}
var reverseMapper = ServiceProvider.GetService<IAbpReverseMapperlyMapper<TDestination, TSource>>();
if (reverseMapper != null)
{
reverseMapper.BeforeReverseMap(source);
var destinationExtraProperties = GetExtraProperties(destination);
reverseMapper.ReverseMap(source, destination);
TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull<MapExtraPropertiesAttribute>(), source, destination, destinationExtraProperties);
reverseMapper.AfterReverseMap(source, destination);
return destination;
}
throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperlyMapper<TSource, TDestination>))} or" +
$" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper<TSource, TDestination>))} was found");
}
protected virtual bool TryToMapCollection<TSource, TDestination>(TSource source, TDestination? destination, out TDestination collectionResult)
{
if (!ObjectMappingHelper.IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
{
collectionResult = default!;
return false;
}
var mapperType = typeof(IAbpMapperlyMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType);
var mapper = ServiceProvider.GetService(mapperType);
if (mapper == null)
{
mapperType = typeof(IAbpReverseMapperlyMapper<,>).MakeGenericType(destinationArgumentType, sourceArgumentType);
mapper = ServiceProvider.GetService(mapperType);
if (mapper == null)
{
//skip, no specific mapper
collectionResult = default!;
return false;
}
}
var invoker = MapCache.GetOrAdd(
$"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}",
_ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null));
var sourceList = source!.As<IList>();
var result = definitionGenericType.IsGenericType
? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As<IList>()
: Array.CreateInstance(destinationArgumentType, sourceList.Count);
if (destination != null && !destination.GetType().IsArray)
{
//Clear destination collection if destination not an array, We won't change array just same behavior as AutoMapper.
destination.As<IList>().Clear();
}
for (var i = 0; i < sourceList.Count; i++)
{
var invokeResult = destination == null
? invoker(this, sourceList[i]!, null!)
: invoker(this, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!);
if (definitionGenericType.IsGenericType)
{
result.Add(invokeResult);
destination?.As<IList>().Add(invokeResult);
}
else
{
result[i] = invokeResult;
}
}
if (destination != null && destination.GetType().IsArray)
{
//Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper.
collectionResult = (TDestination)result;
return true;
}
//Return the destination if destination exists. The parameter reference equals with return object.
collectionResult = destination ?? (TDestination)result;
return true;
}
protected virtual Func<object, object, object, object?> CreateMapDelegate(
Type mapperType,
Type sourceArgumentType,
Type destinationArgumentType,
bool hasDestination)
{
var methods = typeof(MapperlyAutoObjectMappingProvider)
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(Map))
.Where(x =>
{
var parameters = x.GetParameters();
return (hasDestination || parameters.Length == 1) &&
(!hasDestination || parameters.Length == 2);
})
.ToList();
if (methods.Count == 0)
{
throw new AbpException($"Could not find a method named '{nameof(Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (methods.Count > 1)
{
throw new AbpException($"Found more than one method named '{nameof(Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
var method = methods[0].MakeGenericMethod(sourceArgumentType, destinationArgumentType);
var instanceParam = Expression.Parameter(typeof(object), "mapper");
var sourceParam = Expression.Parameter(typeof(object), "source");
var destinationParam = Expression.Parameter(typeof(object), "destination");
var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!);
var callParams = new List<Expression>
{
Expression.Convert(sourceParam, sourceArgumentType)
};
if (hasDestination)
{
callParams.Add(Expression.Convert(destinationParam, destinationArgumentType));
}
var call = Expression.Call(instanceCast, method, callParams);
var callConvert = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<object, object, object, object?>>(callConvert, instanceParam, sourceParam, destinationParam).Compile();
}
protected virtual ExtraPropertyDictionary GetExtraProperties<TDestination>(TDestination destination)
{
var extraProperties = new ExtraPropertyDictionary();
if (destination is not IHasExtraProperties hasExtraProperties)
{
return extraProperties;
}
foreach (var property in hasExtraProperties.ExtraProperties)
{
extraProperties.Add(property.Key, property.Value);
}
return extraProperties;
}
protected virtual void TryMapExtraProperties<TSource, TDestination>(MapExtraPropertiesAttribute? mapExtraPropertiesAttribute, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty)
{
if (mapExtraPropertiesAttribute != null &&
typeof(IHasExtraProperties).IsAssignableFrom(typeof(TDestination)) &&
typeof(IHasExtraProperties).IsAssignableFrom(typeof(TSource)))
{
MapExtraProperties<TSource, TDestination>(
source!.As<IHasExtraProperties>(),
destination!.As<IHasExtraProperties>(),
destinationExtraProperty,
mapExtraPropertiesAttribute.DefinitionChecks,
mapExtraPropertiesAttribute.IgnoredProperties,
mapExtraPropertiesAttribute.MapToRegularProperties
);
}
}
protected virtual void MapExtraProperties<TSource, TDestination>(
IHasExtraProperties source,
IHasExtraProperties destination,
ExtraPropertyDictionary destinationExtraProperty,
MappingPropertyDefinitionChecks? definitionChecks = null,
string[]? ignoredProperties = null,
bool mapToRegularProperties = false)
{
var result = destinationExtraProperty.IsNullOrEmpty()
? new Dictionary<string, object?>()
: new Dictionary<string, object?>(destinationExtraProperty);
if (source.ExtraProperties != null && destination.ExtraProperties != null)
{
ExtensibleObjectMapper
.MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
source.ExtraProperties,
result,
definitionChecks,
ignoredProperties
);
}
ObjectHelper.TrySetProperty(destination, x => x.ExtraProperties, () => new ExtraPropertyDictionary(result));
if (mapToRegularProperties)
{
destination.SetExtraPropertiesToRegularProperties();
}
}
}

2
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs

@ -188,7 +188,7 @@ public static class ExtensibleObjectMapper
return false;
}
if (definitionChecks != null)
if (definitionChecks != null && definitionChecks.Value != MappingPropertyDefinitionChecks.Null)
{
if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Source))
{

11
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs

@ -5,20 +5,25 @@ namespace Volo.Abp.ObjectExtending;
[Flags]
public enum MappingPropertyDefinitionChecks : byte
{
/// <summary>
/// Same as Null, We need to use this in Attribute to avoid null checks.
/// </summary>
Null = 0,
/// <summary>
/// No check. Copy all extra properties from the source to the destination.
/// </summary>
None = 0,
None = 1 << 0,
/// <summary>
/// Copy the extra properties defined for the source class.
/// </summary>
Source = 1,
Source = 1 << 1,
/// <summary>
/// Copy the extra properties defined for the destination class.
/// </summary>
Destination = 2,
Destination = 1 << 2,
/// <summary>
/// Copy extra properties defined for both of the source and destination classes.

135
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Volo.Abp.DependencyInjection;
@ -25,7 +26,7 @@ public class DefaultObjectMapper<TContext> : DefaultObjectMapper, IObjectMapper<
public class DefaultObjectMapper : IObjectMapper, ITransientDependency
{
protected static ConcurrentDictionary<string, MethodInfo> MethodInfoCache { get; } = new ConcurrentDictionary<string, MethodInfo>();
protected static readonly ConcurrentDictionary<string, Func<object, object, object, object?>> MapCache = new();
public IAutoObjectMappingProvider AutoObjectMappingProvider { get; }
protected IServiceProvider ServiceProvider { get; }
@ -122,7 +123,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
protected virtual bool TryToMapCollection<TSource, TDestination>(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult)
{
if (!IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
if (!ObjectMappingHelper.IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
{
collectionResult = default!;
return false;
@ -137,46 +138,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
return false;
}
var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}";
var method = MethodInfoCache.GetOrAdd(
cacheKey,
_ =>
{
var methods = specificMapper
.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(IObjectMapper<object, object>.Map))
.Where(x =>
{
var parameters = x.GetParameters();
if (destination == null && parameters.Length != 1 ||
destination != null && parameters.Length != 2 ||
parameters[0].ParameterType != sourceArgumentType)
{
return false;
}
return destination == null || parameters[1].ParameterType == destinationArgumentType;
})
.ToList();
if (methods.IsNullOrEmpty())
{
throw new AbpException($"Could not find a method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" +
$" in the type '{mapperType}'.");
}
if (methods.Count > 1)
{
throw new AbpException($"Found more than one method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" +
$" in the type '{mapperType}'.");
}
return methods.First();
}
);
var invoker = MapCache.GetOrAdd(
$"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}",
_ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null));
var sourceList = source!.As<IList>();
var result = definitionGenericType.IsGenericType
@ -192,8 +156,8 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
for (var i = 0; i < sourceList.Count; i++)
{
var invokeResult = destination == null
? method.Invoke(specificMapper, new [] { sourceList[i] })!
: method.Invoke(specificMapper, new [] { sourceList[i], Activator.CreateInstance(destinationArgumentType)! })!;
? invoker(specificMapper, sourceList[i]!, null!)
: invoker(specificMapper, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!);
if (definitionGenericType.IsGenericType)
{
@ -218,61 +182,64 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
return true;
}
protected virtual bool IsCollectionGenericType<TSource, TDestination>(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType)
protected virtual Func<object, object, object, object?> CreateMapDelegate(
Type mapperType,
Type sourceArgumentType,
Type destinationArgumentType,
bool hasDestination)
{
sourceArgumentType = default!;
destinationArgumentType = default!;
definitionGenericType = default!;
if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) ||
(!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray))
{
return false;
}
var methods = mapperType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(IObjectMapper<object, object>.Map))
.Where(x =>
{
var parameters = x.GetParameters();
if (!hasDestination && parameters.Length != 1 ||
hasDestination && parameters.Length != 2 ||
parameters[0].ParameterType != sourceArgumentType)
{
return false;
}
var supportedCollectionTypes = new[]
{
typeof(IEnumerable<>),
typeof(ICollection<>),
typeof(Collection<>),
typeof(IList<>),
typeof(List<>)
};
return !hasDestination || parameters[1].ParameterType == destinationArgumentType;
})
.ToList();
if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition()))
if (methods.Count == 0)
{
sourceArgumentType = typeof(TSource).GenericTypeArguments[0];
throw new AbpException($"Could not find a method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (typeof(TSource).IsArray)
if (methods.Count > 1)
{
sourceArgumentType = typeof(TSource).GetElementType()!;
throw new AbpException($"Found more than one method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (sourceArgumentType == default!)
{
return false;
}
var method = methods[0];
definitionGenericType = typeof(List<>);
if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition()))
{
destinationArgumentType = typeof(TDestination).GenericTypeArguments[0];
var instanceParam = Expression.Parameter(typeof(object), "mapper");
var sourceParam = Expression.Parameter(typeof(object), "source");
var destinationParam = Expression.Parameter(typeof(object), "destination");
if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) ||
typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>))
{
definitionGenericType = typeof(Collection<>);
}
}
var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!);
var callParams = new List<Expression>
{
Expression.Convert(sourceParam, sourceArgumentType)
};
if (typeof(TDestination).IsArray)
if (hasDestination)
{
destinationArgumentType = typeof(TDestination).GetElementType()!;
definitionGenericType = typeof(Array);
callParams.Add(Expression.Convert(destinationParam, destinationArgumentType));
}
return destinationArgumentType != default!;
var call = Expression.Call(instanceCast, method, callParams);
var callConvert = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<object, object, object, object?>>(callConvert, instanceParam, sourceParam, destinationParam).Compile();
}
protected virtual TDestination AutoMap<TSource, TDestination>(object source)

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

@ -0,0 +1,81 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Volo.Abp.ObjectMapping;
public static class ObjectMappingHelper
{
private static readonly ConcurrentDictionary<(Type, Type), (Type sourceArgumentType, Type destinationArgumentType, Type definitionGenericType)?> Cache = new();
public static bool IsCollectionGenericType<TSource, TDestination>(
out Type sourceArgumentType,
out Type destinationArgumentType,
out Type definitionGenericType)
{
var cached = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), _ => IsCollectionGenericTypeInternal<TSource, TDestination>());
if (cached == null)
{
sourceArgumentType = destinationArgumentType = definitionGenericType = null!;
return false;
}
(sourceArgumentType, destinationArgumentType, definitionGenericType) = cached.Value;
return true;
}
private static (Type, Type, Type)? IsCollectionGenericTypeInternal<TSource, TDestination>()
{
if (!IsCollectionGenericTypeInternal(typeof(TSource), out var sourceArgumentType, out _) ||
!IsCollectionGenericTypeInternal(typeof(TDestination), out var destinationArgumentType, out var definitionGenericType))
{
return null;
}
return (sourceArgumentType, destinationArgumentType, definitionGenericType);
}
private static bool IsCollectionGenericTypeInternal(Type type, out Type elementType, out Type definitionGenericType)
{
var supportedCollectionTypes = new[]
{
typeof(IEnumerable<>),
typeof(ICollection<>),
typeof(Collection<>),
typeof(IList<>),
typeof(List<>)
};
if (type.IsArray)
{
elementType = type.GetElementType()!;
definitionGenericType = type;
return true;
}
if (type.IsGenericType &&
supportedCollectionTypes.Contains(type.GetGenericTypeDefinition()) ||
type.GetInterfaces().Any(i => i.IsGenericType && supportedCollectionTypes.Contains(i.GetGenericTypeDefinition())))
{
elementType = type.GetGenericArguments()[0];
definitionGenericType = type.GetGenericTypeDefinition();
if (definitionGenericType == typeof(IEnumerable<>) ||
definitionGenericType == typeof(IList<>))
{
definitionGenericType = typeof(List<>);
}
if (definitionGenericType == typeof(ICollection<>))
{
definitionGenericType = typeof(Collection<>);
}
return true;
}
elementType = null!;
definitionGenericType = null!;
return false;
}
}

82
framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs

@ -0,0 +1,82 @@
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.Mapperly;
using Volo.Abp.ObjectExtending.TestObjects;
using Volo.Abp.Testing;
using Xunit;
namespace Mapperly;
public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly Volo.Abp.ObjectMapping.IObjectMapper _objectMapper;
public AbpAutoMapperExtensibleDtoExtensions_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<Volo.Abp.ObjectMapping.IObjectMapper>();
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default()
{
var person = new ExtensibleTestPerson()
.SetProperty("Name", "John")
.SetProperty("Age", 42)
.SetProperty("ChildCount", 2)
.SetProperty("Sex", "male")
.SetProperty("CityName", "Adana");
var personDto = new ExtensibleTestPersonDto()
.SetProperty("ExistingDtoProperty", "existing-value");
_objectMapper.Map(person, personDto);
personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
personDto.GetProperty<int>("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor
personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor
personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
}
[Fact]
public void MapExtraProperties_Also_Should_Map_To_RegularProperties()
{
var person = new ExtensibleTestPerson()
.SetProperty("Name", "John")
.SetProperty("Age", 42);
var personDto = new ExtensibleTestPersonWithRegularPropertiesDto()
.SetProperty("IsActive", true);
_objectMapper.Map(person, personDto);
//Defined in both classes
personDto.HasProperty("Name").ShouldBe(false);
personDto.Name.ShouldBe("John");
//Defined in both classes
personDto.HasProperty("Age").ShouldBe(false);
personDto.Age.ShouldBe(42);
//Should not clear existing values
personDto.HasProperty("IsActive").ShouldBe(false);
personDto.IsActive.ShouldBe(true);
}
[Fact(Skip = "Mapperly requires IHasExtraProperties.ExtraPropertyDictionary to be marked as nullable")]
public void MapExtraPropertiesTo_Should_Ignored_If_ExtraProperties_Is_Null()
{
var person = new ExtensibleTestPerson();
person.SetExtraPropertiesAsNull();
var personDto = new ExtensibleTestPersonDto();
personDto.SetExtraPropertiesAsNull();
Should.NotThrow(() => _objectMapper.Map(person, personDto));
person.ExtraProperties.ShouldBe(null);
personDto.ExtraProperties.ShouldBeEmpty();
}
}

17
framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.test.props" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AssemblyName>Volo.Abp.Mapperly.Tests</AssemblyName>
<PackageId>Volo.Abp.Mapperly.Tests</PackageId>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
<ProjectReference Include="..\..\test\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>
</Project>

59
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs

@ -0,0 +1,59 @@
using Microsoft.Extensions.DependencyInjection;
using Riok.Mapperly.Abstractions;
using Shouldly;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Mapperly;
public class MyClass
{
public string Id { get; set; }
public string Name { get; set; }
}
public class MyClassDto
{
public string Id { get; set; }
public string Name { get; set; }
}
[Mapper]
public partial class MyClassMapper : MapperBase<MyClass, MyClassDto>
{
public override partial MyClassDto Map(MyClass source);
public override partial void Map(MyClass source, MyClassDto destination);
public override void BeforeMap(MyClass source)
{
source.Name = "BeforeMap " + source.Name;
}
public override void AfterMap(MyClass source, MyClassDto destination)
{
destination.Name = source.Name + " AfterMap";
}
}
public class AbpMapperlyBeforeAndAfterMethod_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly IObjectMapper _objectMapper;
public AbpMapperlyBeforeAndAfterMethod_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<IObjectMapper>();
}
[Fact]
public void BeforeAndAfterMethods_Should_Be_Called_When_Mapping()
{
var myClass = new MyClass { Id = "1", Name = "Test" };
var myClassDto = _objectMapper.Map<MyClass, MyClassDto>(myClass);
myClassDto.Name.ShouldBe("BeforeMap Test AfterMap");
}
}

114
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Mapperly.SampleClasses;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Mapperly;
public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly IObjectMapper _objectMapper;
public AbpMapperlyModule_Basic_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<IObjectMapper>();
}
[Fact]
public void Should_Replace_IAutoObjectMappingProvider()
{
Assert.True(ServiceProvider.GetRequiredService<IAutoObjectMappingProvider>() is MapperlyAutoObjectMappingProvider);
}
[Fact]
public void Should_Map_Objects_With_AutoMap_Attributes()
{
var dto = _objectMapper.Map<MyEntity, MyEntityDto>(new MyEntity { Number = 42 });
dto.Number.ShouldBe(42);
}
[Fact]
public void Should_Map_Objects_With_Existing_Target_Object()
{
var dto = new MyEntityDto {Id = Guid.Empty, Number = 42};
_objectMapper.Map<MyEntity, MyEntityDto>(new MyEntity { Id = Guid.NewGuid(), Number = 43 }, dto);
dto.Number.ShouldBe(43);
dto.Id.ShouldNotBe(Guid.Empty);
}
[Fact]
public void Should_Map_Collection()
{
var dto = _objectMapper.Map<List<MyEntity>, List<MyEntityDto>>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
});
dto.Count.ShouldBe(2);
dto[0].Number.ShouldBe(42);
dto[1].Number.ShouldBe(43);
var dto2 = _objectMapper.Map<IReadOnlyList<MyEntity>, MyEntityDto[]>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
}.AsReadOnly());
dto2.Length.ShouldBe(2);
dto2[0].Number.ShouldBe(42);
dto2[1].Number.ShouldBe(43);
var dtoList = new List<MyEntityDto>();
{
new MyEntityDto() { Number = 44 };
new MyEntityDto() { Number = 45 };
}
_objectMapper.Map<List<MyEntity>, List<MyEntityDto>>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
}, dtoList);
dtoList.Count.ShouldBe(2);
dtoList[0].Number.ShouldBe(42);
dtoList[1].Number.ShouldBe(43);
var dtoArray = dtoList.ToArray();
_objectMapper.Map<IReadOnlyList<MyEntity>, MyEntityDto[]>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
}.AsReadOnly(), dtoArray);
dtoArray.Length.ShouldBe(2);
dtoArray[0].Number.ShouldBe(42);
dtoArray[1].Number.ShouldBe(43);
}
[Fact]
public void Should_Map_Enum()
{
var dto = _objectMapper.Map<MyEnum, MyEnumDto>(MyEnum.Value3);
dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3
}
[Fact]
public void Should_Throw_Exception_If_Mapper_Is_Not_Found()
{
var exception = Assert.Throws<AbpException>(() =>_objectMapper.Map<MyEntity, MyClassDto>(new MyEntity()));
exception.Message.ShouldBe("No " +
"Volo.Abp.Mapperly.IAbpMapperlyMapper<Volo.Abp.Mapperly.SampleClasses.MyEntity,Volo.Abp.Mapperly.MyClassDto> or " +
"Volo.Abp.Mapperly.IAbpReverseMapperlyMapper<Volo.Abp.Mapperly.SampleClasses.MyEntity,Volo.Abp.Mapperly.MyClassDto>" +
" was found");
}
}

191
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Mapperly.SampleClasses;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Mapperly;
public class AbpMapperlyModule_Specific_ObjectMapper_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly IObjectMapper _objectMapper;
public AbpMapperlyModule_Specific_ObjectMapper_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<IObjectMapper>();
}
[Fact]
public void Should_Use_Specific_Object_Mapper_If_Registered()
{
var dto = _objectMapper.Map<MyEntity, MyEntityDto2>(new MyEntity { Number = 42 });
dto.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
}
[Fact]
public void Specific_Object_Mapper_Should_Be_Used_For_Collections_If_Registered()
{
// IEnumerable
_objectMapper.Map<IEnumerable<MyEntity>, IEnumerable<MyEntityDto2>>(new List<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var destination = new List<MyEntityDto2>()
{
new MyEntityDto2 { Number = 44 }
};
var returnIEnumerable = _objectMapper.Map<IEnumerable<MyEntity>, IEnumerable<MyEntityDto2>>(
new List<MyEntity>()
{
new MyEntity { Number = 42 }
}, destination);
returnIEnumerable.First().Number.ShouldBe(43);
ReferenceEquals(destination, returnIEnumerable).ShouldBeTrue();
// ICollection
_objectMapper.Map<ICollection<MyEntity>, ICollection<MyEntityDto2>>(new List<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var returnICollection = _objectMapper.Map<ICollection<MyEntity>, ICollection<MyEntityDto2>>(
new List<MyEntity>()
{
new MyEntity { Number = 42 }
}, destination);
returnICollection.First().Number.ShouldBe(43);
ReferenceEquals(destination, returnICollection).ShouldBeTrue();
// Collection
_objectMapper.Map<Collection<MyEntity>, Collection<MyEntityDto2>>(new Collection<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var destination2 = new Collection<MyEntityDto2>()
{
new MyEntityDto2 { Number = 44 }
};
var returnCollection = _objectMapper.Map<Collection<MyEntity>, Collection<MyEntityDto2>>(
new Collection<MyEntity>()
{
new MyEntity { Number = 42 }
}, destination2);
returnCollection.First().Number.ShouldBe(43);
ReferenceEquals(destination2, returnCollection).ShouldBeTrue();
// IList
_objectMapper.Map<IList<MyEntity>, IList<MyEntityDto2>>(new List<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var returnIList = _objectMapper.Map<IList<MyEntity>, IList<MyEntityDto2>>(
new List<MyEntity>()
{
new MyEntity { Number = 42 }
}, destination);
returnIList.First().Number.ShouldBe(43);
ReferenceEquals(destination, returnIList).ShouldBeTrue();
// List
_objectMapper.Map<List<MyEntity>, List<MyEntityDto2>>(new List<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var returnList = _objectMapper.Map<List<MyEntity>, List<MyEntityDto2>>(
new List<MyEntity>()
{
new MyEntity { Number = 42 }
}, destination);
returnList.First().Number.ShouldBe(43);
ReferenceEquals(destination, returnList).ShouldBeTrue();
// Array
_objectMapper.Map<MyEntity[], MyEntityDto2[]>(new MyEntity[]
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var destinationArray = new MyEntityDto2[]
{
new MyEntityDto2 { Number = 40 }
};
var returnArray = _objectMapper.Map<MyEntity[], MyEntityDto2[]>(new MyEntity[]
{
new MyEntity { Number = 42 }
}, destinationArray);
returnArray.First().Number.ShouldBe(43);
// array should not be changed. Same as Mapperly.
destinationArray.First().Number.ShouldBe(40);
ReferenceEquals(returnArray, destinationArray).ShouldBeFalse();
}
[Fact]
public void Specific_Object_Mapper_Should_Support_Multiple_IObjectMapper_Interfaces()
{
var myEntityDto2 = _objectMapper.Map<MyEntity, MyEntityDto2>(new MyEntity { Number = 42 });
myEntityDto2.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
var myEntity = _objectMapper.Map<MyEntityDto2, MyEntity>(new MyEntityDto2 { Number = 42 });
myEntity.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
// IEnumerable
_objectMapper.Map<IEnumerable<MyEntity>, IEnumerable<MyEntityDto2>>(new List<MyEntity>()
{
new MyEntity { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
_objectMapper.Map<IEnumerable<MyEntityDto2>, IEnumerable<MyEntity>>(new List<MyEntityDto2>()
{
new MyEntityDto2 { Number = 42 }
}).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source.
}
[Fact]
public void Should_Use_Destination_Object_Constructor_If_Available()
{
var id = Guid.NewGuid();
var dto = _objectMapper.Map<MyEntity, MyEntityDtoWithMappingMethods>(new MyEntity { Number = 42, Id = id });
dto.Key.ShouldBe(id);
dto.No.ShouldBe(42);
}
[Fact]
public void Should_Use_Destination_Object_MapFrom_Method_If_Available()
{
var id = Guid.NewGuid();
var dto = new MyEntityDtoWithMappingMethods();
_objectMapper.Map(new MyEntity { Number = 42, Id = id }, dto);
dto.Key.ShouldBe(id);
dto.No.ShouldBe(42);
}
[Fact]
public void Should_Use_Source_Object_Method_If_Available_To_Create_New_Object()
{
var id = Guid.NewGuid();
var entity = _objectMapper.Map<MyEntityDtoWithMappingMethods, MyEntity>(new MyEntityDtoWithMappingMethods { Key = id, No = 42 });
entity.Id.ShouldBe(id);
entity.Number.ShouldBe(42);
}
[Fact]
public void Should_Use_Source_Object_Method_If_Available_To_Map_Existing_Object()
{
var id = Guid.NewGuid();
var entity = new MyEntity();
_objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }, entity);
entity.Id.ShouldBe(id);
entity.Number.ShouldBe(42);
}
}

68
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs

@ -0,0 +1,68 @@
using System;
using Riok.Mapperly.Abstractions;
using Shouldly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Mapperly;
public class MyDIClass
{
public string Id { get; set; }
public DateTime Birthday { get; set; }
}
public class MyDIClassDto
{
public string Id { get; set; }
public DateTime Birthday { get; set; }
}
public class BirthdayCalculatorService : ITransientDependency
{
public DateTime Birthday => DateTime.Parse("2025-01-01");
}
[Mapper]
public partial class MyDIClassMapper : MapperBase<MyDIClass, MyDIClassDto>
{
private readonly BirthdayCalculatorService _birthdayCalculatorService;
public MyDIClassMapper(BirthdayCalculatorService birthdayCalculatorService)
{
_birthdayCalculatorService = birthdayCalculatorService;
}
public override partial MyDIClassDto Map(MyDIClass source);
public override partial void Map(MyDIClass source, MyDIClassDto destination);
public override void AfterMap(MyDIClass source, MyDIClassDto destination)
{
destination.Birthday = _birthdayCalculatorService.Birthday;
}
}
public class AbpMapperly_Dependency_Injection_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly IObjectMapper _objectMapper;
private readonly BirthdayCalculatorService _birthdayCalculatorService;
public AbpMapperly_Dependency_Injection_Tests()
{
_objectMapper = GetRequiredService<IObjectMapper>();
_birthdayCalculatorService = GetRequiredService<BirthdayCalculatorService>();
}
[Fact]
public void DI_Test()
{
var myClass = new MyDIClass { Id = "1", Birthday = DateTime.Now };
var myClassDto = _objectMapper.Map<MyDIClass, MyDIClassDto>(myClass);
myClassDto.Birthday.ShouldBe(_birthdayCalculatorService.Birthday);
}
}

86
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs

@ -0,0 +1,86 @@
using Microsoft.Extensions.DependencyInjection;
using Riok.Mapperly.Abstractions;
using Shouldly;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Mapperly;
public class MyReverseClass
{
public string Id { get; set; }
public string Name { get; set; }
}
public class MyReverseClassDto
{
public string Id { get; set; }
public string Name { get; set; }
}
[Mapper]
public partial class MyReverseClassMapper : TwoWayMapperBase<MyReverseClass, MyReverseClassDto>
{
public override partial MyReverseClassDto Map(MyReverseClass source);
public override partial void Map(MyReverseClass source, MyReverseClassDto destination);
public override partial MyReverseClass ReverseMap(MyReverseClassDto destination);
public override partial void ReverseMap(MyReverseClassDto destination, MyReverseClass source);
public override void BeforeReverseMap(MyReverseClassDto destination)
{
destination.Name = "BeforeReverseMap " + destination.Name;
}
public override void AfterReverseMap(MyReverseClassDto destination, MyReverseClass source)
{
source.Name = destination.Name + " AfterReverseMap";
}
}
public class AbpReverseMapperly_Tests : AbpIntegratedTest<MapperlyTestModule>
{
private readonly IObjectMapper _objectMapper;
public AbpReverseMapperly_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<IObjectMapper>();
}
[Fact]
public void Map_Test()
{
var myClass = new MyReverseClass { Id = "1", Name = "Test" };
var myClassDto = _objectMapper.Map<MyReverseClass, MyReverseClassDto>(myClass);
myClassDto.Name.ShouldBe("Test");
myClass.Id = "2";
myClass.Name = "Test2";
_objectMapper.Map<MyReverseClass, MyReverseClassDto>(myClass, myClassDto);
myClassDto.Id.ShouldBe("2");
myClassDto.Name.ShouldBe("Test2");
}
[Fact]
public void ReverseMap_Test()
{
var myClassDto = new MyReverseClassDto { Id = "1", Name = "Test" };
var myClass = _objectMapper.Map<MyReverseClassDto, MyReverseClass>(myClassDto);
myClass.Name.ShouldBe("BeforeReverseMap Test AfterReverseMap");
myClassDto.Id = "2";
myClassDto.Name = "Test2";
_objectMapper.Map<MyReverseClassDto, MyReverseClass>(myClassDto, myClass);
myClass.Id.ShouldBe("2");
myClass.Name.ShouldBe("BeforeReverseMap Test2 AfterReverseMap");
}
}

13
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs

@ -0,0 +1,13 @@
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Mapperly;
[DependsOn(
typeof(AbpMapperlyModule),
typeof(AbpObjectExtendingTestModule)
)]
public class MapperlyTestModule : AbpModule
{
}

44
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs

@ -0,0 +1,44 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
using Volo.Abp.Mapperly.SampleClasses;
using Volo.Abp.ObjectExtending.TestObjects;
[Mapper]
public partial class MyEntityMapper : MapperBase<MyEntity, MyEntityDto>
{
public override partial MyEntityDto Map(MyEntity source);
public override partial void Map(MyEntity source, MyEntityDto destination);
}
[Mapper]
public partial class MyEnumMapper : MapperBase<MyEnum, MyEnumDto>
{
public override partial MyEnumDto Map(MyEnum source);
public override void Map(MyEnum source, MyEnumDto destination)
{
destination = Map(source);
}
}
[Mapper]
[MapExtraProperties(IgnoredProperties = ["CityName"])]
public partial class ExtensibleTestPersonMapper : MapperBase<ExtensibleTestPerson, ExtensibleTestPersonDto>
{
public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source);
public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonDto destination);
}
[Mapper]
[MapExtraProperties(MapToRegularProperties = true)]
public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : MapperBase<ExtensibleTestPerson, ExtensibleTestPersonWithRegularPropertiesDto>
{
[MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Name))]
[MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Age))]
[MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.IsActive))]
public override partial ExtensibleTestPersonWithRegularPropertiesDto Map(ExtensibleTestPerson source);
public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonWithRegularPropertiesDto destination);
}

10
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Mapperly.SampleClasses;
public class MyEntity
{
public Guid Id { get; set; }
public int Number { get; set; }
}

10
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Mapperly.SampleClasses;
public class MyEntityDto
{
public Guid Id { get; set; }
public int Number { get; set; }
}

10
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Mapperly.SampleClasses;
public class MyEntityDto2
{
public Guid Id { get; set; }
public int Number { get; set; }
}

43
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs

@ -0,0 +1,43 @@
using System;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.Mapperly.SampleClasses;
//TODO: Move tests to Volo.Abp.ObjectMapping test project
public class MyEntityDtoWithMappingMethods : IMapFrom<MyEntity>, IMapTo<MyEntity>
{
public Guid Key { get; set; }
public int No { get; set; }
public MyEntityDtoWithMappingMethods()
{
}
public MyEntityDtoWithMappingMethods(MyEntity entity)
{
MapFrom(entity);
}
public void MapFrom(MyEntity source)
{
Key = source.Id;
No = source.Number;
}
MyEntity IMapTo<MyEntity>.MapTo()
{
return new MyEntity
{
Id = Key,
Number = No
};
}
void IMapTo<MyEntity>.MapTo(MyEntity destination)
{
destination.Id = Key;
destination.Number = No;
}
}

39
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs

@ -0,0 +1,39 @@
using Volo.Abp.DependencyInjection;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.Mapperly.SampleClasses;
public class MyEntityToMyEntityDto2Mapper : IObjectMapper<MyEntity, MyEntityDto2>, IObjectMapper<MyEntityDto2, MyEntity>, ITransientDependency
{
public MyEntityDto2 Map(MyEntity source)
{
return new MyEntityDto2
{
Id = source.Id,
Number = source.Number + 1
};
}
public MyEntityDto2 Map(MyEntity source, MyEntityDto2 destination)
{
destination.Id = source.Id;
destination.Number = source.Number + 1;
return destination;
}
public MyEntity Map(MyEntityDto2 source)
{
return new MyEntity
{
Id = source.Id,
Number = source.Number + 1
};
}
public MyEntity Map(MyEntityDto2 source, MyEntity destination)
{
destination.Id = source.Id;
destination.Number = source.Number + 1;
return destination;
}
}

8
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.Mapperly.SampleClasses;
public enum MyEnum
{
Value1 = 1,
Value2,
Value3
}

9
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs

@ -0,0 +1,9 @@
namespace Volo.Abp.Mapperly.SampleClasses;
public enum MyEnumDto
{
Value1 = 2,
Value2,
Value3,
Value
}

10
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Mapperly.SampleClasses;
public class MyNotMappedDto
{
public Guid Id { get; set; }
public int Number { get; set; }
}

1
modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj

@ -21,6 +21,7 @@
<ProjectReference Include="..\..\..\identity\src\Volo.Abp.Identity.Application\Volo.Abp.Identity.Application.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.UI.Navigation\Volo.Abp.UI.Navigation.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Emailing\Volo.Abp.Emailing.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
</ItemGroup>
<ItemGroup>

21
modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs

@ -0,0 +1,21 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Identity;
using Volo.Abp.Mapperly;
namespace Volo.Abp.Account;
[Mapper]
[MapExtraProperties]
public partial class IdentityUserToProfileDtoMapper : MapperBase<IdentityUser, ProfileDto>
{
[MapperIgnoreTarget(nameof(ProfileDto.HasPassword))]
public override partial ProfileDto Map(IdentityUser source);
[MapperIgnoreTarget(nameof(ProfileDto.HasPassword))]
public override partial void Map(IdentityUser source, ProfileDto destination);
public override void AfterMap(IdentityUser source, ProfileDto destination)
{
destination.HasPassword = source.PasswordHash != null;
}
}

10
modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs

@ -1,4 +1,4 @@
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.Emailing;
using Volo.Abp.Identity;
using Volo.Abp.Modularity;
@ -12,7 +12,8 @@ namespace Volo.Abp.Account;
typeof(AbpAccountApplicationContractsModule),
typeof(AbpIdentityApplicationModule),
typeof(AbpUiNavigationModule),
typeof(AbpEmailingModule)
typeof(AbpEmailingModule),
typeof(AbpMapperlyModule)
)]
public class AbpAccountApplicationModule : AbpModule
{
@ -23,11 +24,6 @@ public class AbpAccountApplicationModule : AbpModule
options.FileSets.AddEmbedded<AbpAccountApplicationModule>();
});
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountApplicationModuleAutoMapperProfile>(validate: true);
});
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword";

15
modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs

@ -1,15 +0,0 @@
using AutoMapper;
using Volo.Abp.Identity;
namespace Volo.Abp.Account;
public class AbpAccountApplicationModuleAutoMapperProfile : Profile
{
public AbpAccountApplicationModuleAutoMapperProfile()
{
CreateMap<IdentityUser, ProfileDto>()
.ForMember(dest => dest.HasPassword,
op => op.MapFrom(src => src.PasswordHash != null))
.MapExtraProperties();
}
}

20
modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs

@ -1,20 +0,0 @@
using AutoMapper;
using Volo.Abp.Account.Blazor.Pages.Account;
using Volo.Abp.AutoMapper;
using Volo.Abp.Identity;
namespace Volo.Abp.Account.Blazor;
public class AbpAccountBlazorAutoMapperProfile : Profile
{
public AbpAccountBlazorAutoMapperProfile()
{
CreateMap<ProfileDto, PersonalInfoModel>()
.MapExtraProperties()
.Ignore(x => x.PhoneNumberConfirmed)
.Ignore(x => x.EmailConfirmed);
CreateMap<PersonalInfoModel, UpdateProfileDto>()
.MapExtraProperties();
}
}

27
modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs

@ -0,0 +1,27 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Account.Blazor.Pages.Account;
using Volo.Abp.Mapperly;
using Volo.Abp.Identity;
namespace Volo.Abp.Account.Blazor;
[Mapper]
[MapExtraProperties]
public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase<ProfileDto, PersonalInfoModel>
{
[MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))]
[MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))]
public override partial PersonalInfoModel Map(ProfileDto source);
[MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))]
[MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))]
public override partial void Map(ProfileDto source, PersonalInfoModel destination);
}
[Mapper]
[MapExtraProperties]
public partial class PersonalInfoModelToUpdateProfileDtoMapper : MapperBase<PersonalInfoModel, UpdateProfileDto>
{
public override partial UpdateProfileDto Map(PersonalInfoModel source);
public override partial void Map(PersonalInfoModel source, UpdateProfileDto destination);
}

15
modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs

@ -2,7 +2,7 @@
using Volo.Abp.Account.Blazor.Pages.Account;
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Volo.Abp.AspNetCore.Components.Web.Theming.Routing;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity;
@ -13,21 +13,16 @@ namespace Volo.Abp.Account.Blazor;
[DependsOn(
typeof(AbpAspNetCoreComponentsWebThemingModule),
typeof(AbpAutoMapperModule),
typeof(AbpMapperlyModule),
typeof(AbpAccountApplicationContractsModule)
)]
public class AbpAccountBlazorModule : AbpModule
{
private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner();
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<AbpAccountBlazorModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountBlazorAutoMapperProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpAccountBlazorModule>();
Configure<AbpNavigationOptions>(options =>
{
@ -39,7 +34,7 @@ public class AbpAccountBlazorModule : AbpModule
options.AdditionalAssemblies.Add(typeof(AbpAccountBlazorModule).Assembly);
});
}
public override void PostConfigureServices(ServiceConfigurationContext context)
{
OneTimeRunner.Run(() =>

2
modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj

@ -10,7 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Components.Web.Theming\Volo.Abp.AspNetCore.Components.Web.Theming.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
</ItemGroup>
<ItemGroup>

15
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs

@ -1,15 +0,0 @@
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity;
using AutoMapper;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
namespace Volo.Abp.Account.Web;
public class AbpAccountWebAutoMapperProfile : Profile
{
public AbpAccountWebAutoMapperProfile()
{
CreateMap<ProfileDto, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel>()
.MapExtraProperties();
}
}

13
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs

@ -0,0 +1,13 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
using Volo.Abp.Mapperly;
namespace Volo.Abp.Account.Web;
[Mapper]
[MapExtraProperties]
public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase<ProfileDto, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel>
{
public override partial AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel Map(ProfileDto source);
public override partial void Map(ProfileDto source, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel destination);
}

14
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs

@ -8,7 +8,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http.ProxyScripting.Generators.JQuery;
using Volo.Abp.Identity.AspNetCore;
@ -24,14 +24,14 @@ namespace Volo.Abp.Account.Web;
[DependsOn(
typeof(AbpAccountApplicationContractsModule),
typeof(AbpIdentityAspNetCoreModule),
typeof(AbpAutoMapperModule),
typeof(AbpMapperlyModule),
typeof(AbpAspNetCoreMvcUiThemeSharedModule),
typeof(AbpExceptionHandlingModule)
)]
public class AbpAccountWebModule : AbpModule
{
private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner();
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
@ -59,11 +59,7 @@ public class AbpAccountWebModule : AbpModule
ConfigureProfileManagementPage();
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpAccountWebModule>();
Configure<DynamicJavaScriptProxyOptions>(options =>
{
@ -96,7 +92,7 @@ public class AbpAccountWebModule : AbpModule
});
}
public override void PostConfigureServices(ServiceConfigurationContext context)
{
OneTimeRunner.Run(() =>

2
modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj

@ -39,7 +39,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\identity\src\Volo.Abp.Identity.AspNetCore\Volo.Abp.Identity.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
<ProjectReference Include="..\Volo.Abp.Account.Application.Contracts\Volo.Abp.Account.Application.Contracts.csproj" />
</ItemGroup>

46
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json

@ -15,6 +15,7 @@
"CmsKit.Ratings": "Hodnocení",
"CmsKit.Reactions": "Reakce",
"CmsKit.Tags": "Tagy",
"CmsKit.MarkedItems": "Označené položky",
"CmsKit:0002": "Obsah již existuje!",
"CmsKit:0003": "Entitu {0} nelze označit.",
"CmsKit:Blog:0001": "Daný slimák ({Slug}) již existuje!",
@ -26,8 +27,19 @@
"CmsKit:Rating:0001": "Entitu {EntityType} nelze hodnotit.",
"CmsKit:Reaction:0001": "Entita {EntityType} nemůže mít reakce.",
"CmsKit:Tag:0002": "Entitu nelze označit!",
"CmsKit:MarkedItem:ToggleConfirmation": "Opravdu chcete přepnout označenou položku?",
"ToggleFavorite": "Přidat/Odebrat z oblíbených",
"FavoritesFilterMessage": "Pro filtrování oblíbených se přihlaste",
"FilterOnFavorites": "Filtrovat podle oblíbených",
"CommentAuthorizationExceptionMessage": "Tyto komentáře nejsou povoleny pro veřejné zobrazení.",
"CmsKit:Modals:Login": "Přihlášení",
"CmsKit:Modals:LoginModalDefaultMessage": "Pro pokračování se přihlaste!",
"CmsKit:Modals:YouAreNotAuthenticated": "Tato operace pro vás není autorizována.",
"CommentDeletionConfirmationMessage": "Tento komentář a všechny odpovědi budou smazány!",
"CmsKit:MarkedItem:0001": "Entitu {EntityType} nelze označit.",
"CmsKit:MarkedItem:0002": "Pro typ entity '{EntityType}' nebyla nalezena žádná definice.",
"CmsKit:MarkedItem:0003": "Definice pro typ entity '{EntityType}' již existuje. Každý typ entity by měl mít pouze jednu definici.",
"CmsKit:MarkedItem:LoginMessage": "Pro označení této položky se přihlaste.",
"Comments": "Komentáře",
"Content": "Obsah",
"ContentDeletionConfirmationMessage": "Opravdu chcete tento obsah smazat?",
@ -134,6 +146,7 @@
"SamplePageMessage": "Ukázková stránka pro modul Pro",
"SaveChanges": "Uložit změny",
"Script": "Skript",
"SelectLayout": "Vybrat rozložení",
"SelectAll": "Vybrat vše",
"Send": "Poslat",
"SendMessage": "Poslat zprávu",
@ -216,6 +229,8 @@
"Feature:ReactionEnableDescription": "Systém reakcí CMS Kit, který umožňuje uživatelům posílat reakce entitám, jako jsou BlogPost, Komentáře atd.",
"Feature:TagEnable": "Označování",
"Feature:TagEnableDescription": "Systém značek CMS Kit, který umožňuje označování entit, jako je BlogPost.",
"Feature:MarkedItemEnable": "Označená položka",
"Feature:MarkedItemEnableDescription": "Systém označování CMS Kit, který umožňuje uživatelům označovat entity jako oblíbené.",
"DeleteBlogPostMessage": "Blog bude smazán. Jsi si jistá?",
"CaptchaCode": "Captcha kód",
"CommentTextRequired": "Komentář je povinný",
@ -230,6 +245,37 @@
"TagsHelpText": "Značky by měly být odděleny čárkami (např.: tag1, tag2, tag3)",
"ThisPartOfContentCouldntBeLoaded": "Tato část obsahu nemohla být načtena.",
"DuplicateCommentAttemptMessage": "Byl zjištěn duplicitní pokus o vložení komentáře. Váš komentář již byl odeslán.",
"NoBlogPostYet": "Zatím žádný blogový příspěvek!",
"CmsKit:Comment": "Komentář",
"CmsKitCommentOptions:RequireApprovement": "Vyžadovat schválení komentářů",
"CmsKitCommentOptions:RequireApprovementDescription": "Když je povoleno, komentáře budou vyžadovat schválení před zveřejněním.",
"CommentFilter:ApproveState": "Stav schválení",
"ApproveState": "Stav schválení",
"CommentFilter:0": "Vše",
"CommentFilter:1": "Schváleno",
"CommentFilter:2": "Zamítnuto",
"CommentFilter:4": "Čeká",
"ApprovedSuccessfully": "Úspěšně schváleno",
"ApprovalRevokedSuccessfully": "Schválení úspěšně odvoláno",
"Approve": "Schválit",
"Disapproved": "Zamítnuto",
"CommentAlertMessage": "Čeká {0} komentářů na schválení",
"Settings:Menu:CmsKit": "CMS",
"CommentsAwaitingApproval": "Komentáře čekající na schválení",
"JustNow": "Právě teď",
"MinuteAgo": "před 1 minutou",
"MinutesAgo": "před {0} minutami",
"HourAgo": "před 1 hodinou",
"HoursAgo": "před {0} hodinami",
"YesterdayAt": "Včera v {0}",
"DayAt": "{0} v {1}",
"MonthDayAt": "{0} {1} v {2}",
"FullDate": "{0} {1} {2}",
"Minute": "m",
"Hour": "h",
"Day": "d",
"Week": "t",
"CommentSubmittedForApproval": "Váš komentář byl odeslán ke schválení.",
"ChooseAnActionForBlog": "Vyberte akci pro blog",
"AssignBlogPostsToOtherBlog": "Přiřaďte blogové příspěvky k jinému blogu",
"SelectAnBlogToAssign": "Vyberte blog, ke kterému chcete přiřadit blogové příspěvky",

2
modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj

@ -18,7 +18,7 @@
<ProjectReference Include="..\Volo.Abp.Identity.Application.Contracts\Volo.Abp.Identity.Application.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.Identity.Domain\Volo.Abp.Identity.Domain.csproj" />
<ProjectReference Include="..\..\..\permission-management\src\Volo.Abp.PermissionManagement.Application\Volo.Abp.PermissionManagement.Application.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
</ItemGroup>
</Project>

19
modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs

@ -0,0 +1,19 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Identity;
using Volo.Abp.Mapperly;
[Mapper]
[MapExtraProperties]
public partial class IdentityUserToIdentityUserDtoMapper : MapperBase<IdentityUser, IdentityUserDto>
{
public override partial IdentityUserDto Map(IdentityUser source);
public override partial void Map(IdentityUser source, IdentityUserDto destination);
}
[Mapper]
[MapExtraProperties]
public partial class IdentityRoleToIdentityRoleDtoMapper : MapperBase<IdentityRole, IdentityRoleDto>
{
public override partial IdentityRoleDto Map(IdentityRole source);
public override partial void Map(IdentityRole source, IdentityRoleDto destination);
}

11
modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs

@ -1,5 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement;
@ -8,18 +8,13 @@ namespace Volo.Abp.Identity;
[DependsOn(
typeof(AbpIdentityDomainModule),
typeof(AbpIdentityApplicationContractsModule),
typeof(AbpAutoMapperModule),
typeof(AbpMapperlyModule),
typeof(AbpPermissionManagementApplicationModule)
)]
public class AbpIdentityApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<AbpIdentityApplicationModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpIdentityApplicationModuleAutoMapperProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpIdentityApplicationModule>();
}
}

15
modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs

@ -1,15 +0,0 @@
using AutoMapper;
namespace Volo.Abp.Identity;
public class AbpIdentityApplicationModuleAutoMapperProfile : Profile
{
public AbpIdentityApplicationModuleAutoMapperProfile()
{
CreateMap<IdentityUser, IdentityUserDto>()
.MapExtraProperties();
CreateMap<IdentityRole, IdentityRoleDto>()
.MapExtraProperties();
}
}

18
modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs

@ -1,18 +0,0 @@
using AutoMapper;
using Volo.Abp.AutoMapper;
namespace Volo.Abp.Identity.Blazor;
public class AbpIdentityBlazorAutoMapperProfile : Profile
{
public AbpIdentityBlazorAutoMapperProfile()
{
CreateMap<IdentityUserDto, IdentityUserUpdateDto>()
.MapExtraProperties()
.Ignore(x => x.Password)
.Ignore(x => x.RoleNames);
CreateMap<IdentityRoleDto, IdentityRoleUpdateDto>()
.MapExtraProperties();
}
}

25
modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs

@ -0,0 +1,25 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
namespace Volo.Abp.Identity.Blazor;
[Mapper]
[MapExtraProperties]
public partial class IdentityUserDtoToIdentityUserUpdateDtoMapper : MapperBase<IdentityUserDto, IdentityUserUpdateDto>
{
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))]
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))]
public override partial IdentityUserUpdateDto Map(IdentityUserDto source);
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))]
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))]
public override partial void Map(IdentityUserDto source, IdentityUserUpdateDto destination);
}
[Mapper]
[MapExtraProperties]
public partial class IdentityRoleDtoToIdentityRoleUpdateDtoMapper : MapperBase<IdentityRoleDto, IdentityRoleUpdateDto>
{
public override partial IdentityRoleUpdateDto Map(IdentityRoleDto source);
public override partial void Map(IdentityRoleDto source, IdentityRoleUpdateDto destination);
}

13
modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs

@ -1,7 +1,7 @@
using Localization.Resources.AbpUi;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Components.Web.Theming.Routing;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.BlazoriseUI;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Localization;
@ -16,7 +16,7 @@ namespace Volo.Abp.Identity.Blazor;
[DependsOn(
typeof(AbpIdentityApplicationContractsModule),
typeof(AbpAutoMapperModule),
typeof(AbpMapperlyModule),
typeof(AbpPermissionManagementBlazorModule),
typeof(AbpBlazoriseUIModule)
)]
@ -26,12 +26,7 @@ public class AbpIdentityBlazorModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<AbpIdentityBlazorModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpIdentityBlazorAutoMapperProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpIdentityBlazorModule>();
Configure<AbpNavigationOptions>(options =>
{
@ -42,7 +37,7 @@ public class AbpIdentityBlazorModule : AbpModule
{
options.AdditionalAssemblies.Add(typeof(AbpIdentityBlazorModule).Assembly);
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources

2
modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
<ProjectReference Include="..\..\..\permission-management\src\Volo.Abp.PermissionManagement.Blazor\Volo.Abp.PermissionManagement.Blazor.csproj" />
</ItemGroup>

2
modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj

@ -23,7 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Identity.Domain.Shared\Volo.Abp.Identity.Domain.Shared.csproj" />
<ProjectReference Include="..\..\..\users\src\Volo.Abp.Users.Domain\Volo.Abp.Users.Domain.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Ddd.Domain\Volo.Abp.Ddd.Domain.csproj" />
</ItemGroup>

11
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs

@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.Domain;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Modularity;
@ -19,7 +19,7 @@ namespace Volo.Abp.Identity;
typeof(AbpDddDomainModule),
typeof(AbpIdentityDomainSharedModule),
typeof(AbpUsersDomainModule),
typeof(AbpAutoMapperModule)
typeof(AbpMapperlyModule)
)]
public class AbpIdentityDomainModule : AbpModule
{
@ -35,12 +35,7 @@ public class AbpIdentityDomainModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<AbpIdentityDomainModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<IdentityDomainMappingProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpIdentityDomainModule>();
Configure<AbpDistributedEntityEventOptions>(options =>
{

33
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs

@ -0,0 +1,33 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
using Volo.Abp.Users;
namespace Volo.Abp.Identity;
[Mapper]
public partial class IdentityUserToUserEtoMapper : MapperBase<IdentityUser, UserEto>
{
public override partial UserEto Map(IdentityUser source);
public override partial void Map(IdentityUser source, UserEto destination);
}
[Mapper]
public partial class IdentityClaimTypeToIdentityClaimTypeEtoMapper : MapperBase<IdentityClaimType, IdentityClaimTypeEto>
{
public override partial IdentityClaimTypeEto Map(IdentityClaimType source);
public override partial void Map(IdentityClaimType source, IdentityClaimTypeEto destination);
}
[Mapper]
public partial class IdentityRoleToIdentityRoleEtoMapper : MapperBase<IdentityRole, IdentityRoleEto>
{
public override partial IdentityRoleEto Map(IdentityRole source);
public override partial void Map(IdentityRole source, IdentityRoleEto destination);
}
[Mapper]
public partial class OrganizationUnitToOrganizationUnitEtoMapper : MapperBase<OrganizationUnit, OrganizationUnitEto>
{
public override partial OrganizationUnitEto Map(OrganizationUnit source);
public override partial void Map(OrganizationUnit source, OrganizationUnitEto destination);
}

15
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs

@ -1,15 +0,0 @@
using AutoMapper;
using Volo.Abp.Users;
namespace Volo.Abp.Identity;
public class IdentityDomainMappingProfile : Profile
{
public IdentityDomainMappingProfile()
{
CreateMap<IdentityUser, UserEto>();
CreateMap<IdentityClaimType, IdentityClaimTypeEto>();
CreateMap<IdentityRole, IdentityRoleEto>();
CreateMap<OrganizationUnit, OrganizationUnitEto>();
}
}

57
modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs

@ -1,57 +0,0 @@
using AutoMapper;
using Volo.Abp.AutoMapper;
using Volo.Abp.Identity.Web.Pages.Identity.Roles;
using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel;
using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel;
namespace Volo.Abp.Identity.Web;
public class AbpIdentityWebAutoMapperProfile : Profile
{
public AbpIdentityWebAutoMapperProfile()
{
CreateUserMappings();
CreateRoleMappings();
}
protected virtual void CreateUserMappings()
{
//List
CreateMap<IdentityUserDto, EditUserModalModel.UserInfoViewModel>()
.Ignore(x => x.Password);
//CreateModal
CreateMap<CreateUserModalModel.UserInfoViewModel, IdentityUserCreateDto>()
.MapExtraProperties()
.ForMember(dest => dest.RoleNames, opt => opt.Ignore());
CreateMap<IdentityRoleDto, CreateUserModalModel.AssignedRoleViewModel>()
.ForMember(dest => dest.IsAssigned, opt => opt.Ignore());
//EditModal
CreateMap<EditUserModalModel.UserInfoViewModel, IdentityUserUpdateDto>()
.MapExtraProperties()
.ForMember(dest => dest.RoleNames, opt => opt.Ignore());
CreateMap<IdentityRoleDto, EditUserModalModel.AssignedRoleViewModel>()
.ForMember(dest => dest.IsAssigned, opt => opt.Ignore());
CreateMap<IdentityUserDto, EditUserModalModel.DetailViewModel>()
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
.ForMember(dest => dest.ModifiedBy, opt => opt.Ignore());
}
protected virtual void CreateRoleMappings()
{
//List
CreateMap<IdentityRoleDto, EditModalModel.RoleInfoModel>();
//CreateModal
CreateMap<CreateModalModel.RoleInfoModel, IdentityRoleCreateDto>()
.MapExtraProperties();
//EditModal
CreateMap<EditModalModel.RoleInfoModel, IdentityRoleUpdateDto>()
.MapExtraProperties();
}
}

94
modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs

@ -0,0 +1,94 @@
using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
using Volo.Abp.Identity.Web.Pages.Identity.Roles;
using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel;
using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel;
namespace Volo.Abp.Identity.Web;
[Mapper]
public partial class IdentityUserDtoToEditUserModalModelUserInfoViewModelMapper : MapperBase<IdentityUserDto, EditUserModalModel.UserInfoViewModel>
{
[MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))]
public override partial EditUserModalModel.UserInfoViewModel Map(IdentityUserDto source);
[MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))]
public override partial void Map(IdentityUserDto source, EditUserModalModel.UserInfoViewModel destination);
}
[Mapper]
[MapExtraProperties]
public partial class CreateUserModalModelUserInfoViewModelToIdentityUserCreateDtoMapper : MapperBase<CreateUserModalModel.UserInfoViewModel, IdentityUserCreateDto>
{
[MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))]
public override partial IdentityUserCreateDto Map(CreateUserModalModel.UserInfoViewModel source);
[MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))]
public override partial void Map(CreateUserModalModel.UserInfoViewModel source, IdentityUserCreateDto destination);
}
[Mapper]
[MapExtraProperties]
public partial class IdentityRoleDtoToCreateUserModalModelAssignedRoleViewModelMapper : MapperBase<IdentityRoleDto, CreateUserModalModel.AssignedRoleViewModel>
{
[MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))]
public override partial CreateUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source);
[MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))]
public override partial void Map(IdentityRoleDto source, CreateUserModalModel.AssignedRoleViewModel destination);
}
[Mapper]
public partial class EditUserModalModelUserInfoViewModelToIdentityUserUpdateDtoMapper : MapperBase<EditUserModalModel.UserInfoViewModel, IdentityUserUpdateDto>
{
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))]
public override partial IdentityUserUpdateDto Map(EditUserModalModel.UserInfoViewModel source);
[MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))]
public override partial void Map(EditUserModalModel.UserInfoViewModel source, IdentityUserUpdateDto destination);
}
[Mapper]
public partial class IdentityRoleDtoToEditUserModalModelAssignedRoleViewModelMapper : MapperBase<IdentityRoleDto, EditUserModalModel.AssignedRoleViewModel>
{
[MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))]
public override partial EditUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source);
[MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))]
public override partial void Map(IdentityRoleDto source, EditUserModalModel.AssignedRoleViewModel destination);
}
[Mapper]
public partial class IdentityUserDtoToEditUserModalModelDetailViewModelMapper : MapperBase<IdentityUserDto, EditUserModalModel.DetailViewModel>
{
[MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))]
[MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))]
public override partial EditUserModalModel.DetailViewModel Map(IdentityUserDto source);
[MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))]
[MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))]
public override partial void Map(IdentityUserDto source, EditUserModalModel.DetailViewModel destination);
}
[Mapper]
public partial class IdentityRoleDtoToEditModalModelRoleInfoModelMapper : MapperBase<IdentityRoleDto, EditModalModel.RoleInfoModel>
{
public override partial EditModalModel.RoleInfoModel Map(IdentityRoleDto source);
public override partial void Map(IdentityRoleDto source, EditModalModel.RoleInfoModel destination);
}
[Mapper]
[MapExtraProperties]
public partial class CreateModalModelRoleInfoModelToIdentityRoleCreateDtoMapper : MapperBase<CreateModalModel.RoleInfoModel, IdentityRoleCreateDto>
{
public override partial IdentityRoleCreateDto Map(CreateModalModel.RoleInfoModel source);
public override partial void Map(CreateModalModel.RoleInfoModel source, IdentityRoleCreateDto destination);
}
[Mapper]
[MapExtraProperties]
public partial class EditModalModelRoleInfoModelToIdentityRoleUpdateDtoMapper : MapperBase<EditModalModel.RoleInfoModel, IdentityRoleUpdateDto>
{
public override partial IdentityRoleUpdateDto Map(EditModalModel.RoleInfoModel source);
public override partial void Map(EditModalModel.RoleInfoModel source, IdentityRoleUpdateDto destination);
}

11
modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs

@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.PageToolbars;
using Volo.Abp.AutoMapper;
using Volo.Abp.Mapperly;
using Volo.Abp.Http.ProxyScripting.Generators.JQuery;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Identity.Web.Navigation;
@ -19,7 +19,7 @@ using Volo.Abp.Threading;
namespace Volo.Abp.Identity.Web;
[DependsOn(typeof(AbpIdentityApplicationContractsModule))]
[DependsOn(typeof(AbpAutoMapperModule))]
[DependsOn(typeof(AbpMapperlyModule))]
[DependsOn(typeof(AbpPermissionManagementWebModule))]
[DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))]
public class AbpIdentityWebModule : AbpModule
@ -51,12 +51,7 @@ public class AbpIdentityWebModule : AbpModule
options.FileSets.AddEmbedded<AbpIdentityWebModule>();
});
context.Services.AddAutoMapperObjectMapper<AbpIdentityWebModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpIdentityWebAutoMapperProfile>(validate: true);
});
context.Services.AddMapperlyObjectMapper<AbpIdentityWebModule>();
Configure<RazorPagesOptions>(options =>
{

2
modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj

@ -40,7 +40,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Identity.Application.Contracts\Volo.Abp.Identity.Application.Contracts.csproj" />
<ProjectReference Include="..\..\..\permission-management\src\Volo.Abp.PermissionManagement.Web\Volo.Abp.PermissionManagement.Web.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj" />
</ItemGroup>

29
npm/ng-packs/apps/dev-app/project.json

@ -6,25 +6,17 @@
"prefix": "app",
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"outputs": [
"{options.outputPath}"
],
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/dev-app/browser",
"outputPath": "dist/apps/dev-app",
"index": "apps/dev-app/src/index.html",
"main": "apps/dev-app/src/main.ts",
"polyfills": "apps/dev-app/src/polyfills.ts",
"browser": "apps/dev-app/src/main.ts",
"polyfills": ["apps/dev-app/src/polyfills.ts"],
"tsConfig": "apps/dev-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"allowedCommonJsDependencies": [
"chart.js",
"js-sha256"
],
"assets": [
"apps/dev-app/src/favicon.ico",
"apps/dev-app/src/assets"
],
"allowedCommonJsDependencies": ["chart.js", "js-sha256"],
"assets": ["apps/dev-app/src/favicon.ico", "apps/dev-app/src/assets"],
"styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.rtl.min.css",
@ -149,12 +141,9 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
"sourceMap": true
}
},
"defaultConfiguration": "production"
@ -251,4 +240,4 @@
"defaultConfiguration": "production"
}
}
}
}

2
npm/ng-packs/packages/account-core/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/account/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

3
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -47,7 +47,7 @@ import { eExtensibleComponents } from '../../enums/components';
import { ExtensibleDateTimePickerComponent } from '../date-time-picker/extensible-date-time-picker.component';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { ExtensibleFormPropService } from '../../services/extensible-form-prop.service';
import { NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-form-multiselect.component';
@ -67,6 +67,7 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
ShowPasswordDirective,
PermissionDirective,
LocalizationPipe,
AsyncPipe,
NgClass,
NgComponentOutlet,
NgTemplateOutlet,

2
npm/ng-packs/packages/components/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

6
npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts

@ -21,3 +21,9 @@ export const authGuard: CanActivateFn = () => {
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
return false;
};
export const asyncAuthGuard: CanActivateFn = () => {
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
return false;
};

10
npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts

@ -1,4 +1,4 @@
import { makeEnvironmentProviders, Provider, inject, provideAppInitializer } from '@angular/core';
import { makeEnvironmentProviders, Provider, provideAppInitializer, inject } from '@angular/core';
import { TitleStrategy } from '@angular/router';
import {
provideHttpClient,
@ -24,7 +24,7 @@ import { ABP, SortableItem } from '../models';
import { AuthErrorFilterService } from '../abstracts';
import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants';
import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services';
import { DefaultQueueManager, getInitialData, localeInitializer } from '../utils';
import { DefaultQueueManager, getInitialData } from '../utils';
import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './';
import { timezoneInterceptor, transferStateInterceptor } from '../interceptors';
@ -109,14 +109,12 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
withFetch(),
withInterceptors([transferStateInterceptor, timezoneInterceptor]),
),
provideAppInitializer(() => {
getInitialData();
localeInitializer();
provideAppInitializer(async () => {
inject(LocalizationService);
inject(LocalStorageListenerService);
inject(RoutesHandler);
await getInitialData();
}),
LocaleProvider,
CookieLanguageProvider,
{

9
npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts

@ -50,12 +50,13 @@ export async function getInitialData() {
}),
);
await lastValueFrom(result$);
await localeInitializer(injector);
}
export function localeInitializer() {
const injector = inject(Injector);
const sessionState = injector.get(SessionStateService);
const { registerLocaleFn }: ABP.Root = injector.get(CORE_OPTIONS);
export function localeInitializer(injector?: Injector) {
const currentInjector = injector || inject(Injector);
const sessionState = currentInjector.get(SessionStateService);
const { registerLocaleFn }: ABP.Root = currentInjector.get(CORE_OPTIONS);
const lang = sessionState.getLanguage() || 'en';

2
npm/ng-packs/packages/core/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/feature-management/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/identity/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

37
npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts

@ -6,10 +6,10 @@ import {
CanActivateFn,
} from '@angular/router';
import { Observable } from 'rxjs';
import { Observable, timer, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs';
import { OAuthService } from 'angular-oauth2-oidc';
import { AuthService, IAbpGuard } from '@abp/ng.core';
import { AuthService, IAbpGuard, EnvironmentService } from '@abp/ng.core';
/**
* @deprecated Use `abpOAuthGuard` *function* instead.
@ -53,3 +53,36 @@ export const abpOAuthGuard: CanActivateFn = (
authService.navigateToLogin(params);
return false;
};
export const asyncAbpOAuthGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const oAuthService = inject(OAuthService);
const authService = inject(AuthService);
const environmentService = inject(EnvironmentService);
const { oAuthConfig } = environmentService.getEnvironment();
if (oAuthConfig?.responseType === 'code') {
return firstValueFrom(
timer(0, 100).pipe(
map(() => oAuthService.hasValidAccessToken()),
filter(Boolean),
take(1),
timeout(3000),
catchError(() => {
authService.navigateToLogin({ returnUrl: state.url });
return of(false);
})
)
);
}
if (oAuthService.hasValidAccessToken()) {
return true;
}
authService.navigateToLogin({ returnUrl: state.url });
return false;
};

7
npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts

@ -2,6 +2,7 @@ import {
AuthService,
AuthGuard,
authGuard,
asyncAuthGuard,
ApiInterceptor,
PIPE_TO_LOGIN_FN_KEY,
CHECK_AUTHENTICATION_STATE_FN_KEY,
@ -10,7 +11,7 @@ import {
import { Provider, makeEnvironmentProviders, inject, provideAppInitializer } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { AbpOAuthGuard, abpOAuthGuard } from '../guards';
import { AbpOAuthGuard, abpOAuthGuard, asyncAbpOAuthGuard, } from '../guards';
import { OAuthConfigurationHandler } from '../handlers';
import { OAuthApiInterceptor } from '../interceptors';
import { AbpOAuthService, BrowserTokenStorageService, OAuthErrorFilterService } from '../services';
@ -31,6 +32,10 @@ export function provideAbpOAuth({ ssr = false }: { ssr?: boolean }) {
{
provide: authGuard,
useValue: abpOAuthGuard,
},
{
provide: asyncAuthGuard,
useValue: asyncAbpOAuthGuard,
},
{
provide: ApiInterceptor,

2
npm/ng-packs/packages/oauth/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/permission-management/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template

@ -10,7 +10,7 @@
"types": [],
"lib": [
"dom",
"es2018"
"es2020"
]
},
"exclude": [

2
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template

@ -10,7 +10,7 @@
"types": [],
"lib": [
"dom",
"es2018"
"es2020"
]
},
"exclude": [

2
npm/ng-packs/packages/schematics/tsconfig.json

@ -1,7 +1,7 @@
{
"compilerOptions": {
"baseUrl": "tsconfig",
"lib": ["es2019", "dom"],
"lib": ["es2020", "dom"],
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",

2
npm/ng-packs/packages/setting-management/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/tenant-management/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/theme-basic/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

2
npm/ng-packs/packages/theme-shared/tsconfig.lib.json

@ -7,7 +7,7 @@
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"],
"lib": ["dom", "es2020"],
"useDefineForClassFields": false
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],

9
npm/ng-packs/scripts/publish.ts

@ -47,7 +47,7 @@ program.parse(process.argv);
if (program.preview) await replaceWithPreview(program.nextVersion);
await execa('yarn', ['build', '--noInstall', '--skipNgcc'], { stdout: 'inherit' });
await execa('yarn', ['build', '--noInstall'], { stdout: 'inherit' });
await execa('yarn', ['build:schematics'], { stdout: 'inherit' });
} catch (error) {
console.error(error.stderr);
@ -98,12 +98,7 @@ program.parse(process.argv);
})();
async function updateVersion(version: string) {
await execa(
'yarn',
['update-version', version],
{ stdout: 'inherit', cwd: '../' },
);
await execa('yarn', ['update-version', version], { stdout: 'inherit', cwd: '../' });
await execa('yarn', ['replace-with-tilde']);
}

2
npm/ng-packs/source-code-requirements/tsconfig.lib.json

@ -6,7 +6,7 @@
"declaration": true,
"inlineSources": true,
"types": [],
"lib": ["dom", "es2018"]
"lib": ["dom", "es2020"]
},
"angularCompilerOptions": {
"enableIvy": true,

3
npm/ng-packs/tsconfig.base.json

@ -10,7 +10,8 @@
"importHelpers": true,
"target": "es2020",
"module": "esnext",
"lib": ["es2017", "dom"],
"lib": ["es2020", "dom"],
"esModuleInterop": true,
"baseUrl": "./",
"allowSyntheticDefaultImports": true,
"paths": {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save