|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 43 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,170 @@ |
|||
# Common Errors in JWT Bearer Authentication |
|||
|
|||
When implementing JWT Bearer authentication in an ABP(tiered) application, you might occasionally encounter errors starting with `IDX`. These errors are related to JWT Bearer Token validation and this article will help you understand and resolve them. |
|||
|
|||
## Enable JWT Bearer authentication |
|||
|
|||
Your API project usually contains the following code, which enables JWT Bearer authentication and makes it as the default authentication scheme. |
|||
|
|||
We simply configure the JWT's `Authority` and `Audience` properties, and it will work fine. |
|||
|
|||
```csharp |
|||
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
|||
.AddJwtBearer(options => |
|||
{ |
|||
options.Authority = "https://localhost:44301/"; //configuration["AuthServer:Authority"]; |
|||
options.Audience = "MyProjectName"; |
|||
}); |
|||
``` |
|||
|
|||
> `AddJwtBearer` and `AddAbpJwtBearer` will do the same thing, but `AddAbpJwtBearer` is recommended. |
|||
|
|||
## JWT authentication process |
|||
|
|||
Let's take a look at how the above code works. |
|||
|
|||
A JWT Token usually consists of three parts: `Header`, `Payload`, and `Signature`. |
|||
|
|||
- `Header`: Contains the type and signing algorithm of the token |
|||
- `Payload`: Contains the claims of the token, including `sub`, `aud`, `exp`, `iat`, `iss`, `jti`, `preferred_username`, `given_name`, `role`, `email`, etc. |
|||
- `Signature`: The cryptographic signature of the token used to verify its authenticity |
|||
|
|||
Here is an example of a JWT Token issued by `AuthServer(OpenIddict)`: |
|||
|
|||
The `Header` part: |
|||
|
|||
 |
|||
|
|||
The `Payload` part: |
|||
|
|||
 |
