diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png
new file mode 100644
index 0000000000..b61bdb6ddb
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md
new file mode 100644
index 0000000000..75bc1a9124
--- /dev/null
+++ b/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
+
+
+ ```
+
+- Add HttpApi.Client project reference
+
+ ```xml
+
+ ```
+ 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(configuration.GetSection("Oidc:Options"));
+
+ context.Services.AddTransient(sp =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ options.Browser = sp.GetRequiredService();
+ return new OidcClient(options);
+ });
+
+ context.Services.AddTransient(sp =>
+ new HttpClient(sp.GetRequiredService())
+ {
+ // Temporarily. We'll use ABP's Proxy for sendind requests.
+ BaseAddress = new Uri(configuration.GetValue("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()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ ConfigureConfiguration(builder);
+
+ builder.Services.AddApplication(options =>
+ {
+ options.Services.ReplaceConfiguration(builder.Configuration);
+ });
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService()
+ .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 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(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)_
+
+ 
+
+- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ > 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.
+ >
+ > 
+
+
+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
+
+
+
+
+ #512BD4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- 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|
+| --- | --- |
+|  |  |
+
+| UWP |
+| --- |
+|  |
+
+| MacCatalyst |
+| --- |
+|  |
+
+
+## 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.
+
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif
new file mode 100644
index 0000000000..75d0333dec
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif
new file mode 100644
index 0000000000..a61d09ede4
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif
new file mode 100644
index 0000000000..6085e216d0
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png
new file mode 100644
index 0000000000..9b0539c1ff
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif
new file mode 100644
index 0000000000..3f895e8c52
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png
new file mode 100644
index 0000000000..521675d53c
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md
new file mode 100644
index 0000000000..75bc1a9124
--- /dev/null
+++ b/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
+
+
+ ```
+
+- Add HttpApi.Client project reference
+
+ ```xml
+
+ ```
+ 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(configuration.GetSection("Oidc:Options"));
+
+ context.Services.AddTransient(sp =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ options.Browser = sp.GetRequiredService();
+ return new OidcClient(options);
+ });
+
+ context.Services.AddTransient(sp =>
+ new HttpClient(sp.GetRequiredService())
+ {
+ // Temporarily. We'll use ABP's Proxy for sendind requests.
+ BaseAddress = new Uri(configuration.GetValue("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()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ ConfigureConfiguration(builder);
+
+ builder.Services.AddApplication(options =>
+ {
+ options.Services.ReplaceConfiguration(builder.Configuration);
+ });
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService()
+ .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 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(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)_
+
+ 
+
+- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ > 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.
+ >
+ > 
+
+
+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
+
+
+
+
+ #512BD4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- 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|
+| --- | --- |
+|  |  |
+
+| UWP |
+| --- |
+|  |
+
+| MacCatalyst |
+| --- |
+|  |
+
+
+## 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.
+
diff --git a/docs/en/Multi-Lingual-Entities.md b/docs/en/Multi-Lingual-Entities.md
index 85746a3fea..8a707b24d0 100644
--- a/docs/en/Multi-Lingual-Entities.md
+++ b/docs/en/Multi-Lingual-Entities.md
@@ -1,100 +1,6 @@
# 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
-
-`IHasMultiLingual` interface is used to mark multi lingual entities. The entities marked with `IHasMultiLingual` interface must define language-neutral information. The entities marked with `IHasMultiLingual` contains a collection of Translations which contains language-dependent information.
-
-Example:
-
-```csharp
-public class Product : Entity, IMultiLingualEntity
-{
- public decimal Price { get; set; }
-
- public ICollection 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` interface to map multi lingual entities to DTOs.
-
-Example:
-
-```csharp
-public class MultiLingualProductObjectMapper : IObjectMapper, ITransientDependency
-{
- private readonly IMultiLingualObjectManager _multiLingualObjectManager;
-
- public MultiLingualProductObjectMapper(IMultiLingualObjectManager multiLingualObjectManager)
- {
- _multiLingualObjectManager = multiLingualObjectManager;
- }
-
- public ProductDto Map(Product source)
- {
- var translation = _multiLingualObjectManager.GetTranslation(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();
- }
-}
-```
-
-`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(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.
\ No newline at end of file
+https://github.com/abpframework/abp/issues/1169
diff --git a/docs/en/UI/Blazor/Error-Handling.md b/docs/en/UI/Blazor/Error-Handling.md
index f43456e3a7..35aff041af 100644
--- a/docs/en/UI/Blazor/Error-Handling.md
+++ b/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.
+> 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**
````csharp
@@ -26,10 +28,24 @@ There are different type of `Exception` classes handled differently by the ABP F
@code
{
+ //for Blazor WASM
private void TestException()
{
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);
+ }
+ }
}
````
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
index d622ba535a..fd5567396b 100644
--- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
+++ b/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();
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();
diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
index 5d0462bc5f..4ff49012cf 100644
--- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
+++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
@@ -73,7 +73,17 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
{
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();
diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
index 4f4d3727bb..5521c28d1e 100644
--- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
+++ b/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.DependencyInjection;
using Volo.Abp.DynamicProxy;
+using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Volo.Abp.Auditing;
@@ -41,7 +42,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
else
{
var currentUser = serviceScope.ServiceProvider.GetRequiredService();
- await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper);
+ var unitOfWorkManager = serviceScope.ServiceProvider.GetRequiredService();
+ await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper, unitOfWorkManager);
}
}
}
@@ -105,7 +107,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
AbpAuditingOptions options,
ICurrentUser currentUser,
IAuditingManager auditingManager,
- IAuditingHelper auditingHelper)
+ IAuditingHelper auditingHelper,
+ IUnitOfWorkManager unitOfWorkManager)
{
var hasError = false;
using (var saveHandle = auditingManager.BeginScope())
@@ -129,6 +132,21 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
{
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();
}
}
diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
index 3422b36d63..e4f5f82620 100644
--- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
+++ b/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) {
abp.notify.success(l('PersonalSettingsSaved'));
+
+ volo.abp.account.profile.get().then(function(profile){
+ $("#ConcurrencyStamp").val(profile.concurrencyStamp);
+ });
});
});
});
diff --git a/modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json b/modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
similarity index 100%
rename from modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json
rename to modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
index 078288572b..14833cd3e4 100644
--- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
+++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
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);
}
- 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);
- foreach (var doc in docs)
+ if(docs.Any())
{
- if (doc.FileName == project.NavigationDocumentName)
- {
- continue;
- }
-
- if (doc.FileName == project.ParametersDocumentName)
- {
- continue;
- }
-
- await _elasticSearchService.AddOrUpdateAsync(doc);
+ await _elasticSearchService.AddOrUpdateManyAsync(docs);
}
}
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
index 7a47cd7b45..6a6c8cc7e4 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
+++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net;
@@ -69,7 +70,24 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
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 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)
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
index c84df577b7..2c7cf1f3e6 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
+++ b/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 AddOrUpdateManyAsync(IEnumerable documents, CancellationToken cancellationToken = default);
+
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteAllAsync(CancellationToken cancellationToken = default);
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
index d03d8078e4..060c6dcf08 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
+++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
@@ -383,7 +383,7 @@ namespace Volo.Docs.GitHub.Documents
catch (Exception ex)
{
//TODO: Only handle when document is really not available
- Logger.LogWarning(ex.Message, ex);
+ Logger.LogWarning($"{ex.Message}: {rawUrl}", ex);
throw new DocumentNotFoundException(rawUrl);
}
}