Browse Source

Merge branch 'dev' into berkan/trainings

pull/11792/head
berkansasmaz 5 years ago
parent
commit
d85c75a3c4
No known key found for this signature in database GPG Key ID: 3AD81E2CA002230D
  1. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png
  2. 529
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md
  3. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif
  4. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif
  5. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif
  6. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png
  7. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif
  8. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png
  9. 529
      docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md
  10. 100
      docs/en/Multi-Lingual-Entities.md
  11. 16
      docs/en/UI/Blazor/Error-Handling.md
  12. 12
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
  13. 12
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
  14. 22
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
  15. 4
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
  16. 0
      modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
  17. 19
      modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
  18. 20
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
  19. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
  20. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

529
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

529
docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

100
docs/en/Multi-Lingual-Entities.md

@ -1,100 +1,6 @@
# Multi Lingual Entities # Multi Lingual Entities
ABP Framework defines two basic interfaces for Multi-Lingual entity definitions to provide a standard model for translating entities. This feature is still under development.
Follow the below link to get information about the development status
## IHasMultiLingual https://github.com/abpframework/abp/issues/1169
`IHasMultiLingual<TTranslation>` interface is used to mark multi lingual entities. The entities marked with `IHasMultiLingual<TTranslation>` interface must define language-neutral information. The entities marked with `IHasMultiLingual<TTranslation>` contains a collection of Translations which contains language-dependent information.
Example:
```csharp
public class Product : Entity, IMultiLingualEntity<ProductTranslation>
{
public decimal Price { get; set; }
public ICollection<ProductTranslation> Translations { get; set; }
}
```
## IMultiLingualTranslation
`IMultiLingualTranslation` interface is used to mark translation of a Multi-Lingual entity. The entities marked with `IMultiLingualTranslation` interface must define language dependent information. The entities marked with `IMultiLingualTranslation` contains Language field which contains a language code for the translation.
Example:
```csharp
public class ProductTranslation : Entity, IMultiLingualTranslation
{
public string Name { get; set; }
public string Language { get; set; }
}
```
## Map to DTO object
ABP provdies the [Object To Object Mapping](Object-To-Object-Mapping.md) system, you can implement the `IObjectMapper<TSource, TDestination>` interface to map multi lingual entities to DTOs.
Example:
```csharp
public class MultiLingualProductObjectMapper : IObjectMapper<Product, ProductDto>, ITransientDependency
{
private readonly IMultiLingualObjectManager _multiLingualObjectManager;
public MultiLingualProductObjectMapper(IMultiLingualObjectManager multiLingualObjectManager)
{
_multiLingualObjectManager = multiLingualObjectManager;
}
public ProductDto Map(Product source)
{
var translation = _multiLingualObjectManager.GetTranslation<Product, ProductDto>(source);
return new ProductDto
{
Price = source.Price,
Id = source.Id,
Name = translation?.Name
};
}
public ProductDto Map(Product source, ProductDto destination)
{
return default;
}
}
```
### AutoMapper integration
ABP provides the `CreateMultiLingualMap` extension method for mapping multilingual entities to DTOs.
Example:
```csharp
public class ProductProfile : Profile
{
public ProductProfile()
{
var mapResult = this.CreateMultiLingualMap<Product, ProductTranslation, ProductDto>();
}
}
```
`CreateMultiLingualMap` extension method returns an object of type `CreateMultiLingualMapResult` which contains `EntityMap` and `TranslationMap` fields. These fields can be used to customize multi lingual mapping.
Example:
```csharp
this.CreateMultiLingualMap<Order, OrderTranslation, OrderListDto>(context)
.EntityMap.ForMember(dest => dest.ProductCount, opt => opt.MapFrom(src => src.Products.Count));
```
## IMultiLingualObjectManager
`IMultiLingualObjectManager` interface defines `GetTranslation` and `GetTranslationAsync` method to get the translation object of the entity.
The default implementation of the `IMultiLingualObjectManager` interface finds the translation with selected UI language first. If there is no translation with selected UI language, then extension method searches for the default language setting (see [Setting](Settings.md)) and uses the translation in default language. If extension method couldn't find any translation in current UI language or default language, it uses one of the existing translations.

16
docs/en/UI/Blazor/Error-Handling.md

@ -16,6 +16,8 @@ There are different type of `Exception` classes handled differently by the ABP F
`UserFriendlyException` is a special type of exception. You can directly show a error message dialog to the user by throwing such an exception. `UserFriendlyException` is a special type of exception. You can directly show a error message dialog to the user by throwing such an exception.
> For Blazor Server, exceptions must be handled manually. Otherwise it crashes the whole application. Auto Exception Handling is not possible with Blazor Server right now, please follow [this issue](https://github.com/abpframework/abp/issues/8195) to see progress. In meantime, you can use try-catch blocks and call the `HandleErrorAsync` method of ABP to handle errors manually, which shows an error dialog for you.
**Example** **Example**
````csharp ````csharp
@ -26,10 +28,24 @@ There are different type of `Exception` classes handled differently by the ABP F
@code @code
{ {
//for Blazor WASM
private void TestException() private void TestException()
{ {
throw new UserFriendlyException("A user friendly error message!"); throw new UserFriendlyException("A user friendly error message!");
} }
//for Blazor Server
private async Task TestException()
{
try
{
throw new UserFriendlyException("A user friendly error message!");
}
catch(UserFriendlyException ex)
{
await HandleErrorAsync(ex);
}
}
} }
```` ````

12
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs

@ -55,7 +55,17 @@ public class AbpAuditHubFilter : IHubFilter
var unitOfWorkManager = invocationContext.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); var unitOfWorkManager = invocationContext.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
if (unitOfWorkManager.Current != null) if (unitOfWorkManager.Current != null)
{ {
await unitOfWorkManager.Current.SaveChangesAsync(); try
{
await unitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!auditingManager.Current.Log.Exceptions.Contains(ex))
{
auditingManager.Current.Log.Exceptions.Add(ex);
}
}
} }
await saveHandle.SaveAsync(); await saveHandle.SaveAsync();