|||
|
|||
### TokenValidationParameters |
|||
|
|||
In the `JwtBearerOptions`, there is a `TokenValidationParameters` property, which is used to validate the JWT Token. |
|||
|
|||
The default implementation for JWT Token validation is `JsonWebTokenHandler`, which comes from the [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/) package. |
|||
|
|||
We didn't set the `TokenValidationParameters` property in the code above, so the default values below will be used: |
|||
|
|||
```csharp |
|||
//... |
|||
TokenValidationParameters.ValidateAudience = true |
|||
TokenValidationParameters.ValidAudience = "MyProjectName" |
|||
TokenValidationParameters.ValidAudiences = null |
|||
|
|||
TokenValidationParameters.ValidateIssuer = true |
|||
TokenValidationParameters.ValidIssuer = null |
|||
TokenValidationParameters.ValidIssuers = null |
|||
//... |
|||
``` |
|||
|
|||
### JWT Bearer Token Validation Process |
|||
|
|||
During JWT Bearer authentication, API website will get the token from the HTTP request and validate it. |
|||
|
|||
The `JsonWebTokenHandler` will get the `OpenID Connect` metadata from the `AuthServer`, it will be used in the validation process, the current metadata request address is: https://localhost:44301/.well-known/openid-configuration , it is a fixed address calculated from the `Authority` property. |
|||
|
|||
First, the token's Signature is verified using the public key obtained from `OpenID Connect` metadata(https://localhost:44301/.well-known/jwks). |
|||
|
|||
Then, the payload is validated. The payload is a JSON object containing essential information such as the `token type`, `expiration time`, `issuer`, and `audience` etc. |
|||
|
|||
Most of the validation problems we may encounter are payload validation failures, for example: |
|||
|
|||
#### Lifetime |
|||
|
|||
If the token in your request has expired, the validation will fail. You will see the exception information like `IDX10230` in the log. |
|||
|
|||
#### Audience |
|||
|
|||
The `ValidAudience` of `TokenValidationParameters` is `MyProjectName`, the `aud` in the payload of the token is also `MyProjectName`, if the token does not contain `aud` or the `aud` does not match, the validation will fail. You may see the exception information like `IDX10206`, `IDX10277` or `IDX10208`. |
|||
|
|||
> If the `ValidateAudience` of `TokenValidationParameters` is `false`, then the `aud` will not be validated. |
|||
|
|||
#### Issuer |
|||
|
|||
The default value of `TokenValidationParameters.ValidateIssuer` is `true`, it requires the token's payload to contain the `issuer` field, and it must match one of `TokenValidationParameters.ValidIssuer` or `TokenValidationParameters.ValidIssuers`. |
|||
|
|||
> The default value of `ValidIssuer` or `ValidIssuers` is `null`, it will use the `issuer` from the `OpenID Connect` metadata as the default value. |
|||
|
|||
1. If the token's payload does not contain the `issuer` field, you may see the error `IDX10211`. |
|||
2. If the API website cannot get the `OpenID Connect` metadata from AuthServer website, the validation will fail. You may see the error `IDX10204`, the full exception message is: `IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty.` |
|||
3. If the `issuer` does not match, the validation will fail. You may see the error `IDX10205` in the log. |
|||
|
|||
> If the `ValidateIssuer` of `TokenValidationParameters` is `false`, then the `issuer` will not be validated. |
|||
|
|||
> Please note that `OpenIddict` will use the current HTTP request information as the value of `issuer`. If the AuthServer website is deployed behind a reverse proxy or similar deployment configurations, the `issuer` in the token may not be the value you expect. In this case, please specify it manually. |
|||
|
|||
```csharp |
|||
PreConfigure<OpenIddictServerBuilder>(serverBuilder => |
|||
{ |
|||
serverBuilder.SetIssuer("https://localhost:44301/"); |
|||
}); |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
To troubleshoot any `IDX` errors during JWT authentication, you can enable detailed logging by configuring the `identitymodel` logs as follows: |
|||
|
|||
```csharp |
|||
using System.Diagnostics.Tracing; |
|||
using Microsoft.IdentityModel.Logging; |
|||
|
|||
public class Program |
|||
{ |
|||
public async static Task<int> Main(string[] args) |
|||
{ |
|||
IdentityModelEventSource.ShowPII = true; |
|||
IdentityModelEventSource.Logger.LogLevel = EventLevel.Verbose; |
|||
var wilsonTextLogger = newTextWriterEventListener("Logs/identitymodel.txt"); |
|||
wilsonTextLogger.EnableEvents(IdentityModelEventSource.Logger, EventLevel.Verbose); |
|||
|
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Additionally, you can enable `OpenIddict`'s `Verbose` logs for more detailed debugging information: |
|||
|
|||
```csharp |
|||
var loggerConfiguration = new LoggerConfiguration() |
|||
.MinimumLevel.Debug() |
|||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) |
|||
.MinimumLevel.Override("OpenIddict", LogEventLevel.Verbose) |
|||
.Enrich.FromLogContext() |
|||
.WriteTo.Async(c => c.File("Logs/logs.txt")) |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
For JWT authentication, you need to pay attention to the following key points: |
|||
|
|||
1. Ensure your API website can communicate with the AuthServer properly |
|||
2. Verify that the `aud` claim in your token matches the expected audience |
|||
3. Confirm that the `issuer` claim in your token is valid and matches the configuration |
|||
|
|||
You can customize the `JwtBearerOptions`'s `TokenValidationParameters` to modify the validation rules to meet your actual needs. |
|||
|
|||
For example, if your `issuer` needs to support multiple subdomains, you can use the [Owl.TokenWildcardIssuerValidator](https://github.com/maliming/Owl.TokenWildcardIssuerValidator) library to customize the validation. |
|||
|
|||
```csharp |
|||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) |
|||
.AddJwtBearer(options => |
|||
{ |
|||
options.Authority = "https://abp.io"; |
|||
options.Audience = "abp_io"; |
|||
|
|||
options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; |
|||
options.TokenValidationParameters.ValidIssuers = new[] |
|||
{ |
|||
"https://{0}.abp.io" |
|||
}; |
|||
}); |
|||
``` |
|||
|
|||
## References |
|||
|
|||
- [Configure JWT bearer authentication in ASP.NET Core]([https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-auth?view=aspnetcore-8.0](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/configure-jwt-bearer-authentication)) |
|||
- [OpenIddict](https://github.com/openiddict/openiddict-core) |
|||
- [IdentityModel](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) |
|||
- [Owl.TokenWildcardIssuerValidator](https://github.com/maliming/Owl.TokenWildcardIssuerValidator) |
|||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 142 KiB |
@ -0,0 +1,262 @@ |
|||
# Using Microsoft AI Extensions Library and OpenAI to Summarize User Comments |
|||
|
|||
Either you are building an e-commerce application or a simple blog, **user comments** (about your products or blog posts) **can grow rapidly**, making it harder for users to get the gist of discussions at a glance. AI is a pretty good tool to solve the problem. By using AI, you can **summarize all the user comments** and show a single paragraph to your users, so they can easily understand the overall thought of users about the product or the blog post. |
|||
|
|||
In this tutorial, we’ll walk through a real-life implementation of using AI to summarize multiple user comments in an application. I will implement the solution based on ABP's **[CMS Kit](https://abp.io/docs/latest/modules/cms-kit)** library, as it already features a **[commenting system](https://abp.io/docs/latest/modules/cms-kit/comments)** and a [demo application](https://cms-kit-demo.abpdemo.com/) that displays user comments on **[gallery images](https://cms-kit-demo.abpdemo.com/image-gallery)** (it has not a comment summary feature yet, we will implement it in this tutorial). |
|||
|
|||
## A Screenshot |
|||
|
|||
Here, an example screenshot from the application with the comment summary feature: |
|||
|
|||
 |
|||
|
|||
## Cloning the Repository |
|||
|
|||
If you want to follow the development, you can clone the [CMS Kit Demo repository](https://github.com/abpframework/cms-kit-demo) to your computer and make it running by following the instructions on the [README file](https://github.com/abpframework/cms-kit-demo?tab=readme-ov-file#cms-kit-demo). |
|||
|
|||
I suggest to you to play a little with [the application](https://cms-kit-demo.abpdemo.com/) (create a new user for yourself, add some comments to the images in the gallery), so you understand how it works. |
|||
|
|||
## Preparing the Solution for AI |
|||
|
|||
I will use [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) to use the AI features. It is an abstraction library that can work with multiple AI models and tools. I will use an OpenAI model in the demo. |
|||
|
|||
The first step is to add the [Microsoft.Extensions.AI.OpenAI](http://nuget.org/packages/Microsoft.Extensions.AI.OpenAI) NuGet package to the project: |
|||
|
|||
````bash |
|||
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease |
|||
```` |
|||
|
|||
>The Microsoft AI Extensions Library was in preview at the time when I wrote this article. If it has a stable release now, you can remove the `--prerelease` parameter for the preceding command. |
|||
|
|||
We will store the OpenAI key and model name in user secrets. So, locate the root path of the CMS Kit project (`src\CmsKitDemo` folder) and execute the following commands in order in a command-line terminal: |
|||
|
|||
````bash |
|||
dotnet user-secrets init |
|||
dotnet user-secrets set OpenAIKey <your-openai-key> |
|||
dotnet user-secrets set ModelName <your-openai-model-name> |
|||
```` |
|||
|
|||
For this example, you need to have an [OpenAI API Key](https://platform.openai.com/). That's all. Now, we are ready to use the AI. |
|||
|
|||
## Implementing the AI Summarization |
|||
|
|||
Let's start from the most important point of this article: Comment summarization. I will create a class named `AiCommentSummarizer` to implement the summarization work. Here, the full content of that class: |
|||
|
|||
````csharp |
|||
using System.Text; |
|||
using Microsoft.Extensions.AI; |
|||
using OpenAI; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace CmsKitDemo.Utils; |
|||
|
|||
public class AiCommentSummarizer : ITransientDependency |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
public AiCommentSummarizer(IConfiguration configuration) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
public async Task<string> SummarizeAsync(string[] commentTexts) |
|||
{ |
|||
// Get the model and key from the configuration |
|||
var aiModel = _configuration["ModelName"]; |
|||
var apiKey = _configuration["OpenAIKey"]; |
|||
|
|||
if (aiModel.IsNullOrEmpty() || apiKey.IsNullOrEmpty()) |
|||
{ |
|||
return ""; |
|||
} |
|||
|
|||
// Create the IChatClient |
|||
var client = new OpenAIClient(apiKey) |
|||
.GetChatClient(aiModel) |
|||
.AsIChatClient(); |
|||
|
|||
// Create a prompt (input for AI) |
|||
var promptBuilder = new StringBuilder(); |
|||
|
|||
promptBuilder.AppendLine( |
|||
@"There are comments from different users of our website about an image. |
|||
We want to summarize the comments into a single comment. |
|||
Return a single comment with a maximum of 512 characters. Comments are separated by a newline character and given below." |
|||
); |
|||
promptBuilder.AppendLine(); |
|||
|
|||
foreach (var commentText in commentTexts) |
|||
{ |
|||
promptBuilder.AppendLine("User comment:"); |
|||
promptBuilder.AppendLine(commentText); |
|||
promptBuilder.AppendLine(); |
|||
} |
|||
|
|||
// Submit the prompt and get the response |
|||
var response = await client.GetResponseAsync( |
|||
promptBuilder.ToString(), |
|||
new ChatOptions { MaxOutputTokens = 1024 } |
|||
); |
|||
|
|||
return response.Text; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
That class is pretty simple and already decorated with comments: |
|||
|
|||
* First, we are getting the API Key and an OpenAI model name from user secrets. I used `gpt-4.1` as the model name, but you can use another available model. |
|||
* Then we are obtaining an `IChatClient` reference for OpenAI. `IChatClient` interface is an abstraction that is provided by the [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) library, so we can implement rest of the code independently from OpenAI. |
|||
* Then we continue by building a proper prompt (input) for the AI operation. |
|||
* And finally we are using the AI to generate a response (the summary). |
|||
|
|||
At this point, all the AI-related work has already been done. The rest of this article explains how to integrate that summarization feature with the [CMS Kit Demo application](https://cms-kit-demo.abpdemo.com/). |
|||
|
|||
## Adding a CommentsSummary Property to the GalleryImage Entity |
|||
|
|||
The `GalleryImage` entity is used to represent an image on [the image gallery](https://cms-kit-demo.abpdemo.com/image-gallery). I add a `CommentsSummary` property to that entity: |
|||
|
|||
````csharp |
|||
public class GalleryImage : CreationAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Description { get; set; } |
|||
|
|||
public Guid CoverImageMediaId { get; set; } |
|||
|
|||
public string CommentsSummary { get; set; } // The new property is here |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
Since the CMS Kit Demo application uses Entity Framework Core, I need to add a new database schema migration and update the database: |
|||
|
|||
````bash |
|||
dotnet ef migrations add Added_Summary_To_GalleryImage |
|||
dotnet ef database update |
|||
```` |
|||
|
|||
## Updating the Summary |
|||
|
|||
Great, we have a `GalleryImage.CommentsSummary` property now. But, how will it be updated when a users adds or removes a comment for an image? To implement that; |
|||
|
|||
* We will listen all the change events for user comments (when a user adds, removes or updates a comment). |
|||
* Whenever a comment is changed, we will find the related gallery image, retrieve all the user comments for this image, use the `AiCommentSummarizer` class to summarize all the comments. |
|||
* Finally, we wil set the `GalleryImage.CommentsSummary` property with the generated summary text. |
|||
|
|||
Here, the implementation: |
|||
|
|||
````csharp |
|||
using CmsKitDemo.Entities; |
|||
using CmsKitDemo.Utils; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Entities.Events; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.CmsKit.Comments; |
|||
|
|||
namespace CmsKitDemo.EventHandlers; |
|||
|
|||
public class GalleryImageCommentListener : |
|||
ILocalEventHandler<EntityChangedEventData<Comment>>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IRepository<GalleryImage, Guid> _galleryImageRepository; |
|||
private readonly IRepository<Comment, Guid> _commentRepository; |
|||
private readonly AiCommentSummarizer _aiCommentSummarizer; |
|||
|
|||
public GalleryImageCommentListener( |
|||
IRepository<GalleryImage, Guid> galleryImageRepository, |
|||
IRepository<Comment, Guid> commentRepository, |
|||
AiCommentSummarizer aiCommentSummarizer) |
|||
{ |
|||
_galleryImageRepository = galleryImageRepository; |
|||
_commentRepository = commentRepository; |
|||
_aiCommentSummarizer = aiCommentSummarizer; |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityChangedEventData<Comment> eventData) |
|||
{ |
|||
var comment = eventData.Entity; |
|||
|
|||
//Here, we only interest in comments related to image gallery items |
|||
if (comment.EntityType != CmsKitDemoConsts.ImageGalleryEntityType) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (!Guid.TryParse(comment.EntityId, out var galleryImageId)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Get the related image from database |
|||
var galleryImage = await _galleryImageRepository.FindAsync(galleryImageId); |
|||
if (galleryImage == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// Get all the comments related to the image |
|||
var queryable = await _commentRepository.GetQueryableAsync(); |
|||
var allCommentTexts = await queryable |
|||
.Where(c => c.EntityType == CmsKitDemoConsts.ImageGalleryEntityType && |
|||
c.EntityId == comment.EntityId) |
|||
.Select(c => c.Text) |
|||
.ToArrayAsync(); |
|||
|
|||
// Update the summary of comments related to the image |
|||
if (allCommentTexts.Length <= 0) |
|||
{ |
|||
galleryImage.CommentsSummary = ""; |
|||
} |
|||
else |
|||
{ |
|||
galleryImage.CommentsSummary = |
|||
await _aiCommentSummarizer.SummarizeAsync(allCommentTexts); |
|||
} |
|||
|
|||
// Update the image in database |
|||
await _galleryImageRepository.UpdateAsync(galleryImage); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Let's explain that class: |
|||
|
|||
* `GalleryImageCommentListener` implements the `ILocalEventHandler<EntityChangedEventData<Comment>>` interface. In this way, it can handle an event whenever a `Comment` [entity](https://abp.io/docs/latest/framework/architecture/domain-driven-design/entities) is changed (created, updated or deleted). We are using ABP's [local event bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/local) and its [pre-defined events](https://abp.io/docs/latest/framework/infrastructure/event-bus/local#pre-built-events). |
|||
* `HandleEventAsync` is called by the ABP Framework whenever a new `Comment` is created, or an existing `Comment` is deleted or updated. |
|||
* ABP's `Comment` entity is reusable and it can be associated with any kind of objects (blog posts, images, etc). So, first we are checking if this comment is related to an image gallery item. |
|||
* Then we are getting the related `GalleryImage` entity from the database. |
|||
* And getting all comments (including the new one) from the database for this image. |
|||
* Finally, using the `AiCommentSummarizer` class to generate the summary and set the `CommentsSummary` property. |
|||
|
|||
## Show the Summary Card on the UI |
|||
|
|||
Everything is ready on the backend. Now, we can show the summary text on the user interface. To do, that, I added `CommentsSummary` property also to the `GalleryImageDto` class and used it on the `/Pages/Gallery/Detail.cshtml` view: |
|||
|
|||
````csharp |
|||
@if (!Model.Image.CommentsSummary.IsNullOrEmpty()) |
|||
{ |
|||
<div class="card mt-3"> |
|||
<div class="card-body"> |
|||
<h6 class="card-title">Summary of the User Comments</h6> |
|||
<p class="mb-auto">@Model.Image.CommentsSummary</p> |
|||
</div> |
|||
</div> |
|||
} |
|||
```` |
|||
|
|||
That section renders the following card on the user interface: |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I demonstrated how to use [Microsoft AI Extensions Library](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) to work with OpenAI for summarization of multiple user comments. I reused the [ABP's CMS Kit Demo application](https://github.com/abpframework/cms-kit-demo) to show it in a more real world example. |
|||
|
|||
## Source Code |
|||
|
|||
* [Source code of the CMS Kit Demo application](https://github.com/abpframework/cms-kit-demo) |
|||
* [All the changes made for this article (as a pull request)](https://github.com/abpframework/cms-kit-demo/pull/18) |
|||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,268 @@ |
|||
# Resolving Tenant from Route in ABP Framework |
|||
|
|||
The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more. |
|||
|
|||
This article will demonstrate how to resolve tenant information from the route. |
|||
|
|||
## Tenant Information in Routes |
|||
|
|||
In the ABP Framework, tenant information in routes is handled by the `RouteTenantResolveContributor`. |
|||
|
|||
Let's say your application is hosted at `https://abp.io` and you have a tenant named `acme`. You can add the `{__tenant}` variable to your controller or page routes like this: |
|||
|
|||
```csharp |
|||
[Route("{__tenant}/[Controller]")] |
|||
public class MyController : MyProjectNameController |
|||
{ |
|||
[HttpGet] |
|||
public IActionResult Get() |
|||
{ |
|||
return Ok("Hello My Page"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
```cshtml |
|||
@page "{__tenant?}/mypage" |
|||
@model MyPageModel |
|||
|
|||
<html> |
|||
<body> |
|||
<h1>My Page</h1> |
|||
</body> |
|||
</html> |
|||
``` |
|||
|
|||
When you access `https://abp.io/acme/my` or `https://abp.io/acme/mypage`, ABP will automatically resolve the tenant information from the route. |
|||
|
|||
## Adding __tenant to Global Routes |
|||
|
|||
While we've shown how to add `{__tenant}` to individual controllers or pages, you might want to add it globally to your entire application. Here's how to implement this: |
|||
|
|||
```cs |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Microsoft.AspNetCore.Mvc.ApplicationModels; |
|||
|
|||
namespace MyCompanyName; |
|||
|
|||
public class AddTenantRouteToPages : IPageRouteModelConvention, IApplicationModelConvention |
|||
{ |
|||
public void Apply(PageRouteModel model) |
|||
{ |
|||
var selectorCount = model.Selectors.Count; |
|||
var selectorModels = new List<SelectorModel>(); |
|||
for (var i = 0; i < selectorCount; i++) |
|||
{ |
|||
var selector = model.Selectors[i]; |
|||
selectorModels.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[a-zA-Z0-9]+$)}", selector.AttributeRouteModel!.Template!.RemovePreFix("/")) |
|||
} |
|||
}); |
|||
} |
|||
foreach (var selectorModel in selectorModels) |
|||
{ |
|||
model.Selectors.Add(selectorModel); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class AddTenantRouteToControllers :IApplicationModelConvention |
|||
{ |
|||
public void Apply(ApplicationModel application) |
|||
{ |
|||
var controllers = application.Controllers; |
|||
foreach (var controller in controllers) |
|||
{ |
|||
var selector = controller.Selectors.FirstOrDefault(); |
|||
if (selector == null || selector.AttributeRouteModel == null) |
|||
{ |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", controller.ControllerName) |
|||
} |
|||
}); |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = controller.ControllerName |
|||
} |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
var template = selector.AttributeRouteModel?.Template; |
|||
template = template.IsNullOrWhiteSpace() ? "{__tenant:regex(^[[a-zA-Z0-9]]+$)}" : AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", template.RemovePreFix("/")); |
|||
controller.Selectors.Add(new SelectorModel |
|||
{ |
|||
AttributeRouteModel = new AttributeRouteModel |
|||
{ |
|||
Template = template |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Register the services: |
|||
|
|||
```cs |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
|
|||
PostConfigure<RazorPagesOptions>(options => |
|||
{ |
|||
options.Conventions.Add(new AddTenantRouteToPages()); |
|||
}); |
|||
|
|||
PostConfigure<MvcOptions>(options => |
|||
{ |
|||
options.Conventions.Add(new AddTenantRouteToControllers()); |
|||
}); |
|||
|
|||
// Configure cookie path to prevent authentication cookie loss |
|||
context.Services.ConfigureApplicationCookie(x => |
|||
{ |
|||
x.Cookie.Path = "/"; |
|||
}); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
After implementing this, you'll notice that all controllers in your Swagger UI will have the `{__tenant}` route added: |
|||
|
|||
 |
|||
|
|||
## Handling Navigation Links |
|||
|
|||
To ensure navigation links automatically include tenant information, we need to add middleware that dynamically adds the tenant to the PathBase: |
|||
|
|||
```cs |
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
//... |
|||
app.Use(async (httpContext, next) => |
|||
{ |
|||
var tenantMatch = Regex.Match(httpContext.Request.Path, "^/([^/.]+)(?:/.*)?$"); |
|||
if (tenantMatch.Groups.Count > 1 && !string.IsNullOrEmpty(tenantMatch.Groups[1].Value)) |
|||
{ |
|||
var tenantName = tenantMatch.Groups[1].Value; |
|||
if (!tenantName.IsNullOrWhiteSpace()) |
|||
{ |
|||
var tenantStore = httpContext.RequestServices.GetRequiredService<ITenantStore>(); |
|||
var tenantNormalizer = httpContext.RequestServices.GetRequiredService<ITenantNormalizer>(); |
|||
var tenantInfo = await tenantStore.FindAsync(tenantNormalizer.NormalizeName(tenantName)!); |
|||
if (tenantInfo != null) |
|||
{ |
|||
if (httpContext.Request.Path.StartsWithSegments(new PathString(tenantName.EnsureStartsWith('/')), out var matchedPath, out var remainingPath)) |
|||
{ |
|||
var originalPath = httpContext.Request.Path; |
|||
var originalPathBase = httpContext.Request.PathBase; |
|||
httpContext.Request.Path = remainingPath; |
|||
httpContext.Request.PathBase = originalPathBase.Add(matchedPath); |
|||
try |
|||
{ |
|||
await next(httpContext); |
|||
} |
|||
finally |
|||
{ |
|||
httpContext.Request.Path = originalPath; |
|||
httpContext.Request.PathBase = originalPathBase; |
|||
} |
|||
return; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
await next(httpContext); |
|||
}); |
|||
app.UseRouting(); |
|||
app.MapAbpStaticAssets(); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
After setting the PathBase, we need to add a custom tenant resolver to extract tenant information from the `PathBase`: |
|||
|
|||
```cs |
|||
public class MyRouteTenantResolveContributor : RouteTenantResolveContributor |
|||
{ |
|||
public const string ContributorName = "MyRoute"; |
|||
|
|||
public override string Name => ContributorName; |
|||
|
|||
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext) |
|||
{ |
|||
var tenantId = httpContext.GetRouteValue(context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey) ?? httpContext.Request.PathBase.ToString(); |
|||
var tenantIdStr = tenantId?.ToString()?.RemovePreFix("/"); |
|||
return Task.FromResult(!tenantIdStr.IsNullOrWhiteSpace() ? Convert.ToString(tenantIdStr) : null); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Register the MyRouteTenantResolveContributor with the ABP Framework: |
|||
|
|||
```cs |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
Configure<AbpTenantResolveOptions>(options => |
|||
{ |
|||
options.TenantResolvers.Add(new MyRouteTenantResolveContributor()); |
|||
}); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
### Modifying abp.appPath |
|||
|
|||
```csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//... |
|||
context.Services.AddOptions<AbpThemingOptions>().Configure<IServiceProvider>((options, rootServiceProvider) => |
|||
{ |
|||
var currentTenant = rootServiceProvider.GetRequiredService<ICurrentTenant>(); |
|||
if (!currentTenant.Name.IsNullOrWhiteSpace()) |
|||
{ |
|||
options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/'); |
|||
} |
|||
}); |
|||
|
|||
context.Services.RemoveAll(x => x.ServiceType == typeof(IOptions<AbpThemingOptions>)); |
|||
context.Services.Add(ServiceDescriptor.Scoped(typeof(IOptions<>), typeof(OptionsManager<>))); |
|||
//... |
|||
} |
|||
``` |
|||
|
|||
Browser console output: |
|||
|
|||
```cs |
|||
> https://localhost:44303/acme/ |
|||
> abp.appPath |
|||
> '/acme/' |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
By following these steps, you can implement tenant resolution from routes in the ABP Framework and handle navigation links appropriately. This approach provides a clean and maintainable way to manage multi-tenancy in your application. |
|||
|
|||
|
|||
## References |
|||
|
|||
- [ABP Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy) |
|||
- [Routing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) |
|||
- [HTML base tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) |
|||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 112 KiB |
@ -1,3 +1,52 @@ |
|||
# Configuration |
|||
|
|||
ASP.NET Core has an flexible and extensible key-value based configuration system. In fact, the configuration system is a part of Microsoft.Extensions libraries and it is independent from ASP.NET Core. That means it can be used in any type of application. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) to learn the configuration infrastructure. ABP is 100% compatible with the configuration system. |
|||
ASP.NET Core has an flexible and extensible key-value based configuration system. The configuration system is a part of Microsoft.Extensions libraries and it is independent from ASP.NET Core. That means it can be used in any type of application. |
|||
|
|||
See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) to learn the configuration infrastructure. ABP is 100% compatible with the configuration system. |
|||
|
|||
## Getting the Configuration |
|||
|
|||
You may need to get the `IConfiguration` service in various places in your codebase. The following section shows two common ways. |
|||
|
|||
### In Module Classes |
|||
|
|||
You typically need to get configuration while initializing your application. You can get the `IConfiguration` service using the `ServiceConfigurationContext.Configuration` property inside your [module class](../architecture/modularity/basics.md) as the following example: |
|||
|
|||
````csharp |
|||
public class MyAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var connectionString = context.Configuration["ConnectionStrings:Default"]; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`context.Configuration` is a shortcut property for the `context.Services.GetConfiguration()` method. In general, prefer using `context.Configuration` for simplicity and readability when working within module classes. Use `context.Services.GetConfiguration()` in other contexts where you have an `IServiceCollection` object but do not have access to the `context.Configuration` property. (`IServiceCollection.GetConfiguration` is an extension method that can be used whenever you have an `IServiceCollection` object). |
|||
|
|||
### In Your Services |
|||
|
|||
You can directly [inject](dependency-injection.md) the `IConfiguration` service into your services: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
public MyService(IConfiguration configuration) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
public string? GetConnectionString() |
|||
{ |
|||
return _configuration["ConnectionStrings:Default"]; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## See Also |
|||
|
|||
* [Microsoft's Configuration Documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) |
|||
* [The Options Pattern](options.md) |
|||
|
|||
|
|||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 459 KiB |