@ -0,0 +1,151 @@ |
|||
# ABP Platform 9.2 RC Has Been Released |
|||
|
|||
We are happy to release [ABP](https://abp.io) version **9.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. |
|||
|
|||
Try this version and provide feedback for a more stable version of ABP v9.2! Thanks to you in advance. |
|||
|
|||
## Get Started with the 9.2 RC |
|||
|
|||
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). |
|||
|
|||
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: |
|||
|
|||
 |
|||
|
|||
## Migration Guide |
|||
|
|||
There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v9.x or earlier: [ABP Version 9.2 Migration Guide](https://abp.io/docs/9.2/release-info/migration-guides/abp-9-2) |
|||
|
|||
## What's New with ABP v9.2? |
|||
|
|||
In this section, I will introduce some major features released in this version. |
|||
Here is a brief list of titles explained in the next sections: |
|||
|
|||
* Added `ApplicationName` Property to Isolate Background Jobs & Background Workers |
|||
* Docs Module: Added "Alternative Words" to Filter Items |
|||
* Introducing the Bunny BLOB Storage Provider |
|||
* Upgraded `MongoDB.Driver` to v3.1.0 |
|||
* Identity Pro Module: Require Email Verification to Register |
|||
* Switching users during OAuth login |
|||
|
|||
### Added ApplicationName Property to Isolate Background Jobs & Background Workers |
|||
|
|||
ABP's [Background Jobs Module](https://abp.io/docs/latest/modules/background-jobs) has been enhanced with a new `ApplicationName` property that helps isolate jobs and workers across multiple applications sharing the same database. |
|||
|
|||
Previously, when different applications used the BackgroundJobs module and shared a database, an application might encounter jobs that didn't belong to it. This would lead to failed processing attempts and marking jobs as `IsAbandoned = true` with a "Undefined background job for the job name" error, preventing these jobs from ever being executed. |
|||
|
|||
With the new `ApplicationName` property, applications now properly filter jobs at the repository level, ensuring each application only processes job types it recognizes. This prevents the incorrect abandonment of jobs and ensures consistent behavior in multi-application scenarios. |
|||
|
|||
You can set `ApplicationName` of `AbpBackgroundJobWorkerOptions` to your application name to isolate jobs and workers across multiple applications sharing the same database: |
|||
|
|||
```csharp |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<AbpBackgroundJobWorkerOptions>(options => |
|||
{ |
|||
options.ApplicationName = context.Services.GetApplicationName()!; |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
> For more information, please refer to the [Background Jobs Module](https://abp.io/docs/latest/modules/background-jobs) documentation and the [PR](https://github.com/abpframework/abp/pull/22169) that added this feature. |
|||
|
|||
### Docs Module: Added "Alternative Words" to Filter Items |
|||
|
|||
[ABP's Docs Module](https://abp.io/docs/9.2/modules/docs) now supports "alternative words" to enhance the search functionality when filtering documentation items. This feature addresses a common user experience issue where users might search using terminology different from what appears in the documentation. |
|||
|
|||
For example, when a user searches for "Error" in the documentation, they may actually be looking for content related to "Exception Handling." With this new feature, documentation items can now be configured with alternative keywords that are considered during filtering. |
|||
|
|||
The implementation allows defining optional "keywords" for items in the navigation tree. For example: |
|||
|
|||
```json |
|||
{ |
|||
"text": "Exception Handling", |
|||
"path": "framework/fundamentals/exception-handling.md", |
|||
"keywords": ["Error", "Another Value"] |
|||
} |
|||
``` |
|||
|
|||
When users search or filter content, the system now considers both the original text and these alternative keywords, improving discoverability of relevant documentation sections. This enhancement makes the documentation more accessible and user-friendly, especially for newcomers who might not be familiar with the exact terminology used in the ABP documentation. |
|||
|
|||
### Introducing the Bunny BLOB Storage Provider |
|||
|
|||
ABP v9.2 RC introduces a new BLOB storage provider for [Bunny Storage](https://bunny.net/storage/), a global edge storage solution. This addition expands ABP's BLOB Storage options beyond the existing providers like Azure, AWS, and others. |
|||
|
|||
The [Bunny BLOB Storage Provider](https://abp.io/docs/9.2/framework/infrastructure/blob-storing/bunny) allows ABP applications to seamlessly integrate with Bunny's CDN-backed storage service, which offers high-performance content delivery through its global network. |
|||
|
|||
To use this new provider, you'll need to: |
|||
|
|||
* Run `abp add-package Volo.Abp.BlobStoring.Bunny` command. |
|||
* And then configure the provider in your module's `ConfigureServices` method: |
|||
|
|||
```csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseBunny(bunny => |
|||
{ |
|||
bunny.StorageZoneName = "your-storage-zone"; |
|||
bunny.ApiKey = "your-api-key"; |
|||
bunny.Region = "your-region"; // de, ny, la, sg, or sy |
|||
}); |
|||
}); |
|||
}); |
|||
``` |
|||
|
|||
This integration provides ABP applications with an efficient and globally distributed storage solution, particularly beneficial for applications requiring fast content delivery across different geographical regions. To use this new provider and make the related configurations, you can refer to the [Bunny Storage Provider](https://abp.io/docs/9.2/framework/infrastructure/blob-storing/bunny) documentation always. |
|||
|
|||
> This new BLOB Storage provider is contributed by [@suhaib-mousa](https://github.com/suhaib-mousa). Thanks to him for his contribution! |
|||
> We are always happy to see the community contributing to the ABP Framework and encouraging them to contribute more. |
|||
|
|||
### Upgraded `MongoDB.Driver` to `v3.1.0` |
|||
|
|||
ABP v9.2 RC includes an upgrade to `MongoDB.Driver` version `3.1.0`. This significant version bump from previous releases brings several improvements and new features that benefit ABP applications using MongoDB as their database. |
|||
|
|||
The upgrade provides: |
|||
|
|||
* Async/Await Support: Write non-blocking, asynchronous code easily. |
|||
* Fluent API: Build queries and updates intuitively with Builders. |
|||
* LINQ Support: Use LINQ for querying MongoDB collections. |
|||
* and more ... |
|||
|
|||
> For more information, please refer to the [MongoDB.Driver release notes](https://github.com/mongodb/mongo-csharp-driver/releases/tag/v3.1.0). |
|||
|
|||
We have prepared a [migration guide](https://abp.io/docs/9.2/release-info/migration-guides/MongoDB-Driver-2-to-3) for this upgrade. Please refer to it to learn more about the changes and how to migrate your application. |
|||
|
|||
### Identity Pro Module: Require Email Verification to Register |
|||
|
|||
[ABP Identity Pro module](https://abp.io/docs/9.2/modules/identity-pro) has been enhanced with a new feature that allows administrators to require email verification during the registration process. This security improvement ensures that users must verify their email addresses before their registration is considered complete. Enabling this feature is especially important for applications that want to prevent spam registrations. |
|||
|
|||
Administrators can enable or disable this feature through the **Identity management -> Identity Verification (tab)** settings page (by checking the `Enforce email verification to register` checkbox): |
|||
|
|||
 |
|||
|
|||
### Switching users during OAuth login |
|||
|
|||
If you have an OAuth/Auth Server application using the [ABP Account Pro module](https://abp.io/docs/9.2/modules/account-pro) , you can pass the `prompt=select_account` parameter to force the user to select an account. |
|||
|
|||
 |
|||
|
|||
For more information, please refer to the [Switching users during OAuth login](https://abp.io/docs/9.2/modules/account-pro#switching-users-during-oauth-login) documentation. |
|||
|
|||
### New ABP Community Articles |
|||
|
|||
There are exciting articles contributed by the ABP community as always. I will highlight some of them here: |
|||
|
|||
* [Implementing CQRS with MediatR in ABP](https://abp.io/community/articles/implementing-cqrs-with-mediatr-in-abp-xiqz2iio) by [Engincan Veske](https://github.com/EngincanV) |
|||
* [Using Vue Components in a Razor Pages ABP Application](https://abp.io/community/articles/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv) by [Enis Necipoglu](https://github.com/enisn) |
|||
* [Using ABP's AWS Blob Storing Provider with DigitalOcean Spaces](https://abp.io/community/articles/using-abps-aws-blob-storing-provider-with-digitalocean-spaces-7hlyb25g) by [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) |
|||
* [Video Post: Using Vue Components in a Razor Pages ABP Application](https://abp.io/community/articles/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv) by [Enis Necipoglu](https://github.com/enisn) |
|||
* [Understanding the Embedded Files in ABP Framework](https://abp.io/community/articles/understanding-the-embedded-files-in-abp-framework-nsrp8aa9) by [Liming Ma](https://github.com/maliming) |
|||
* [How to Change the CurrentUser in ABP?](https://abp.io/community/articles/how-to-change-the-currentuser-in-abp-i3uu1m7g) by [Engincan Veske](https://github.com/EngincanV) |
|||
|
|||
|
|||
Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. |
|||
|
|||
## Conclusion |
|||
|
|||
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/9.2/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.2 RC and provide feedback to help us release a more stable version. |
|||
|
|||
Thanks for being a part of this community! |
|||
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 727 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 140 KiB |
@ -0,0 +1,781 @@ |
|||
# Developing a Multi-Timezone Application Using the ABP Framework |
|||
|
|||
When developing multi-timezone applications, we need to handle users from different time zones and make sure they see the correct time. The system also needs to support users changing their timezone (like when traveling or moving) and make sure all time displays update correctly to show accurate time information. |
|||
|
|||
All these scenarios require us to handle timezone conversions correctly in our application. The ABP framework provides a complete solution for these challenges. |
|||
|
|||
In this article, we'll show you step by step how to handle multi-timezone in the ABP framework. |
|||
|
|||
> The content mentioned in this article will be available after the ABP 9.2 version |
|||
|
|||
## Timezone Settings |
|||
|
|||
The ABP framework provides a setting called `Abp.Timing.TimeZone` for setting and getting the timezone of users, tenants, or applications. The default value is empty, which means the application will use the server's time zone. Check out the [Timing documentation](https://abp.io/docs/latest/framework/infrastructure/timing) for more information. |
|||
|
|||
## ISO 8601 Date Time Format |
|||
|
|||
Different countries and regions may use different time formats: |
|||
|
|||
* Year-Month-Day (YYYY-MM-DD): Mainly used in China, Japan, Korea, Canada (official standard), Germany (ISO standard), ISO 8601 international standard, etc. Example: 2025-03-11 |
|||
* Day-Month-Year (DD-MM-YYYY): Mainly used in UK, India, Australia, New Zealand, most European countries (like France, Germany, Italy, Spain), some South American countries, etc. Example: 11-03-2025 or 11/03/2025 |
|||
* Month-Day-Year (MM-DD-YYYY): Mainly used in USA, Philippines, some parts of Canada, etc. Example: 03-11-2025 or 03/11/2025 |
|||
* Day.Month.Year (DD.MM.YYYY): Mainly used in Germany, Russia, Switzerland, Hungary, Czech Republic, etc. Example: 11.03.2025 |
|||
|
|||
Also, different countries/regions might use different separators (like slash /, hyphen -, dot .), and some countries use different month abbreviations or full names (like March 11, 2025). |
|||
|
|||
ISO 8601 uses a standard format to avoid confusion between different date formats and ensure global compatibility. |
|||
|
|||
It has 4 parts: |
|||
|
|||
* Date part: `YYYY-MM-DD` |
|||
* `T` as a separator |
|||
* Time part: `HH:MM:SS` |
|||
* Timezone part: `Z` or `+/-HH:MM` |
|||
|
|||
You'll usually see formats like: `YYYY-MM-DDTHH:MM:SSZ` or `YYYY-MM-DDTHH:MM:SS+/-HH:MM`, for example: `2025-03-11T10:30:00Z` or `2025-03-11T22:30:00+03:00` |
|||
|
|||
When our application needs to handle multiple timezones, we usually use ISO 8601 to represent time. |
|||
|
|||
## Enabling Multi-Timezone Support |
|||
|
|||
When we set the `Kind` of `AbpClockOptions` to `DateTimeKind.Utc`, the ABP framework will normalize all times. Times written to the database and returned to the frontend will be in `UTC`. the `SupportsMultipleTimezone` property will be `true` in the `IClock` service. |
|||
|
|||
```csharp |
|||
Configure<AbpClockOptions>(options => |
|||
{ |
|||
options.Kind = DateTimeKind.Utc; |
|||
}); |
|||
``` |
|||
|
|||
### Using DateTime to Store Time |
|||
|
|||
Assuming the `DateTime` stored in the database is `2025-03-01 10:30:00`, then the time returned to the front end will be `2025-03-01T10:30:00Z`. This is a time in ISO 8601 format. Because `DateTime` does not have timezone information, the framework will assume it is `UTC` time. |
|||
|
|||
### Using DateTimeOffset to Store Time |
|||
|
|||
If you use `DateTimeOffset` to store time, the ABP framework will not normalize `DateTimeOffset`, but will return it directly to the front end. |
|||
|
|||
Assuming the `DateTimeOffset` stored in the database is `2025-03-01 13:30:00 +03:00`, then the time returned to the front end will be `2025-03-01T13:30:00+03:00`. This is also a time in ISO 8601 format. |
|||
|
|||
We recommend using `DateTimeOffset` to store time because it has timezone information. |
|||
|
|||
## Timezone Conversion |
|||
|
|||
### Converting UTC Time to User Time |
|||
|
|||
The `IClock` service has 2 methods to convert a given `UTC` time to the user time: |
|||
|
|||
```csharp |
|||
DateTime ConvertToUserTime(utcDateTime dateTime) |
|||
DateTimeOffset ConvertToUserTime(DateTimeOffset dateTimeOffset) |
|||
``` |
|||
|
|||
> If `SupportsMultipleTimezone` is `false` or `dateTime.Kind` is not `Utc` or no timezone is set, it will return the given `DateTime` or `DateTimeOffset` without any changes. |
|||
|
|||
**Example:** |
|||
|
|||
If the user's timezone is `Europe/Istanbul` |
|||
|
|||
```csharp |
|||
// 2025-03-01T05:30:00Z |
|||
var utcTime = new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Utc); |
|||
|
|||
var userTime = Clock.ConvertToUserTime(utcTime); |
|||
|
|||
// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours later. |
|||
userTime.Kind.ShouldBe(DateTimeKind.Unspecified); |
|||
userTime.ToString("O").ShouldBe("2025-03-01T08:30:00"); |
|||
``` |
|||
|
|||
```csharp |
|||
// 2025-03-01T05:30:00Z |
|||
var utcTime = new DateTimeOffset(new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Utc), TimeSpan.Zero); |
|||
|
|||
var userTime = Clock.ConvertToUserTime(utcTime); |
|||
|
|||
// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours later. |
|||
userTime.Offset.ShouldBe(TimeSpan.FromHours(3)); |
|||
userTime.ToString("O").ShouldBe("2025-03-01T08:30:00.0000000+03:00"); |
|||
``` |
|||
|
|||
### Converting User Time to UTC |
|||
|
|||
The `IClock` service has 1 method to convert a given user time to UTC. |
|||
|
|||
```csharp |
|||
DateTime ConvertToUtc(DateTime dateTime) |
|||
``` |
|||
|
|||
> If `SupportsMultipleTimezone` is `false` or `dateTime.Kind` is `Utc` or no timezone is set, it will return the given `DateTime` without any changes. |
|||
|
|||
**Example:** |
|||
|
|||
If the user's timezone is `Europe/Istanbul` |
|||
|
|||
```csharp |
|||
// 2025-03-01T05:30:00 |
|||
var userTime = new DateTime(2025, 3, 1, 5, 30, 0, DateTimeKind.Unspecified); //Same as Local |
|||
|
|||
var utcTime = Clock.ConvertToUtc(userTime); |
|||
|
|||
// Europe/Istanbul has 3 hours difference with UTC. So, the result will be 3 hours earlier. |
|||
utcTime.Kind.ShouldBe(DateTimeKind.Utc); |
|||
utcTime.ToString("O").ShouldBe("2025-03-01T02:30:00.0000000Z"); |
|||
``` |
|||
|
|||
## Handling Timezone in Different UIs |
|||
|
|||
We'll use the `TimeZoneApp` project to demonstrate handling timezone in different UIs. It has a `Meeting` entity, with several time properties. |
|||
|
|||
```csharp |
|||
public class Meeting : AggregateRoot<Guid> |
|||
{ |
|||
public string Subject { get; set; } |
|||
|
|||
public DateTime StartTime { get; set; } |
|||
|
|||
public DateTime EndTime { get; set; } |
|||
|
|||
public DateTime ActualStartTime { get; set; } |
|||
|
|||
public DateTime? CanceledTime { get; set; } |
|||
|
|||
public DateTimeOffset ReminderTime { get; set; } |
|||
|
|||
public DateTimeOffset? FollowUpTime { get; set; } |
|||
|
|||
public string Description { get; set; } |
|||
} |
|||
``` |
|||
|
|||
`TimeZoneApp` project is an ABP layered architecture project, it sets a global `Europe/Istanbul` timezone, it contains 4 websites |
|||
|
|||
* `API.Host`: API website, it does not have UI, it returns data in JSON format |
|||
* `AuthServer`: Authentication server, it uses Razor Pages as UI |
|||
* `Web`: Razor Pages website, it uses JavaScript to manage Meeting creation and editing and display |
|||
* `Blazor`: Blazor Server website, it uses Blazor to manage Meeting creation and editing and display |
|||
|
|||
All 4 applications are enabled for multi-timezone support, and use the `UseAbpTimeZone` middleware. |
|||
|
|||
> Blazor WASM and Angular do not need to use the `UseAbpTimeZone` middleware |
|||
|
|||
|
|||
### DateTime in API Response |
|||
|
|||
In the API response, we usually use the ISO 8601 format time, as you can see, after enabling multi-timezone support, the API returns time to the front end as UTC time. |
|||
|
|||
`2025-03-01T09:30:00Z` and `2025-03-01T12:30:00+00:00` are ISO 8601 format time. |
|||
|
|||
```json |
|||
[ |
|||
{ |
|||
"subject": "ABP Developer Guide", |
|||
"startTime": "2025-03-01T09:30:00Z", |
|||
"endTime": "2025-03-01T10:30:00Z", |
|||
"actualStartTime": "2025-03-01T11:30:00Z", |
|||
"canceledTime": null, |
|||
"reminderTime": "2025-03-01T12:30:00+00:00", |
|||
"followUpTime": "2025-03-01T13:30:00+00:00", |
|||
"description": "We will discuss the ABP developer guide.", |
|||
"id": "2af0abd3-be06-ecff-5d4c-3a1895ac7950" |
|||
}, |
|||
{ |
|||
"subject": "ABP Training", |
|||
"startTime": "2025-03-01T09:30:00Z", |
|||
"endTime": "2025-03-01T10:30:00Z", |
|||
"actualStartTime": "2025-03-01T11:30:00Z", |
|||
"canceledTime": "2025-03-01T12:00:00Z", |
|||
"reminderTime": "2025-03-01T12:30:00+00:00", |
|||
"followUpTime": "2025-03-01T13:30:00+00:00", |
|||
"description": "ABP training for the new developers.", |
|||
"id": "290b0cb6-3e50-6324-1e79-3a1895ac795f" |
|||
} |
|||
] |
|||
``` |
|||
|
|||
### Handling Timezone in MVC/Razor Pages |
|||
|
|||
In the `AuthServer` project, we handle time conversion in a simple way: |
|||
1. First, we get the `Meeting` entities from the database using `IRepository<Meeting, Guid>`. At this point, all `DateTime` values are in UTC. |
|||
2. Then, when displaying the times in the view, we use `Clock.ConvertToUserTime` to show them in the user's timezone. |
|||
|
|||
> Note: The `ConvertToUserTime` method will only convert times if multi-timezone support is enabled in the application. |
|||
|
|||
```csharp |
|||
public class IndexModel : AbpPageModel |
|||
{ |
|||
public List<Meeting>? Meetings { get; set; } |
|||
|
|||
protected IRepository<Meeting, Guid> MeetingRepository { get; } |
|||
|
|||
public IndexModel(IRepository<Meeting, Guid> meetingRepository) |
|||
{ |
|||
MeetingRepository = meetingRepository; |
|||
} |
|||
|
|||
public async Task OnGetAsync() |
|||
{ |
|||
Meetings = await MeetingRepository.GetListAsync(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```html |
|||
<div class="container"> |
|||
<abp-row> |
|||
<div class="table-responsive"> |
|||
<table class="table table-striped table-hover mt-3"> |
|||
<thead> |
|||
<tr> |
|||
<th>@L["Subject"]</th> |
|||
<th>@L["StartTime"] / @L["EndTime"]</th> |
|||
<th>@L["ActualStartTime"]</th> |
|||
<th>@L["CanceledTime"]</th> |
|||
<th>@L["ReminderTime"]</th> |
|||
<th>@L["FollowUpTime"]</th> |
|||
<th>@L["Description"]</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
@foreach (var meeting in Model.Meetings) |
|||
{ |
|||
<tr> |
|||
<td>@meeting.Subject</td> |
|||
<td>@Clock.ConvertToUserTime(meeting.StartTime) ➡️ @Clock.ConvertToUserTime(meeting.EndTime)</td> |
|||
<td>@Clock.ConvertToUserTime(meeting.ActualStartTime)</td> |
|||
<td>@(meeting.CanceledTime.HasValue ? Clock.ConvertToUserTime(meeting.CanceledTime.Value) : "N/A")</td> |
|||
<td>@Clock.ConvertToUserTime(meeting.ReminderTime).DateTime</td> |
|||
<td>@(meeting.FollowUpTime.HasValue ? Clock.ConvertToUserTime(meeting.FollowUpTime.Value).DateTime : "N/A")</td> |
|||
<td>@meeting.Description</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</abp-row> |
|||
</div> |
|||
``` |
|||
|
|||
 |
|||
|
|||
### Handling Timezone in JavaScript |
|||
|
|||
In the `Web` project, we use JavaScript to handle timezone. |
|||
|
|||
#### Displaying Time in UI |
|||
|
|||
* `timeZoneApp.meetings.meeting.getList` gets all `Meeting` entities and displays them in `DataTables` |
|||
* `abp.clock.normalizeToLocaleString()` is the ABP JavaScript API, it converts `UTC` time to the current user's timezone, and then calls its `toLocaleString` method to format time |
|||
* `dataFormat: "datetime"` is the ABP DataTable extension method, it calls the `abp.clock.normalizeToLocaleString` method to convert and format time |
|||
|
|||
> If the current application is not enabled for multi-timezone support, then the `abp.clock.normalizeToLocaleString` method will not convert the time, it will just call the `Date` object's `toLocaleString` method. |
|||
|
|||
```js |
|||
var dataTable = $('#MeetingsTable').DataTable( |
|||
abp.libs.datatables.normalizeConfiguration({ |
|||
serverSide: true, |
|||
paging: true, |
|||
order: [[1, "asc"]], |
|||
searching: false, |
|||
scrollX: true, |
|||
ajax: abp.libs.datatables.createAjax(timeZoneApp.meetings.meeting.getList), |
|||
columnDefs: [ |
|||
{ |
|||
title: l('Actions'), |
|||
rowAction: { |
|||
items: |
|||
[ |
|||
{ |
|||
text: l('Edit'), |
|||
visible: abp.auth.isGranted('TimeZoneApp.Meetings.Edit'), |
|||
action: function (data) { |
|||
editModal.open({ id: data.record.id }); |
|||
}, |
|||
}, |
|||
{ |
|||
text: l('Delete'), |
|||
visible: abp.auth.isGranted('TimeZoneApp.Meetings.Delete'), |
|||
confirmMessage: function (data) { |
|||
return l('MeetingDeletionConfirmationMessage', data.record.subject); |
|||
}, |
|||
action: function (data) { |
|||
timeZoneApp.meetings.meeting |
|||
.delete(data.record.id) |
|||
.then(function() { |
|||
abp.notify.info(l('SuccessfullyDeleted')); |
|||
dataTable.ajax.reload(); |
|||
}); |
|||
} |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
title: l('Subject'), |
|||
data: "subject" |
|||
}, |
|||
{ |
|||
title: l('StartTime') + ' / ' + l('StartTime'), |
|||
data: "startTime", |
|||
render: function (data, type, row) { |
|||
return abp.clock.normalizeToLocaleString(row.startTime) + ' ➡️ ' + abp.clock.normalizeToLocaleString(row.endTime); |
|||
} |
|||
}, |
|||
{ |
|||
title: l('ActualStartTime'), |
|||
data: "actualStartTime", |
|||
dataFormat: "datetime" |
|||
}, |
|||
{ |
|||
title: l('CanceledTime'), |
|||
data: "canceledTime", |
|||
render: function (data, type, row) { |
|||
return data ? abp.clock.normalizeToLocaleString(data) : 'N/A'; |
|||
} |
|||
}, |
|||
{ |
|||
title: l('ReminderTime'), |
|||
data: "reminderTime", |
|||
dataFormat: "datetime" |
|||
}, |
|||
{ |
|||
title: l('FollowUpTime'), |
|||
data: "followUpTime", |
|||
render: function (data, type, row) { |
|||
return data ? abp.clock.normalizeToLocaleString(data) : 'N/A'; |
|||
} |
|||
}, |
|||
{ |
|||
title: l('Description'), |
|||
data: "description" |
|||
} |
|||
] |
|||
}) |
|||
); |
|||
``` |
|||
|
|||
Below is the screenshot of `DataTables`: |
|||
|
|||
 |
|||
|
|||
|
|||
#### Creating and Editing Meeting |
|||
|
|||
We use `JavaScript` to create and edit `Meeting`. |
|||
|
|||
ABP's [TagHelper](https://abp.io/docs/latest/framework/ui/mvc-razor-pages/tag-helpers) can automatically create forms based on the model, it will generate corresponding HTML tags based on the attributes in the model. For `DateTime` and `DateTimeOffset` attributes, it will generate and initialize a [DateTimePicker](https://www.daterangepicker.com/) component. |
|||
|
|||
**CreateModal** and **EditModal** : |
|||
|
|||
```html |
|||
<abp-dynamic-form abp-model="Meeting" asp-page="/Meetings/CreateModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["NewMeeting"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
``` |
|||
|
|||
```html |
|||
<abp-dynamic-form abp-model="Meeting" asp-page="/Meetings/EditModal"> |
|||
<abp-modal> |
|||
<abp-modal-header title="@L["Update"].Value"></abp-modal-header> |
|||
<abp-modal-body> |
|||
<abp-input asp-for="Id" /> |
|||
<abp-form-content /> |
|||
</abp-modal-body> |
|||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> |
|||
</abp-modal> |
|||
</abp-dynamic-form> |
|||
``` |
|||
|
|||
You can see that the time in the control has been converted to the current user's timezone. |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
When we submit the form, we need to convert the time to `UTC`. In the `JavaScript` of the `Create` and `Edit` pages, we use the `handleDatepicker` this `jQuery` extension method to handle time in the form, it internally gets the user's local time from the selector `input[type="hidden"][data-hidden-datepicker]`, and then uses the `abp.clock.normalizeToString` method to convert the date field in the form to the `ISO 8601` format `UTC` time string. |
|||
|
|||
> If the current application is not enabled for multi-timezone support, then the `abp.clock.normalizeToString` method will not convert the time, it will just convert to the ISO 8601 format time string without timezone. |
|||
|
|||
```js |
|||
var abp = abp || {}; |
|||
$(function () { |
|||
abp.modals.meetingCreate = function () { |
|||
var initModal = function (publicApi, args) { |
|||
var $form = publicApi.getForm(); |
|||
$form.find('button[type="submit"]').on('click', function (e) { |
|||
$form.handleDatepicker('input[type="hidden"][data-hidden-datepicker]'); |
|||
}); |
|||
}; |
|||
|
|||
return { |
|||
initModal: initModal |
|||
} |
|||
}; |
|||
}); |
|||
``` |
|||
|
|||
The requested data is as follows: |
|||
|
|||
```csharp |
|||
Request URL: Meetings/EditModal |
|||
Request Method: POST |
|||
Payload: |
|||
Id: 0803780b-3762-2af8-1c75-3a1895d59c89 |
|||
Meeting.Subject: ABP Developer Guide |
|||
Meeting.StartTime: 2025-03-01T09:30:00.000Z |
|||
Meeting.EndTime: 2025-03-01T10:30:00.000Z |
|||
Meeting.ActualStartTime: 2025-03-01T11:30:00.000Z |
|||
Meeting.CanceledTime: |
|||
Meeting.ReminderTime: 2025-03-01T12:30:00.000Z |
|||
Meeting.FollowUpTime: 2025-03-01T13:30:00.000Z |
|||
Meeting.Description: We will discuss the ABP developer guide. |
|||
``` |
|||
|
|||
 |
|||
|
|||
In short, we use the `abp.clock.normalizeToLocaleString` method to display time, and use the `abp.clock.normalizeToString` method to modify the time to be submitted. If you submit data via `ajax`, please remember to use the `abp.clock.normalizeToString` method to convert time. |
|||
|
|||
### Handling Timezone in Blazor |
|||
|
|||
We cannot automatically complete some work in `Blazor UI`, we need to inject `IClock` and use the `ConvertToUserTime` and `ConvertToUtc` methods to display and create/update entities. |
|||
|
|||
Below is a complete `Meeting` page, please refer to the usage of `Clock` in it. |
|||
|
|||
```csharp |
|||
@page "/meetings" |
|||
@using Volo.Abp.Application.Dtos |
|||
@using Microsoft.Extensions.Localization |
|||
@using TimeZoneApp.Meetings |
|||
@using TimeZoneApp.Localization |
|||
@using TimeZoneApp.Permissions |
|||
@using Volo.Abp.AspNetCore.Components.Web |
|||
@inject IStringLocalizer<TimeZoneAppResource> L |
|||
@inject AbpBlazorMessageLocalizerHelper<TimeZoneAppResource> LH |
|||
@inherits AbpCrudPageBase<IMeetingAppService, MeetingDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateMeetingDto> |
|||
|
|||
<Card> |
|||
<CardHeader> |
|||
<Row Class="justify-content-between"> |
|||
<Column ColumnSize="ColumnSize.IsAuto"> |
|||
<h2>@L["Meetings"]</h2> |
|||
</Column> |
|||
<Column ColumnSize="ColumnSize.IsAuto"> |
|||
@if (HasCreatePermission) |
|||
{ |
|||
<Button Color="Color.Primary" Clicked="OpenCreateModalAsync">@L["NewMeeting"]</Button> |
|||
} |
|||
</Column> |
|||
</Row> |
|||
</CardHeader> |
|||
<CardBody> |
|||
<DataGrid TItem="MeetingDto" |
|||
Data="Entities" |
|||
ReadData="OnDataGridReadAsync" |
|||
TotalItems="TotalCount" |
|||
ShowPager="true" |
|||
PageSize="PageSize"> |
|||
<DataGridColumns> |
|||
<DataGridEntityActionsColumn TItem="MeetingDto" @ref="@EntityActionsColumn"> |
|||
<DisplayTemplate> |
|||
<EntityActions TItem="MeetingDto" EntityActionsColumn="@EntityActionsColumn"> |
|||
<EntityAction TItem="MeetingDto" |
|||
Text="@L["Edit"]" |
|||
Visible=HasUpdatePermission |
|||
Clicked="() => OpenEditModalAsync(context)" /> |
|||
<EntityAction TItem="MeetingDto" |
|||
Text="@L["Delete"]" |
|||
Clicked="() => DeleteEntityAsync(context)" |
|||
Visible=HasDeletePermission |
|||
ConfirmationMessage="()=>GetDeleteConfirmationMessage(context)" /> |
|||
</EntityActions> |
|||
</DisplayTemplate> |
|||
</DataGridEntityActionsColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.Subject)" |
|||
Caption="@L["Subject"]"></DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.StartTime)" |
|||
Caption="@(L["StartTime"] + "/" + L["EndTime"])"> |
|||
<DisplayTemplate> |
|||
@Clock.ConvertToUserTime(context.StartTime).ToString("yyyy-MM-dd HH:mm:ss") ➡️ @Clock.ConvertToUserTime(context.EndTime).ToString("yyyy-MM-dd HH:mm:ss") |
|||
</DisplayTemplate> |
|||
</DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.ActualStartTime)" |
|||
Caption="@L["ActualStartTime"]"> |
|||
<DisplayTemplate> |
|||
@Clock.ConvertToUserTime(context.ActualStartTime).ToString("yyyy-MM-dd HH:mm:ss") |
|||
</DisplayTemplate> |
|||
</DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.CanceledTime)" |
|||
Caption="@L["CanceledTime"]"> |
|||
<DisplayTemplate> |
|||
@(context.CanceledTime.HasValue ? Clock.ConvertToUserTime(context.CanceledTime.Value).ToString("yyyy-MM-dd HH:mm:ss") : "N/A") |
|||
</DisplayTemplate> |
|||
</DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.ReminderTime)" |
|||
Caption="@L["ReminderTime"]"> |
|||
<DisplayTemplate> |
|||
@(Clock.ConvertToUserTime(context.ReminderTime).ToString("yyyy-MM-dd HH:mm:ss") ) |
|||
</DisplayTemplate> |
|||
</DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.FollowUpTime)" |
|||
Caption="@L["FollowUpTime"]"> |
|||
<DisplayTemplate> |
|||
@(context.FollowUpTime.HasValue ? Clock.ConvertToUserTime(context.FollowUpTime.Value).ToString("yyyy-MM-dd HH:mm:ss") : "N/A") |
|||
</DisplayTemplate> |
|||
</DataGridColumn> |
|||
<DataGridColumn TItem="MeetingDto" |
|||
Field="@nameof(MeetingDto.Description)" |
|||
Caption="@L["Description"]"> |
|||
</DataGridColumn> |
|||
</DataGridColumns> |
|||
</DataGrid> |
|||
</CardBody> |
|||
</Card> |
|||
|
|||
<Modal @ref="@CreateModal"> |
|||
<ModalContent IsCentered="true"> |
|||
<Form> |
|||
<ModalHeader> |
|||
<ModalTitle>@L["NewMeeting"]</ModalTitle> |
|||
<CloseButton Clicked="CloseCreateModalAsync"/> |
|||
</ModalHeader> |
|||
<ModalBody> |
|||
<Validations @ref="@CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false"> |
|||
<Validation MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@L["Subject"]</FieldLabel> |
|||
<TextEdit @bind-Text="@NewEntity.Subject"> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</TextEdit> |
|||
</Field> |
|||
</Validation> |
|||
<Field> |
|||
<FieldLabel>@L["StartTime"] / @L["EndTime"]</FieldLabel> |
|||
<DatePicker TValue="DateTime?" @bind-Dates="SelectedDates" InputMode="DateInputMode.DateTime" SelectionMode="DateInputSelectionMode.Range" /> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["ActualStartTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime" @bind-Date="NewEntity.ActualStartTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["CanceledTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime?" @bind-Date="NewEntity.CanceledTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["ReminderTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTimeOffset" @bind-Date="NewEntity.ReminderTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["FollowUpTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTimeOffset?" @bind-Date="NewEntity.FollowUpTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Validation MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@L["Description"]</FieldLabel> |
|||
<TextEdit @bind-Text="@NewEntity.Description"> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</TextEdit> |
|||
</Field> |
|||
</Validation> |
|||
</Validations> |
|||
</ModalBody> |
|||
<ModalFooter> |
|||
<Button Color="Color.Secondary" |
|||
Clicked="CloseCreateModalAsync">@L["Cancel"]</Button> |
|||
<Button Color="Color.Primary" |
|||
Type="@ButtonType.Submit" |
|||
PreventDefaultOnSubmit="true" |
|||
Clicked="CreateEntityAsync">@L["Save"]</Button> |
|||
</ModalFooter> |
|||
</Form> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<Modal @ref="@EditModal"> |
|||
<ModalContent IsCentered="true"> |
|||
<Form> |
|||
<ModalHeader> |
|||
<ModalTitle>@EditingEntity.Subject</ModalTitle> |
|||
<CloseButton Clicked="CloseEditModalAsync"/> |
|||
</ModalHeader> |
|||
<ModalBody> |
|||
<Validations @ref="@EditValidationsRef" Model="@EditingEntity" ValidateOnLoad="false"> |
|||
<Validation MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@L["Subject"]</FieldLabel> |
|||
<TextEdit @bind-Text="@EditingEntity.Subject"> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</TextEdit> |
|||
</Field> |
|||
</Validation> |
|||
<Field> |
|||
<FieldLabel>@L["StartTime"] / @L["EndTime"]</FieldLabel> |
|||
<DatePicker TValue="DateTime?" @bind-Dates="SelectedDates" InputMode="DateInputMode.DateTime" SelectionMode="DateInputSelectionMode.Range" /> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["ActualStartTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime" @bind-Date="EditingEntity.ActualStartTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["CanceledTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTime?" @bind-Date="EditingEntity.CanceledTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["ReminderTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTimeOffset" @bind-Date="EditingEntity.ReminderTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Field> |
|||
<FieldLabel>@L["FollowUpTime"]</FieldLabel> |
|||
<DateEdit TValue="DateTimeOffset?" @bind-Date="EditingEntity.FollowUpTime" InputMode="DateInputMode.DateTime"/> |
|||
</Field> |
|||
<Validation MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@L["Description"]</FieldLabel> |
|||
<TextEdit @bind-Text="@EditingEntity.Description"> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</TextEdit> |
|||
</Field> |
|||
</Validation> |
|||
</Validations> |
|||
</ModalBody> |
|||
<ModalFooter> |
|||
<Button Color="Color.Secondary" |
|||
Clicked="CloseEditModalAsync">@L["Cancel"]</Button> |
|||
<Button Color="Color.Primary" |
|||
Type="@ButtonType.Submit" |
|||
PreventDefaultOnSubmit="true" |
|||
Clicked="UpdateEntityAsync">@L["Save"]</Button> |
|||
</ModalFooter> |
|||
</Form> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
|
|||
@code { |
|||
IReadOnlyList<DateTime?> SelectedDates; |
|||
|
|||
public Meeting() |
|||
{ |
|||
CreatePolicyName = TimeZoneAppPermissions.Meetings.Create; |
|||
UpdatePolicyName = TimeZoneAppPermissions.Meetings.Edit; |
|||
DeletePolicyName = TimeZoneAppPermissions.Meetings.Delete; |
|||
} |
|||
|
|||
protected override async Task OpenCreateModalAsync() |
|||
{ |
|||
await base.OpenCreateModalAsync(); |
|||
|
|||
var now = DateTime.Now; |
|||
SelectedDates = new List<DateTime?> { now.Date.AddHours(10),now.Date.AddDays(7).AddHours(10) }; |
|||
NewEntity.ActualStartTime = now.Date.AddHours(11); |
|||
NewEntity.CanceledTime = now.Date.AddHours(12); |
|||
NewEntity.ReminderTime = now.Date.AddHours(13); |
|||
NewEntity.FollowUpTime = now.Date.AddHours(14); |
|||
} |
|||
|
|||
protected override Task OnCreatingEntityAsync() |
|||
{ |
|||
if (SelectedDates.Count == 2 && SelectedDates[0].HasValue && SelectedDates[1].HasValue) |
|||
{ |
|||
NewEntity.StartTime = Clock.ConvertToUtc(SelectedDates[0]!.Value); |
|||
NewEntity.EndTime = Clock.ConvertToUtc(SelectedDates[1]!.Value); |
|||
} |
|||
|
|||
NewEntity.ActualStartTime = Clock.ConvertToUtc(NewEntity.ActualStartTime); |
|||
NewEntity.CanceledTime = NewEntity.CanceledTime.HasValue ? Clock.ConvertToUtc(NewEntity.CanceledTime.Value) : null; |
|||
|
|||
NewEntity.ReminderTime = Clock.ConvertToUtc(NewEntity.ReminderTime.DateTime); |
|||
NewEntity.FollowUpTime = NewEntity.FollowUpTime.HasValue ? Clock.ConvertToUtc(NewEntity.FollowUpTime.Value.DateTime) : null; |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
protected override async Task OpenEditModalAsync(MeetingDto entity) |
|||
{ |
|||
await base.OpenEditModalAsync(entity); |
|||
|
|||
SelectedDates = new List<DateTime?> { Clock.ConvertToUserTime(EditingEntity.StartTime), Clock.ConvertToUserTime(EditingEntity.EndTime) }; |
|||
EditingEntity.ActualStartTime = Clock.ConvertToUserTime(EditingEntity.ActualStartTime); |
|||
EditingEntity.CanceledTime = EditingEntity.CanceledTime.HasValue ? Clock.ConvertToUserTime(EditingEntity.CanceledTime.Value) : null; |
|||
EditingEntity.ReminderTime = Clock.ConvertToUserTime(EditingEntity.ReminderTime); |
|||
EditingEntity.FollowUpTime = EditingEntity.FollowUpTime.HasValue ? Clock.ConvertToUserTime(EditingEntity.FollowUpTime.Value) : null; |
|||
} |
|||
|
|||
protected override Task OnUpdatingEntityAsync() |
|||
{ |
|||
if (SelectedDates.Count == 2 && SelectedDates[0].HasValue && SelectedDates[1].HasValue) |
|||
{ |
|||
EditingEntity.StartTime = Clock.ConvertToUtc(SelectedDates[0]!.Value); |
|||
EditingEntity.EndTime = Clock.ConvertToUtc(SelectedDates[1]!.Value); |
|||
} |
|||
|
|||
EditingEntity.ActualStartTime = Clock.ConvertToUtc(EditingEntity.ActualStartTime); |
|||
EditingEntity.CanceledTime = EditingEntity.CanceledTime.HasValue ? Clock.ConvertToUtc(EditingEntity.CanceledTime.Value) : null; |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
## Timezone Settings Change |
|||
|
|||
If the timezone settings change, then all times will be converted to the new timezone. For example, if the current timezone changes from `Europe/Istanbul` to `Europe/Berlin`, then all times will be converted to the `Europe/Berlin` timezone. |
|||
|
|||
 |
|||
|
|||
`Europe/Istanbul`: |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
`Europe/Berlin`: |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
## Browser Timezone Detection |
|||
|
|||
When no timezone setting is configured, ABP's MVC, Blazor, and Angular applications will automatically detect the browser's timezone during initialization. The detected timezone is then stored in either the request's Cookie or Header. |
|||
|
|||
This functionality is implemented by the `UseAbpTimeZone` middleware, which follows a specific order to determine the appropriate timezone: |
|||
|
|||
1. First, it attempts to retrieve the timezone from the application/tenant/user settings |
|||
2. If no setting is found, it tries to get the timezone from the request information, including Cookie, Header, QueryString, and Form |
|||
3. Finally, if no timezone information is found, it falls back to using the server's timezone as the default |
|||
|
|||
> The timezone information is stored using the key `__timezone` |
|||
|
|||
## TimeZoneApp Source Code |
|||
|
|||
You can download and view the [TimeZoneApp source code](https://github.com/maliming/TimeZone) for detailed implementation. |
|||
|
|||
## Summary |
|||
|
|||
Through this article, we learned how to handle timezone in different types of UIs. I hope this article is helpful to you. If you have any questions, please contact me at any time. |
|||
@ -0,0 +1,115 @@ |
|||
# How to Change the CurrentUser in ABP? |
|||
|
|||
[ABP Framework](https://abp.io/) provides a powerful service for accessing information about the currently authenticated user in your application. Understanding how to use and modify this service (`ICurrentUser`) is essential for both basic and certain advanced scenarios. |
|||
|
|||
In this article, we'll explore the [`CurrentUser` service](https://abp.io/docs/latest/framework/infrastructure/current-user), its use cases, and how to change it when necessary. |
|||
|
|||
## Understanding the ICurrentUser Service |
|||
|
|||
The `ICurrentUser` interface is the primary service in ABP Framework for obtaining information about the logged-in user. It provides some key properties, such as `Id`, `UserName`, `TenantId`, `Roles` (roleNames), and more... |
|||
|
|||
`ICurrentUser` is implemented on the `ICurrentPrincipalAccessor` service and works with claims as well. So, all of these properties are actually retrieved from the claims. ICurrentUser has some methods to directly work with the claims, such as: |
|||
|
|||
* FindClaim (finds a single claim by name) |
|||
* FindClaims (gets all claims with the given name) |
|||
* IsInRole (checks if the user has a specific role) |
|||
* GetAllClaims (gets all claims of the user) |
|||
|
|||
## Where the CurrentUser Service is Used? |
|||
|
|||
The CurrentUser service is used extensively throughout ABP applications whenever there's a need to access information about the logged-in user. Common scenarios include: authorization checks, logging, setting common properties like `CreatorId`, `LastModifierId`, `DeleterId`, and more... |
|||
|
|||
## When to Change the CurrentUser Service? |
|||
|
|||
While the CurrentUser service works automatically in the context of HTTP requests (it gets the `User` property of the current `HttpContext`), there are advanced scenarios where you might need to manually set or change the current user: |
|||
|
|||
1. **Background workers:** When executing code outside the context of a user request |
|||
2. **Event handlers:** When processing events that may run in a different context |
|||
3. **Unit & integration tests:** When simulating a user for testing purposes |
|||
|
|||
## How to Change the CurrentUser Service? |
|||
|
|||
If you need to change the CurrentUser service, you can inject the `ICurrentPrincipalAccessor` service, use its `Change` method to change the current user, and then use the `CurrentUser` service as usual. |
|||
|
|||
Here's how to change the current user for a specific scope: |
|||
|
|||
```csharp |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
using Volo.Abp.Identity; |
|||
using Volo.Abp.Security.Claims; |
|||
|
|||
namespace MyProject.Products; |
|||
|
|||
public class ProductEventHandler : IDistributedEventHandler<OrderPlacedEto>, ITransientDependency |
|||
{ |
|||
private readonly IProductRepository _productRepository; |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
private readonly IdentityUserManager _userManager; |
|||
|
|||
public ProductEventHandler( |
|||
IProductRepository productRepository, |
|||
ICurrentPrincipalAccessor currentPrincipalAccessor, |
|||
IdentityUserManager userManager |
|||
) |
|||
{ |
|||
_productRepository = productRepository; |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
_userManager = userManager; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(OrderPlacedEto eventData) |
|||
{ |
|||
var product = await _productRepository.FindAsync(eventData.ProductId); |
|||
if (product == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
//Get the admin user |
|||
var adminUser = await _userManager.FindByNameAsync("admin"); |
|||
if (adminUser == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var newPrincipal = new ClaimsPrincipal(new ClaimsIdentity( |
|||
new Claim[] { |
|||
new Claim(AbpClaimTypes.UserId, adminUser.Id.ToString()), |
|||
new Claim(AbpClaimTypes.UserName, "admin"), |
|||
})); |
|||
|
|||
//IMPORTANT: It will set the CreatorId, LastModifierId, etc. with the admin user |
|||
using (_currentPrincipalAccessor.Change(newPrincipal)) |
|||
{ |
|||
product.StockCount -= eventData.Quantity; |
|||
|
|||
// Update the product |
|||
await _productRepository.UpdateAsync(product); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
In this example, we have a distributed event handler that processes an `OrderPlacedEto` event. When an order is placed, we need to update the product's stock count. However, we want this operation to be performed under an admin user's context for auditing purposes. |
|||
|
|||
Here's what the code does step by step: |
|||
|
|||
1. First, it retrieves the product using the product ID from the event data. |
|||
2. Then, it finds the admin user by username using the `_userManager.FindByNameAsync("admin")`. |
|||
3. A new `ClaimsPrincipal` is created with the admin user's claims (`UserId` and `UserName`). |
|||
4. Using the `_currentPrincipalAccessor.Change()` method within a `using` statement, it temporarily changes the current user context to the admin user. |
|||
5. Inside this scope, it updates the product's stock count by subtracting the ordered quantity. |
|||
6. Finally, it saves the changes to the database using the repository. |
|||
|
|||
**The important part here is that any audit properties (like `CreatorId`, `LastModifierId`, etc.) will be set to the admin user's ID because we changed the current principal. Once the using block ends, the original user context is automatically restored.** |
|||
|
|||
This pattern is particularly useful in background jobs, event handlers, or any scenario where you need to perform operations under a specific user's context, regardless of the actual authenticated user. |
|||
|
|||
## Conclusion |
|||
|
|||
The `CurrentUser` service in ABP Framework provides a simple way to access information about the authenticated user. While it works automatically in most scenarios, there are cases where you need to explicitly change the current user identity, particularly in background processing scenarios. |
|||
|
|||
By using the ICurrentPrincipalAccessor.Change() method within a using statement, you can temporarily change the current user for a specific scope of execution, enabling your background processes, event handlers, or tests to execute with the identity of a specific user. |
|||
@ -0,0 +1,38 @@ |
|||
We are excited to introduce the ABP Contributor Program, which is an amazing opportunity for community enthusiasts to contribute to the ABP ecosystem while getting valuable benefits\! |
|||
|
|||
**Create, Contribute and Be Part of Something Bigger** |
|||
|
|||
This is your opportunity to not only share your knowledge but also to grow alongside a passionate community of developers and creators. When you create and contribute to the ABP ecosystem, you’re not just building content, you’re also helping shape the future of ABP. |
|||
|
|||
## **Why Become an ABP Contributor?** |
|||
|
|||
By sharing your knowledge, creating valuable content, and engaging with the ABP Community, you’ll enjoy a range of benefits, including: |
|||
|
|||
* Free ABP Personal License – If you meet all the requirements, you’ll receive an ABP Personal License at no cost |
|||
|
|||
* Community Badge & Title – Stand out in the ABP Community with a special contributor badge and label next to your name. |
|||
|
|||
* Exclusive Discord Role – Get a unique Contributor role on the ABP Community Discord server. |
|||
|
|||
* Increased Visibility – Your contributions will be recognized by hundreds of thousands of developers worldwide. |
|||
|
|||
Completely Free – No fees, just log in, contribute, and start collecting benefits\! |
|||
|
|||
## **How to Apply?** |
|||
|
|||
If you’re passionate about ABP and think you meet the required criteria, follow these simple steps: |
|||
|
|||
Create content related to ABP: blog posts, tutorials, videos, or documentation. You can publish it on the ABP Community website or any external platform (just make sure to submit it to the ABP Community site). |
|||
|
|||
Apply by email: Send your application to marketing@volosoft.com, and the ABP Team will review your eligibility. |
|||
|
|||
Periodic Assessments: The team reviews submissions every three months to determine and renew contributor status. Keep contributing to continue enjoying the perks\! |
|||
|
|||
## **Join Us & Grow\!** |
|||
|
|||
This is your chance to give back to the community, grow your presence, and get rewarded for your contributions. Whether you love writing documentation, sharing tutorials, or building open-source projects, your efforts matter. |
|||
|
|||
Apply today and be part of something impactful\! |
|||
|
|||
|
|||
[image1]: images/img_1.png |
|||
|
Before Width: | Height: | Size: 485 KiB After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1,64 @@ |
|||
# ABP Version 9.2 Migration Guide |
|||
|
|||
This document is a guide for upgrading ABP v9.x solutions to ABP v9.2. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. |
|||
|
|||
## Open-Source (Framework) |
|||
|
|||
### Added `ApplicationName` Property to Isolate Background Jobs & Background Workers |
|||
|
|||
When multiple applications share the same storage for [ABP's Background Jobs Module](../../modules/background-jobs.md), jobs from one application might be visible to another application. This can lead to the following issues: |
|||
|
|||
1. Applications may attempt to process jobs that don't belong to them |
|||
2. These attempts fail with "Undefined background job for the job name" error |
|||
3. Failed jobs are marked as `IsAbandoned = true` |
|||
4. The original application can no longer process these abandoned jobs |
|||
|
|||
To fix this, we added the `ApplicationName` property to the `AbpBackgroundJobWorkerOptions` class. This property allows you to specify the application name, which helps isolate jobs between different applications. (See the [PR](https://github.com/abpframework/abp/pull/22169) for more details.) |
|||
|
|||
**By default there is no breaking change. However, you need to create a migration for the database to add the `ApplicationName` column to the relevant table and apply it to your database.** |
|||
|
|||
### Upgraded `MongoDB.Driver` to `3.1.0` |
|||
|
|||
In this version, we upgraded `MongoDB.Driver` to `3.1.0`. To migrate your application, please refer to our [MongoDB Driver 2 to 3 Migration Guide](./MongoDB-Driver-2-to-3.md) document. |
|||
|
|||
### Replaced Toastr with Custom Implementation (without depending on any 3rd party library) |
|||
|
|||
In this version, we replaced Toastr with a custom implementation that does not depend on any 3rd party library. This is a breaking change if you are using the `Toastr` library. |
|||
|
|||
Here are the migration steps: |
|||
|
|||
1. Remove any direct Toastr dependencies from your application |
|||
2. Update your notification calls to use the new API |
|||
3. Migrate any custom styles or configurations |
|||
|
|||
For detailed implementation guidelines and API documentation, see: |
|||
|
|||
- [Notify Documentation](../../framework/ui/mvc-razor-pages/javascript-api/notify.md) |
|||
- [PR #21940 for more information](https://github.com/abpframework/abp/pull/21940) |
|||
|
|||
|
|||
## PRO |
|||
|
|||
> Please check the **Open-Source (Framework)** section before reading this section. The listed topics might affect your application and you might need to take care of them. |
|||
|
|||
If you are a paid-license owner and using the ABP's paid version, then please follow the following sections to get informed about the breaking changes and apply the necessary ones: |
|||
|
|||
### Identity Pro Module: Require Email Verification to Register |
|||
|
|||
In this version, we added a new setting to the Identity Pro module to require email verification to register. This is a security measure to prevent spam registrations: |
|||
|
|||
 |
|||
|
|||
Typically, no changes are required. However, if you have inherited from the `AccountAppService` class and implemented your own logic, you'll need to update your constructor to match the new signature since two new services are now injected: |
|||
|
|||
```diff |
|||
//code omitted for brevity... |
|||
|
|||
+ protected IDistributedCache<EmailConfirmationCodeCacheItem> EmailConfirmationCodeCache { get; } |
|||
+ protected IdentityErrorDescriber IdentityErrorDescriber { get; } |
|||
|
|||
public AccountAppService( |
|||
- IdentityUserTwoFactorChecker identityUserTwoFactorChecker) |
|||
+ IdentityUserTwoFactorChecker identityUserTwoFactorChecker, |
|||
+ IdentityErrorDescriber identityErrorDescriber) |
|||
``` |
|||
|
After Width: | Height: | Size: 170 KiB |
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -1,4 +1,8 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
@ -1,6 +1,6 @@ |
|||
using Microsoft.AspNetCore.Components.WebView.Maui; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
using Microsoft.Maui; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.MauiBlazor.Bundling; |
|||
|
|||
@ -1,6 +1,9 @@ |
|||
using System; |
|||
using System.IO; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Microsoft.Extensions.Primitives; |
|||
using Microsoft.Maui.Controls.PlatformConfiguration; |
|||
using Microsoft.Maui.Storage; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
@ -1,3 +1,4 @@ |
|||
using System; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.AspNetCore.Bundling; |
|||
using Volo.Abp.AspNetCore.Bundling.Scripts; |
|||
@ -1,3 +1,5 @@ |
|||
using System; |
|||
using System.IO; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.AspNetCore.Bundling; |
|||
using Volo.Abp.AspNetCore.Bundling.Styles; |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.MauiBlazor; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MauiBlazorCurrentTimezoneProvider : ICurrentTimezoneProvider, ISingletonDependency |
|||
{ |
|||
public string? TimeZone { get; set; } |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Timing; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.WebAssembly; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class WebAssemblyCurrentTimezoneProvider : ICurrentTimezoneProvider, ISingletonDependency |
|||
{ |
|||
public string? TimeZone { get; set; } |
|||
} |
|||