12
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs

@ -73,7 +73,17 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
{ {
if (UnitOfWorkManager.Current != null) if (UnitOfWorkManager.Current != null)
{ {
await UnitOfWorkManager.Current.SaveChangesAsync(); try
{
await UnitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!_auditingManager.Current.Log.Exceptions.Contains(ex))
{
_auditingManager.Current.Log.Exceptions.Add(ex);
}
}
} }
await saveHandle.SaveAsync(); await saveHandle.SaveAsync();

22
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs

@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.Aspects; using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy; using Volo.Abp.DynamicProxy;
using Volo.Abp.Uow;
using Volo.Abp.Users; using Volo.Abp.Users;
namespace Volo.Abp.Auditing; namespace Volo.Abp.Auditing;
@ -41,7 +42,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
else else
{ {
var currentUser = serviceScope.ServiceProvider.GetRequiredService<ICurrentUser>(); var currentUser = serviceScope.ServiceProvider.GetRequiredService<ICurrentUser>();
await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper); var unitOfWorkManager = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper, unitOfWorkManager);
} }
} }
} }
@ -105,7 +107,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
AbpAuditingOptions options, AbpAuditingOptions options,
ICurrentUser currentUser, ICurrentUser currentUser,
IAuditingManager auditingManager, IAuditingManager auditingManager,
IAuditingHelper auditingHelper) IAuditingHelper auditingHelper,
IUnitOfWorkManager unitOfWorkManager)
{ {
var hasError = false; var hasError = false;
using (var saveHandle = auditingManager.BeginScope()) using (var saveHandle = auditingManager.BeginScope())
@ -129,6 +132,21 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
{ {
if (ShouldWriteAuditLog(invocation, options, currentUser, hasError)) if (ShouldWriteAuditLog(invocation, options, currentUser, hasError))
{ {
if (unitOfWorkManager.Current != null)
{
try
{
await unitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!auditingManager.Current.Log.Exceptions.Contains(ex))
{
auditingManager.Current.Log.Exceptions.Add(ex);
}
}
}
await saveHandle.SaveAsync(); await saveHandle.SaveAsync();
} }
} }

4
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js

@ -13,6 +13,10 @@
volo.abp.account.profile.update(input).then(function (result) { volo.abp.account.profile.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved')); abp.notify.success(l('PersonalSettingsSaved'));
volo.abp.account.profile.get().then(function(profile){
$("#ConcurrencyStamp").val(profile.concurrencyStamp);
});
}); });
}); });
}); });

0
modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json → modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json

19
modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
@ -134,22 +135,14 @@ namespace Volo.Docs.Admin.Projects
throw new Exception("Cannot find the project with the Id " + projectId); throw new Exception("Cannot find the project with the Id " + projectId);
} }
var docs = await _documentRepository.GetListByProjectId(project.Id); var docs = (await _documentRepository.GetListByProjectId(project.Id))
.Where(doc => doc.FileName != project.NavigationDocumentName && doc.FileName != project.ParametersDocumentName)
.ToList();
await _elasticSearchService.DeleteAllByProjectIdAsync(project.Id); await _elasticSearchService.DeleteAllByProjectIdAsync(project.Id);
foreach (var doc in docs) if(docs.Any())
{ {
if (doc.FileName == project.NavigationDocumentName) await _elasticSearchService.AddOrUpdateManyAsync(docs);
{
continue;
}
if (doc.FileName == project.ParametersDocumentName)
{
continue;
}
await _elasticSearchService.AddOrUpdateAsync(doc);
} }
} }

20
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Elasticsearch.Net; using Elasticsearch.Net;
@ -69,7 +70,24 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
Version = NormalizeField(document.Version) Version = NormalizeField(document.Version)
}; };
await client.IndexAsync(esDocument, x => x.Index(_options.IndexName), cancellationToken); HandleError(await client.IndexAsync(esDocument, x => x.Index(_options.IndexName), cancellationToken));
}
public virtual async Task AddOrUpdateManyAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default)
{
var client = _clientProvider.GetClient();
var esDocuments = documents.Select(x => new EsDocument {
Id = NormalizeField(x.Id),
ProjectId = NormalizeField(x.ProjectId),
Name = x.Name,
FileName = x.FileName,
Content = x.Content,
LanguageCode = NormalizeField(x.LanguageCode),
Version = NormalizeField(x.Version)
});
HandleError(await client.IndexManyAsync(esDocuments, _options.IndexName, cancellationToken));
} }
public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)

2
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs

@ -11,6 +11,8 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default); Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default);
Task AddOrUpdateManyAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteAllAsync(CancellationToken cancellationToken = default); Task DeleteAllAsync(CancellationToken cancellationToken = default);

2
modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs

@ -383,7 +383,7 @@ namespace Volo.Docs.GitHub.Documents
catch (Exception ex) catch (Exception ex)
{ {
//TODO: Only handle when document is really not available //TODO: Only handle when document is really not available
Logger.LogWarning(ex.Message, ex); Logger.LogWarning($"{ex.Message}: {rawUrl}", ex);
throw new DocumentNotFoundException(rawUrl); throw new DocumentNotFoundException(rawUrl);
} }
} }

Loading…
Cancel
Save