diff --git a/Directory.Packages.props b/Directory.Packages.props index 361cbacd9c..c57b8c8f07 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -145,6 +145,7 @@ + diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index de05cb68f6..f1656af336 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -431,6 +431,9 @@ "WhoWeAre_Expert": "About Me", "CreateSolutionFolder": "Create Solution Folder", "CreateSolutionFolderOption": "Specifies if the project will be in a new folder in the output folder or directly the output folder.", + "CreateCrudPage": "Create CRUD Page", + "CreateCrudPageOption": "Generates a sample CRUD page with a Book entity to demonstrate basic operations (Create, Read, Update, Delete).", + "ConnectionString": "Connection string", "BooksPageTitle": "ABP Books", "BooksPageDescription": "Explore ABP books to deepen your understanding and mastery of ABP.", "PackageDetailPage_NuGetPackageInstallationOptions": "There are three ways to install {0} NuGet package to your project", diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md index 5567b42aad..e63db935a5 100644 --- a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md +++ b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md @@ -22,7 +22,7 @@ In the shared database model, all the application data stored in a single physic ![single-shared-database](single-shared-database.png) -This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for must applications. +This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for most applications. In this model, a single database table may contain data of multiple tenants. Each row in these tables have a `TenantId` field which is used to distinguish the tenant data and isolate a tenant's data from other tenant users. To make your entities multi-tenant aware, all you have to do is to implement the `IMultiTenant` interface provided by the ABP Framework. diff --git a/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md new file mode 100644 index 0000000000..1c50a56c8f --- /dev/null +++ b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md @@ -0,0 +1,546 @@ + +# How to Build Persistent Background Jobs with ABP Framework and Quartz + +## Introduction + +In modern SaaS applications, automated background processing is essential for delivering reliable user experiences. Whether you're sending subscription reminders, processing payments, or generating reports, background jobs ensure critical tasks happen on schedule without blocking your main application flow. + +### What is `Quartz.NET`? + +`Quartz.NET` is a powerful, open-source job scheduling library for .NET applications that provides cron-based scheduling for complex time patterns, job persistence across application restarts, clustering support for high-availability scenarios, flexible trigger types, and the ability to pass parameters to jobs through job data maps. It's the de facto standard for enterprise-grade job scheduling in the .NET ecosystem. + +### Quartz Storage Options: In-Memory vs Persistent + +When configuring **Quartz**, you have two primary storage options, each with significant implications for how your application behaves: + +### 🧠 In-Memory Storage (`RAMJobStore`) +- Keeps all job information in application memory. +- **Very fast** – no database overhead. +- **Volatile** – all jobs, triggers, and schedules are lost when the application stops or restarts. +- Best suited for: + - Development environments. + - Scenarios where job loss is acceptable. + +### 🗃️ Persistent Storage (`JobStoreTX` or similar) +- Stores all job information in a database. +- **Reliable** – schedules persist across: + - Application restarts + - Server crashes + - Deployments +- **Supports horizontal scaling** – multiple application instances can share the same job queue. +- **Slight performance overhead** due to database I/O. +- Best choice for: + - Production systems. + - Any scenario where **business continuity and reliability** are critical. + +### How ABP Simplifies Quartz Integration + +ABP handles Quartz configuration, dependency injection, and lifecycle management automatically. Developers define jobs using `QuartzBackgroundWorkerBase` and access services via `ICachedServiceProvider`, following ABP's standard conventions and leveraging optimal service caching for background job scenarios. + +### Benefits of the Integration + +- Full support for ABP’s cross-cutting concerns (e.g., multi-tenancy, localization) +- Robust scheduling powered by Quartz +- Built-in logging, error handling, and performance monitoring +- Scales easily without modifying business logic + +### Real-World Use Case: Subscription Reminders + +In this tutorial, we'll build a subscription reminder system that monitors client subscriptions, identifies those nearing expiration, sends professional email reminders seven days before expiration, tracks reminder history to prevent duplicates, and runs automatically every day at 9:00 AM using Quartz scheduling with PostgreSQL persistence. This system demonstrates how ABP and Quartz work together to solve real business problems with clean, maintainable code that follows enterprise-grade patterns. + +## Installing and Configuring Quartz + +Getting Quartz up and running in an ABP application is straightforward thanks to ABP's dedicated integration package. We'll replace the default background job system with Quartz for persistent job storage and robust scheduling capabilities. + +### Adding the Quartz Package + +The easiest way to add Quartz support to your ABP application is using the ABP CLI. Open a terminal in your project directory and run: + +```bash +abp add-package Volo.Abp.BackgroundWorkers.Quartz +``` + +This command automatically adds the necessary NuGet package reference and updates your module dependencies. The ABP CLI handles all the heavy lifting, ensuring you get the correct version that matches your ABP Framework version. + +### Configuring Quartz for Persistent Storage + +Once the package is installed, you need to configure Quartz to use your database (in my case it is PostgreSQL) for job persistence. This configuration goes in your main module's `PreConfigureServices` method: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpBackgroundJobsQuartzModule), + typeof(AbpBackgroundWorkersQuartzModule) +)] +public class MySaaSApplicationModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureAuthentication(context, configuration); + ConfigureUrls(configuration); + ConfigureImpersonation(context, configuration); + ConfigureQuartz(); // Add this line + } + + private void ConfigureQuartz() + { + PreConfigure(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() + .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(); + + // These services will be cached and reused throughout the job execution + var clientAppService = serviceProvider.GetRequiredService(); + var reminderLogAppService = serviceProvider.GetRequiredService(); + var emailService = serviceProvider.GetRequiredService(); + + 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 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!** 🚀 diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 98a4c9afb5..1bc151bddf 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -42,6 +42,8 @@ ABP supports all the following approaches to store the tenant data in the databa [Saas module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches. +> You can see the community article *[Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)* for more details about different database architectures with practical implementation details. + ## Usage Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible. @@ -456,8 +458,9 @@ The [Tenant Management module](../../../modules/tenant-management.md) provides a ### A note about separate database per tenant approach in open source version While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [Saas module (PRO)](../../../modules/saas.md). -Alternatively you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. +Alternatively, you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. ## See Also * [Features](../../infrastructure/features.md) +* [Article: Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9) diff --git a/docs/en/framework/fundamentals/object-extensions.md b/docs/en/framework/fundamentals/object-extensions.md index b12be63dc0..87ece27f75 100644 --- a/docs/en/framework/fundamentals/object-extensions.md +++ b/docs/en/framework/fundamentals/object-extensions.md @@ -394,6 +394,22 @@ public class MyProfile : Profile It has the same parameters with the `MapExtraPropertiesTo` method. +#### Mapperly Integration + +If you're using the [Mapperly](https://github.com/riok/mapperly) library, the ABP also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. + +You can use the `MapExtraProperties` attribute to Mapperly class: + +````csharp +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToProfileDtoMapper : MapperBase +{ + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} +```` + ## Entity Framework Core Database Mapping If you're using the EF Core, you can map an extra property to a table field in the database. Example: diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 8961d804c7..5a9ea5f82a 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -84,7 +84,7 @@ public class UserAppService : ApplicationService } ```` -You should have defined the mappings before to be able to map objects. See the AutoMapper integration section to learn how to define mappings. +You should have defined the mappings before to be able to map objects. See the AutoMapper/Mapperly integration section to learn how to define mappings. ## AutoMapper Integration @@ -217,13 +217,120 @@ public class MyProfile : Profile } ```` +## Mapperly Integration + +[Mapperly](https://github.com/riok/mapperly) is a .NET source generator for generating object mappings. [Volo.Abp.Mapperly](https://www.nuget.org/packages/Volo.Abp.Mapperly) package defines the Mapperly integration for the `IObjectMapper`. + +Once you define mappings class as below, you can use the `IObjectMapper` interface just like explained before. + +### Define Mapping Classes + +You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase` class: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); +} +```` + +### Before and After Mapping Methods + +The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial void BeforeMap(User source) + { + //TODO: Perform actions before the mapping + } + + public override partial void AfterMap(User source, UserDto destination) + { + //TODO: Perform actions after the mapping + } + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); + + public override partial void BeforeReverseMap(UserDto destination) + { + //TODO: Perform actions before the reverse mapping + } + + public override partial void AfterReverseMap(UserDto destination, User source) + { + //TODO: Perform actions after the reverse mapping + } +} +```` + +### Mapping the Object Extensions + +[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects: + +````csharp +[Mapper] +[MapExtraProperties] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more. + +### Lists and Arrays Support + +ABP Mapperly integration also supports mapping lists and arrays as explained in the [IObjectMapper Interface](#iobjectmappertsource-tdestination-interface) section. + +**Example**: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} + +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` + +### More Mapperly Features + +Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. + ## Advanced Topics ### IObjectMapper Interface -Assume that you have created a **reusable module** which defines AutoMapper profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). +Assume that you have created a **reusable module** which defines AutoMapper/Mapperly profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). -`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper even if the final application uses another default object mapping library. +`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper/Mapperly library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper/Mapperly even if the final application uses another default object mapping library. `IObjectMapper` is used to contextualize the object mapper, so you can use different libraries for different modules/contexts. @@ -281,6 +388,8 @@ public class UserAppService : ApplicationService While using the contextualized object mapper is same as the normal object mapper, you should register the contextualized mapper in your module's `ConfigureServices` method: +When using AutoMapper: + ````csharp [DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule @@ -298,6 +407,20 @@ public class MyModule : AbpModule } ```` +When using Mapperly: + +````csharp +[DependsOn(typeof(AbpMapperlyModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Use Mapperly for MyModule + context.Services.AddMapperlyObjectMapper(); + } +} +```` + `IObjectMapper` is an essential feature for a reusable module where it can be used in multiple applications each may use a different library for object to object mapping. All pre-built ABP modules are using it. But, for the final application, you can ignore this interface and always use the default `IObjectMapper` interface. ### IObjectMapper Interface diff --git a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md new file mode 100644 index 0000000000..2b0c5b623d --- /dev/null +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -0,0 +1,184 @@ +# Migrating from AutoMapper to Mapperly + +## Introduction + +The AutoMapper library is **no longer free for commercial use**. For more details, you can refer to [this announcement post](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/). + +ABP Framework provides both AutoMapper and Mapperly integrations. If your project currently uses AutoMapper and you don't have a commercial license, you can switch to Mapperly by following the steps outlined below. + +## Migration Steps + +Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations: + +1. Replace `Volo.Abp.AutoMapper` with `Volo.Abp.Mapperly` in all `*.csproj` files. +2. Replace `using Volo.Abp.AutoMapper;` with `using Volo.Abp.Mapperly;` in all `*.cs` files. +3. Replace `AbpAutoMapperModule` with `AbpMapperlyModule` in all `*.cs` files. +4. Replace `AddAutoMapperObjectMapper` with `AddMapperlyObjectMapper` in all `*.cs` files. +5. Remove any code sections that configure `Configure`. +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() + .MapExtraProperties() + .Ignore(x => x.IsLockedOut) + .Ignore(x => x.SupportTwoFactor) + .Ignore(x => x.RoleNames); + + CreateMap(); + + CreateMap() + .MapExtraProperties(); + + CreateMap() + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.RoleId, src => src.MapFrom(r => r.Id)); + + CreateMap() + .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 +{ + [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 +{ + public override partial IdentityUserClaimDto Map(IdentityUserClaim source); + + public override partial void Map(IdentityUserClaim source, IdentityUserClaimDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitDtoMapper : MapperBase +{ + public override partial OrganizationUnitDto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitDto destination); +} + +[Mapper] +public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase +{ + 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 +{ + [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 +{ + [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 +{ + [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 +{ + 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/) diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index d2db328fbf..5601fe11d0 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -2,6 +2,33 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. +## 1.1.2 (2025-07-31) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.3`) +* Configured LeptonX Lite logos in the templates. +* Added browser tab memory feature to remember previously selected tabs. +* Enhanced tools section with default credentials display for first-time tool usage. +* Improved module and package loading with better error handling. + +## 1.1.1 (2025-07-22) + +* Enhanced tools section with clear cookies option. +* Fixed language management module name display for imported modules. +* Improved update window messaging with "Skip this version" option. +* Fixed Docker Compose file issues in microservice template. +* Resolved RabbitMQ tool cookie problems. + +## 1.1.0 (2025-07-16) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.2`) +* Enhanced UI scaling for all windows and improved user experience. +* Added tools section in solution runner main area with basic Grafana dashboard for microservice template. +* Improved container management during application building. +* Enhanced background task exception handling. +* Added public account module package reference to Blazor WebApp client. +* Fixed tenant database context updating errors. +* Improved optional module selection UI with better documentation integration. + ## 1.0.2 (2025-06-24) * Enhanced the ABP NuGet package installation experience. diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index 51e0de5bbd..6fe3ba95be 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -4,6 +4,8 @@ This document provides a general overview of the relationship between various ve | **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| +| 1.1.2 | 9.2.3 | +| 1.1.0 - 1.1.1 | 9.2.2 | | 1.0.2 | 9.2.1 | | 1.0.1 | 9.2.0 | | 1.0.0 | 9.2.0 | diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 139c4a9a8b..d2082667aa 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -167,7 +167,7 @@ The `ReplaceDbContext` attribute allows the use of the `ModularCrmDbContext` cla ````csharp public class ModularCrmDbContext : AbpDbContext, - IProductsDbContext, + ICatalogDbContext, IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE { public DbSet Products { get; set; } diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index 03c06f75c9..e2279e3d57 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -171,13 +171,13 @@ This application has a single [entity](../../../framework/architecture/domain-dr using System; using Volo.Abp.Domain.Entities; -namespace TodoApp +namespace TodoApp; + +public class TodoItem : BasicAggregateRoot { - public class TodoItem : BasicAggregateRoot - { - public string Text { get; set; } = string.Empty; - } + public string Text { get; set; } = string.Empty; } + ```` `BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. @@ -281,15 +281,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace TodoApp +namespace TodoApp; + +public interface ITodoAppService : IApplicationService { - public interface ITodoAppService : IApplicationService - { - Task> GetListAsync(); - Task CreateAsync(string text); - Task DeleteAsync(Guid id); - } + Task> GetListAsync(); + Task CreateAsync(string text); + Task DeleteAsync(Guid id); } + ```` ### Data Transfer Object @@ -299,14 +299,14 @@ namespace TodoApp ````csharp using System; -namespace TodoApp +namespace TodoApp; + +public class TodoItemDto { - public class TodoItemDto - { - public Guid Id { get; set; } - public string Text { get; set; } = string.Empty; - } + public Guid Id { get; set; } + public string Text { get; set; } = string.Empty; } + ```` This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`. @@ -323,19 +323,18 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace TodoApp +namespace TodoApp; + +public class TodoAppService : ApplicationService, ITodoAppService { - public class TodoAppService : ApplicationService, ITodoAppService - { - private readonly IRepository _todoItemRepository; + private readonly IRepository _todoItemRepository; - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; } + + // TODO: Implement the methods here... } ```` @@ -412,8 +411,8 @@ Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* proje using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Web.Pages -{ +namespace TodoApp.Web.Pages; + public class IndexModel : TodoAppPageModel { public List TodoItems { get; set; } @@ -430,7 +429,7 @@ namespace TodoApp.Web.Pages TodoItems = await _todoAppService.GetListAsync(); } } -} + ```` This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. @@ -578,34 +577,39 @@ using Microsoft.AspNetCore.Components; using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Blazor.Pages +{{if UI=="Blazor" || UI=="BlazorWebApp"}} +namespace TodoApp.Blazor.Client.Pages; +{{else if UI=="BlazorServer"}} +namespace TodoApp.Blazor.Pages; +{{else if UI=="MAUIBlazor"}} +namespace TodoApp.MauiBlazor.Pages; +{{end}} + +public partial class Index { - public partial class Index - { - [Inject] - private ITodoAppService TodoAppService { get; set; } + [Inject] + private ITodoAppService TodoAppService { get; set; } - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } = string.Empty; + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } = string.Empty; - protected override async Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); } } ``` diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 4167492d16..e304510e48 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -491,6 +491,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Timing.Tests", "test\Volo.Abp.Timing.Tests\Volo.Abp.Timing.Tests.csproj", "{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly", "src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj", "{AF556046-54CD-48BC-9740-3E926DB8B510}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly.Tests", "test\Volo.Abp.Mapperly.Tests\Volo.Abp.Mapperly.Tests.csproj", "{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.MySQL.Pomelo", "src\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo.csproj", "{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}" EndProject Global @@ -1467,6 +1471,14 @@ Global {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Debug|Any CPU.Build.0 = Debug|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.ActiveCfg = Release|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.Build.0 = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.Build.0 = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.Build.0 = Release|Any CPU {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1718,6 +1730,8 @@ Global {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA} {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {AF556046-54CD-48BC-9740-3E926DB8B510} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7} = {447C8A77-E5F0-4538-8687-7383196D04EA} {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs index 751eec21bb..e1c2947c97 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs @@ -42,10 +42,10 @@ using Volo.Abp.AspNetCore.RazorViews; #nullable disable WriteLiteral("\n"); WriteLiteral("\n