mirror of https://github.com/abpframework/abp.git
107 changed files with 2832 additions and 473 deletions
@ -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!** 🚀 |
|||
@ -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/) |
|||
@ -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>>() |
|||
); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public class MyEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public int Number { get; set; } |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public class MyEntityDto |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public int Number { get; set; } |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public class MyEntityDto2 |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public int Number { get; set; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public enum MyEnum |
|||
{ |
|||
Value1 = 1, |
|||
Value2, |
|||
Value3 |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public enum MyEnumDto |
|||
{ |
|||
Value1 = 2, |
|||
Value2, |
|||
Value3, |
|||
Value |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Mapperly.SampleClasses; |
|||
|
|||
public class MyNotMappedDto |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public int Number { get; set; } |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using AutoMapper; |
|||
|
|||
namespace Volo.Abp.Identity; |
|||
|
|||
public class AbpIdentityApplicationModuleAutoMapperProfile : Profile |
|||
{ |
|||
public AbpIdentityApplicationModuleAutoMapperProfile() |
|||
{ |
|||
CreateMap<IdentityUser, IdentityUserDto>() |
|||
.MapExtraProperties(); |
|||
|
|||
CreateMap<IdentityRole, IdentityRoleDto>() |
|||
.MapExtraProperties(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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>(); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue