Browse Source

Merge branch 'dev' into berkan/trainings

pull/11792/head
berkansasmaz 5 years ago
parent
commit
b239c4f9f4
No known key found for this signature in database GPG Key ID: 3AD81E2CA002230D
  1. 32
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  2. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  3. 5
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  4. 601
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md
  5. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif
  6. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png
  7. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png
  8. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif
  9. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif
  10. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png
  11. 4
      docs/en/Previews.md
  12. 2
      docs/en/Tutorials/Part-1.md
  13. 2
      docs/en/Tutorials/Part-10.md
  14. 2
      docs/en/Tutorials/Part-3.md
  15. 2
      docs/en/Tutorials/Part-7.md
  16. 257
      docs/zh-Hans/Emailing.md
  17. 49
      docs/zh-Hans/MailKit.md
  18. 4
      docs/zh-Hans/Tutorials/Part-1.md
  19. 2
      docs/zh-Hans/Tutorials/Part-10.md
  20. 2
      docs/zh-Hans/Tutorials/Part-2.md
  21. 4
      docs/zh-Hans/Tutorials/Part-3.md
  22. 2
      docs/zh-Hans/Tutorials/Part-5.md
  23. 2
      docs/zh-Hans/Tutorials/Part-6.md
  24. 2
      docs/zh-Hans/Tutorials/Part-7.md
  25. 2
      docs/zh-Hans/Tutorials/Part-8.md
  26. 6
      docs/zh-Hans/Tutorials/Part-9.md
  27. 34
      docs/zh-Hans/docs-nav.json
  28. 11
      framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs
  29. 23
      framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs
  30. 4
      framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor
  31. 12
      framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs
  32. 2
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs
  33. 12
      framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs
  34. 28
      framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs
  35. 2
      modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml
  36. 2
      modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml
  37. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs
  38. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor
  39. 4
      npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts

32
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -306,10 +306,10 @@
"PaymentTimeCannotBeFutureTime": "Payment time cannot be a future time!",
"SaveAndDownload": "Save and Download",
"BillingInfo": "Billing Info",
"DeleteInvoice" : "Delete Invoice",
"PaymentStateSetTo" : "Payment state set to {0}",
"DeleteInvoice": "Delete Invoice",
"PaymentStateSetTo": "Payment state set to {0}",
"ChangeState": "Change State",
"Permission:TrialLicense" : "Trial License",
"Permission:TrialLicense": "Trial License",
"Menu:TrialLicenses": "Trial Licenses",
"TrialLicenses": "Trial Licenses",
"UserNameFilter": "Username",
@ -359,18 +359,38 @@
"PurchaseDate": "Purchase date",
"IsAbpBookDownloaded": "ABP book downloaded",
"IsMasteringAbpBookDownloadEnabled": "ABP Book download enabled",
"Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link",
"Permission:Accounting:CustomPaymentLinkGenerator": "Custom Payment Link",
"CustomPaymentLink": "Custom Payment Link",
"Menu:CustomPaymentLink": "Custom Payment Link",
"Amount": "Amount",
"GenerateCustomPaymentLink": "Generate Custom Payment Link",
"GeneratedPaymentLink": "Generated Payment Link",
"CopyText": "Copy Text",
"Permission:CommunityEvents": "Events",
"Menu:Events": "Events",
"Events": "Events",
"EventType": "Event Type",
"Number": "Number",
"RegistrationURL": "Registration URL",
"URL": "URL",
"EventDeletionConfirmationMessage": "Are you sure you want to delete this event?",
"Enum:EventType:0": "Community Talks",
"CreateAnEvent": "Create an event",
"Permission:CommunitySpeakers": "Speakers",
"CreateASpeaker": "Create a speaker",
"Speakers": "Speakers",
"Image": "Image",
"GithubURL": "Github URL",
"SpeakerDeletionConfirmationMessage": "Are you sure you want to delete this speaker?",
"Menu:Speakers": "Speakers",
"ChooseSpeakerImage": "Choose a speaker image...",
"SpeakerImage": "Speaker image",
"ShowPurchaseItemsOfOrganizations": "Purchase Items",
"Enum:OrganizationPurchaseState:0": "Not delivered",
"Enum:OrganizationPurchaseState:1": "Delivered",
"PurchaseItems": "Purchase Items",
"SuccessfullyUpdated": "Successfully updated",
"SuccessfullyAdded": "Successfully added"
"SuccessfullyAdded": "Successfully added",
"PurchaseState": "Purchase State"
}
}
}

7
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -98,6 +98,11 @@
"EULA": "EULA",
"ABPCommercialIntroductionMessage": "Pre-built application modules, advanced startup templates, rapid application development tooling, professional UI themes and premium support.",
"MasteringAbpFrameworkEBook": "Mastering ABP Framework",
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques."
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques.",
"Speakers": "Speakers",
"PreviousEvents": "Previous Events",
"WatchTheEvent": "Watch the Event",
"RegisterNow": "Register Now",
"ThereIsNoEvent": "There is no event."
}
}

5
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -162,6 +162,9 @@
"Previous": "Previous",
"Next": "Next",
"Share": "Share",
"SortBy": "Sort by"
"SortBy": "Sort by",
"NoPublishedEventsYet": "No published events yet.",
"SubscribeYoutubeChannel": "Subscribe to the Youtube Channel",
"Enum:EventType:0": "Talks"
}
}

601
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md

@ -0,0 +1,601 @@
# Integrating MAUI Client via using OpenID Connect
This is a demonstration for connecting ABP backend from MAUI app via using openid connect.
In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application.
> This is by intent. The code flow does not allow the user to log in using a native view in the app. The reason being that this flow ensures that the username and password are never seen by the client (except the browser, which is part of the OS system - aka we trust it). You could enable using a native login view with the Resource Owner Password Credentials (ROPC) flow. But this is also an attack vector. Suppose someone makes a fraud duplicate of your application and tricking users into entering their credentials. The fraudulent app could store those credentials in-between. You just got to enjoy those tin-foil-hat moments when doing security. In other words, using the code flow does not give an attacker that opportunity and therefore is the recommended option for mobile clients.
>
> - [@Mark Allibone](https://mallibone.com/post/xamarin-oidc)
By the way, my motivation for building this sample is presenting just another way for authentication. **Resource Owner Password Credentials** authentication is already provided and it's more common way to do. This is yet another way to authenticate users.
## Source Code
You can also find source code on GitHub in ABP-Samples.
- [abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Creating projects
- Create an ABP project without UI
```bash
abp new Acme.BookStore -t app --no-ui -d mongodb --no-random-ports
```
- Create a maui application
```bash
mkdir maui
cd maui
dotnet new maui -n Acme.BookStore.MauiClient
```
There is a long way for configuring scopes and callback urls for both server and clients. We'll use [WebAuthenticator](https://docs.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android) to perform this operation.
## Configuring IdentityServer
- Go to DbMigrator folder and MAUI client in **appsettings.json**. Add following client code in **IdentityServer:Clients** path:
```json
"BookStore_Maui": {
"ClientId": "BookStore_Maui",
"ClientSecret": "1q2w3e*",
"RootUrl": "bookstore://"
}
```
- Go to **IdentityServerDataSeedContributor** in Domain project under IdentityServer folder. Append following code section into **CreateClientsAsync()** method.
```csharp
// Maui Client
var mauiClientId = configurationSection["BookStore_Maui:ClientId"];
if (!mauiClientId.IsNullOrWhiteSpace())
{
var mauiRootUrl = configurationSection["BookStore_Maui:RootUrl"];
await CreateClientAsync(
name: mauiClientId,
scopes: commonScopes,
grantTypes: new[] { "authorization_code" },
secret: configurationSection["BookStore_Maui:ClientSecret"]?.Sha256(),
requireClientSecret: false,
redirectUri: $"{mauiRootUrl}"
);
}
```
- Run DbMigrator
- Then run HttpApi.Host
### Configuring NGROK
Client will check configuration from `/.well-known/openid-configuration` path and it must be a secured connection between client & server. I prefer to use ngrok to open my backend app to entire web.
- Go to [getting started](https://dashboard.ngrok.com/get-started/setup) page of ngrok _(login or register first)_ and download the ngrok tool.
- Don't forget to login from tool:
```bash
ngrok authtoken XXX
```
_A sample command is being displayed at dashboard where you download ngrok from_
- Open your HttpApi.Host with ngrok
```bash
.\ngrok.exe http https://localhost:44350
```
- You'll see a generated xxx.ngrok.io url. Navigate to `/.well-known/openid-configuration` to check if it's working right.
You should see something like that:
![](art/openid-configuration.png)
Issuer must be your URL, not localhost! If you see still localhost, try to disable host header rewrite.
- Also, ValidIssuers must be defined to validate tokens.
- Add **ValidIssuers** section to your `appsettings.json` of HttpApi.Host
```js
"AuthServer": {
"Authority": "https://localhost:44350",
"RequireHttpsMetadata": "false",
"SwaggerClientId": "BookStore_Swagger",
"SwaggerClientSecret": "1q2w3e*",
"ValidIssuers": [
"https://46fd-45-156-29-175.ngrok.io"
]
},
```
- Then define it in **ConfigureAuthentication** method in Module class
```csharp
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
// ...
options.TokenValidationParameters.ValidIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get<string[]>();
});
}
```
We're done with backend. Let's continue with MAUI app.
## Developing MAUI App
Before we go, there is something to do like configuring dependency injection to get rid of unnecessary huge class coupling.
### Configuring Dependency Injection
- Go to **MauiApplication** class and add `MainPage` in services.
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
```
- And inject MainPage from constructor in **App.xaml.cs**
```csharp
public App(MainPage mainPage)
{
InitializeComponent();
MainPage = mainPage;
}
```
Now MainPage is ready for injecting dependencies to it.
### Configuring OIDC
- Add `IdentityModel.OidcClient` package to project
```xml
<ItemGroup>
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
</ItemGroup>
```
- Create **WebAuthenticatorBrowser**
```csharp
internal class WebAuthenticatorBrowser : IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
WebAuthenticatorResult authResult =
await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(options.EndUrl));
var authorizeResponse = ToRawIdentityUrl(options.EndUrl, authResult);
return new BrowserResult
{
Response = authorizeResponse
};
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return new BrowserResult()
{
ResultType = BrowserResultType.UnknownError,
Error = ex.ToString()
};
}
}
public string ToRawIdentityUrl(string redirectUrl, WebAuthenticatorResult result)
{
IEnumerable<string> parameters = result.Properties.Select(pair => $"{pair.Key}={pair.Value}");
var values = string.Join("&", parameters);
return $"{redirectUrl}#{values}";
}
}
```
- Configure **OidcClient** in **MauiProgram**
```csharp
builder.Services.AddTransient<WebAuthenticatorBrowser>();
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore",
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Go to **MainPage.xaml**, remove everyting and add a button for login
```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage">
<ScrollView>
<Grid RowSpacing="25" RowDefinitions="Auto,Auto,Auto,Auto,*"
Padding="{OnPlatform iOS='30,60,30,30', Default='30'}">
<Button Text="Click to Log In" Clicked="OnLoginClicked" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
</Grid>
</ScrollView>
</ContentPage>
```
- Inject **OidcClient** in **MainPage.xaml.cs** and make login operation.
```csharp
using IdentityModel.OidcClient;
namespace Acme.BookStore.MauiClient;
public partial class MainPage : ContentPage
{
protected OidcClient OidcClient { get; }
public MainPage(OidcClient oidcClient)
{
InitializeComponent();
OidcClient = oidcClient;
}
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
}
```
It still won't work because there is something more to do for each platform. Check out the next step and configure the platforms that you're using.
## Platform Specific Configurations
Each platform (UWP, OSX, iOS and Android) requires some configuration to use authentication from browser. In that step, we'll open a browser and user will login on the browser. After that, as you see in IdentityServer configurations, IdentityServer will redirect 'bookstore://' url that only contains scheme and that scheme is not http. Our application will handle that scheme and will be launched with parameters.
### Android
- Start with creating a new Activity named **BookStoreWebAuthenticatorCallbackActivity**
```csharp
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Acme.BookStore.MauiClient.Platforms.Android;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = CALLBACK_SCHEME)]
public class BookStoreWebAuthenticatorCallbackActivity : Microsoft.Maui.Essentials.WebAuthenticatorCallbackActivity
{
const string CALLBACK_SCHEME = "bookstore";
}
```
- Add `CustomTabsService` to **AndroidManifest.xml** as below. _(queries tags only.)_
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
```
> For some reason, an error occurs with my emulator while targeting SDK 31, so I've changed the target SDK to 30.
- Run the Application and perform a login operation.
AccessToken will be retrieved.
![abp-maui-android-openid-connect](art/android-login-demo.gif)
---
### iOS/MacCatalyst
- Add following key to **Info.plist**
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>mauiessentials</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bookstore</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
```
- Open **AppDelegate** class and override `OpenUrl` and `ContinueUserActivity` methods
```csharp
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (Microsoft.Maui.Essentials.Platform.OpenUrl(app, url, options))
return true;
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
if (Microsoft.Maui.Platform.ContinueUserActivity(application, userActivity, completionHandler))
return true;
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
```
- Make all steps for MacCatalyst, too.
> **Tip:** If your IDE struggles while displaying references and namespace suggestions, make sure you're displaying that file with iOS Target Framework.
>
> You'll find it at the top of the editor.
> ![abp-maui-example-compiler-select](art/compiler-select.png)
![abp-maui-ios-openid-connect](art/ios-login-demo.gif)
![abp-maui-MacCatalyst-openid-connect](art/macos-login-demo.gif)
---
### UWP (Windows)
- Add following protocol extension in `Package.appxmanifest` file.
```xml
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="bookstore">
<uap:DisplayName>BookStore</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
</Applications>
```
> Currently UWP has a bug in MAUI Essentials, I believe the MAUI team will solve it as soon as possible. You can track the status of issue:
> https://github.com/dotnet/maui/issues/2702
- That's it on Windows side. Run the application.
## Refreshing the access token
IdentityServer doesn't return a refresh token by default. So we have to add `offline_access` to our scope while sending login request.
- Add `offline_access` to scope in **MauiApplication.cs** that we configured before.
```csharp
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore offline_access", // <-- Final state must be like this.
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Then check if it's working or not in **MainPage.xaml.cs**. Update OnLoginClicked method as below
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
var refreshResult = await OidcClient.RefreshTokenAsync(loginResult.RefreshToken);
await DisplayAlert("Refresh Result", "New Access Token is: \n\n" + refreshResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
## Storing the access token
In this step we have to store access token & refresh token for future requests.
> [Secure Storage](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android) is highly recommended to store this kind of sensitive data. But it's not topic of this article. You can configure and use SecureStorage on your own. I'll go with `App Properties`.
- Add following class to store key names instead of using magic strings in code.
```csharp
namespace Acme.BookStore.MauiClient;
public static class OidcConsts
{
internal const string AccessTokenKeyName = "__access_token";
internal const string RefreshTokenKeyName = "__refresh_token";
}
```
- Then go back to **MainPage.xaml.cs** and save our tokens after a successfull login.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
// Navigate to an inner page here.
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Add following **AccessTokenHttpMessageHandler** to append AccessToken to our requests & refresh token when required.
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler
{
protected OidcClient OidcClient { get; }
public AccessTokenHttpMessageHandler(OidcClient oidcClient) : base(new HttpClientHandler())
{
OidcClient = oidcClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (App.Current.Properties.TryGetValue(OidcConsts.AccessTokenKeyName, out object currentTokenValue) && currentTokenValue != null)
{
request.SetBearerToken(currentTokenValue?.ToString());
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (App.Current.Properties.TryGetValue(OidcConsts.RefreshTokenKeyName, out object refreshTokenValue) && refreshTokenValue != null)
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshTokenValue?.ToString());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = refreshResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = refreshResult.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(refreshResult.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
else
{
var result = await OidcClient.LoginAsync(new LoginRequest());
request.SetBearerToken(result.AccessToken);
App.Current.Properties[OidcConsts.AccessTokenKeyName] = result.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = result.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(result.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
```
- Then register to dependency injection.
```csharp
builder.Services.AddSingleton<AccessTokenHttpMessageHandler>();
builder.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
BaseAddress = new Uri("https://46fd-45-156-29-175.ngrok.io")
});
```
- Now make we can send request to backend with authentication. Go to **MainPage.xaml.cs** and send a request right after authentication.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
var json = await httpClient.GetStringAsync("/api/identity/users");
await DisplayAlert("/api/identity/users", json, "close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Following result will be returned from API.
<img src="art/identity-users-request-result.png" height="480">
---
## Recap
The purpose of this arcitle is connecting to ABP backend with access token and it's working properly.
I'm planning to integrate HttpApi.Client library of backend project instead of making requests manually as a second part of this article. I'll get inspired by [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) repo to achive that.

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

4
docs/en/Previews.md

@ -1,6 +1,6 @@
# Preview Releases
The preview versions are released **~2 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions.
The preview versions are released **~4 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions.
Versioning of a preview release is like that:
@ -37,4 +37,4 @@ abp switch-to-stable
## Providing Feedback
You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback.
You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback.

2
docs/en/Tutorials/Part-1.md

@ -202,7 +202,7 @@ This will add a new migration class to the project:
![bookstore-efcore-migration](./images/bookstore-efcore-migration.png)
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{end}}

2
docs/en/Tutorials/Part-10.md

@ -126,7 +126,7 @@ migrationBuilder.AddForeignKey(
* Creates an index on the `AuthorId` field.
* Declares the foreign key to the `AppAuthors` table.
> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{end}}

2
docs/en/Tutorials/Part-3.md

@ -129,7 +129,7 @@ Open the `CreateModal.cshtml` file and paste the code below:
</abp-dynamic-form>
````
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateBookViewModel` model class.
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateUpdateBookDto` model class.
* `abp-model` attribute indicates the model object where it's the `Book` property in this case.
* `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page).

2
docs/en/Tutorials/Part-7.md

@ -93,7 +93,7 @@ You can apply changes to the database using the following command, in the same c
dotnet ef database update
````
> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{else if DB=="Mongo"}}

257
docs/zh-Hans/Emailing.md

@ -1,3 +1,256 @@
# Emailing
# 邮件发送系统
待添加
ABP 框架为发送电子邮件提供各种服务、设置和集成;
* 提供用于发送电子邮件的`IEmailSender`服务.
* 定义 [settings](Settings.md)来配置电子邮件发送.
* 集成到[后台作业系统](Background-Jobs.md)以通过后台作业发送电子邮件.
* 提供[MailKit 集成](MailKit.md)包.
## 安装
> 如果你使用的是[应用程序启动模板](Startup-Templates/Application.md),则该软件包已安装。
>
建议使用 [ABP CLI](CLI.md) 安装此包。在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令:
````bash
abp add-package Volo.Abp.Emailing
````
如果你还没有做到这一点,你首先需要安装 ABP CLI。有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.Emailing)。
## 发送电子邮件
### IEmailSender
[Inject](Dependency-Injection.md) 将 `IEmailSender` 注入任何服务并使用 `SendAsync` 方法发送电子邮件。
**Example**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
namespace MyProject
{
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
public MyService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task DoItAsync()
{
await _emailSender.SendAsync(
"target@domain.com", // target email address
"Email subject", // subject
"This is email body..." // email body
);
}
}
}
````
`SendAsync` 方法具有重载以提供更多参数,例如;
* **from**: 你可以将其设置为设置发件人电子邮件地址。如果未提供,则使用默认发件人地址(请参阅下面的电子邮件设置)
* **isBodyHtml**: 表示邮件正文是否可以包含HTML标签。**默认:true**。
> `IEmailSender` 是建议的发送邮件的方式,因为它使你的代码提供者独立。
#### 邮件消息
除了原始参数之外,你还可以传递一个**标准的 `MailMessage` 对象**([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage) ) 到 `SendAsync` 方法以设置更多选项,例如添加附件。
### ISmtpEmailSender
默认情况下,发送电子邮件由标准的 `SmtpClient` 类([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient))实现。实现类是`SmtpEmailSender`。此类还公开了 `ISmtpEmailSender` 服务(除了 `IEmailSender`)。
大多数时候你想直接使用`IEmailSender`来让你的代码提供者独立。但是,如果要创建具有相同电子邮件设置的 `SmtpClient` 对象,可以注入 `ISmtpEmailSender` 并使用其 `BuildClientAsync` 方法获取 `SmtpClient` 对象并自己发送电子邮件。
## 发送邮件任务队列/后台作业
`IEmailSender`有一个`QueueAsync`方法,可以用来将邮件添加到后台作业队列中,在后台线程中发送。通过这种方式,你不会因为等待发送邮件而占用用户的时间。`QueueAsync`方法得到的参数与`SendAsync`方法相同。
发送邮件任务队列可以容忍错误,因为后台作业系统具有重试机制来克服临时网络/服务器问题。
有关后台作业系统的更多信息,请参阅[后台作业文档](Background-Jobs.md)。
## 电子邮件设置
电子邮件发送使用 [设置系统](Settings.md) 来定义设置并在运行时获取这些设置的值。 `Volo.Abp.Emailing.EmailSettingNames` 定义了设置名称的常量,如下所示:
* **Abp.Mailing.DefaultFromAddress**: 当你在发送电子邮件时未指定发件人时,用作发件人的电子邮件地址(就像上面的示例一样).
* **Abp.Mailing.DefaultFromDisplayName**: 当你在发送电子邮件时未指定发件人时,用作发件人的显示名称(就像在上面的示例中一样).
* **Abp.Mailing.Smtp.Host**: SMTP 服务器的 IP/域(默认值:127.0.0.1)。
* **Abp.Mailing.Smtp.Port**: SMTP 服务器的端口(默认值:25).
* **Abp.Mailing.Smtp.UserName**: 用户名,如果 SMTP 服务器需要身份验证需要。
* **Abp.Mailing.Smtp.Password**: 密码,如果 SMTP 服务器需要身份验证需要。 **此值已加密**(请参阅下面的部分).
* **Abp.Mailing.Smtp.Domain**: 账号域,如果 SMTP 服务器需要身份验证需要.
* **Abp.Mailing.Smtp.EnableSsl**: 指示 SMTP 服务器是否使用 SSL 的值(“true”或“false”。默认值:“false”).
* **Abp.Mailing.Smtp.UseDefaultCredentials**:如果为 true,则使用默认凭据,而不是提供的用户名和密码(“true”或“false”。默认值:“true”)。.
可以从[设置管理](Modules/Setting-Management.md)模块的*设置页面*管理电子邮件设置:
![email-settings](images/email-settings.png)
>如果你已从 ABP 启动模板创建解决方案,则已安装设置管理模块。
如果你不使用设置管理模块,你可以简单地在 `appsettings.json` 文件中定义设置:
````json
"Settings": {
"Abp.Mailing.Smtp.Host": "127.0.0.1",
"Abp.Mailing.Smtp.Port": "25",
"Abp.Mailing.Smtp.UserName": "",
"Abp.Mailing.Smtp.Password": "",
"Abp.Mailing.Smtp.Domain": "",
"Abp.Mailing.Smtp.EnableSsl": "false",
"Abp.Mailing.Smtp.UseDefaultCredentials": "true",
"Abp.Mailing.DefaultFromAddress": "noreply@abp.io",
"Abp.Mailing.DefaultFromDisplayName": "ABP application"
}
````
You can set/change these settings programmatically using the `ISettingManager` and store values in a database. See the [setting system document](Settings.md) to understand the setting system better.
你可以使用 `ISettingManager` 以编程方式设置/更改这些设置,并将值存储在数据库中。请参阅 [设置系统文档](Settings.md)更好地了解设置系统。
### 加密 SMTP 密码
*Abp.Mailing.Smtp.Password* 必须是一个**加密**值。如果你使用 `ISettingManager` 设置密码,你不必担心。它在内部加密 set 上的值并在 get 上解密。
如果使用 `appsettings.json` 存储密码,则应手动注入 `ISettingEncryptionService` 并使用其 `Encrypt` 方法获取加密值。这可以通过在你的应用程序中创建一个简单的代码来完成。然后你可以删除代码。更好的是,你可以在应用程序中创建一个 UI 来配置电子邮件设置。在这种情况下,你可以直接使用 `ISettingManager` 而不用担心加密。
### ISmtpEmailSenderConfiguration
如果你不想使用设置系统来存储电子邮件发送配置,你可以将 `ISmtpEmailSenderConfiguration` 服务替换为你自己的实现,以从任何其他来源获取配置。 `ISmtpEmailSenderConfiguration` 默认由 `SmtpEmailSenderConfiguration` 实现,如上所述,它从设置系统中获取配置。
## 文本模板集成
ABP 框架提供了一个强大而灵活的[文本模板系统](Text-Templating.md)。你可以使用文本模板系统来创建动态电子邮件内容。注入 `ITemplateRenderer` 并使用 `RenderAsync` 渲染模板。然后将结果用作电子邮件正文。
虽然你可以定义和使用自己的文本模板,但电子邮件发送系统提供了两个简单的内置文本模板。
**示例:使用标准和简单的消息模板发送电子邮件**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.TextTemplating;
namespace Acme.BookStore.Web
{
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ITemplateRenderer _templateRenderer;
public MyService(
IEmailSender emailSender,
ITemplateRenderer templateRenderer)
{
_emailSender = emailSender;
_templateRenderer = templateRenderer;
}
public async Task DoItAsync()
{
var body = await _templateRenderer.RenderAsync(
StandardEmailTemplates.Message,
new
{
message = "This is email body..."
}
);
await _emailSender.SendAsync(
"target-address@domain.com",
"Email subject",
body
);
}
}
}
````
生成的电子邮件正文将如下所示:
````html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
This is email body...
</body>
</html>
````
电子邮件系统定义了具有给定名称的内置文本模板:
"**Abp.StandardEmailTemplates.Message**" 是最简单的带有文本消息的模板:
````html
{%{{{model.message}}}%}
````
此模板使用“Abp.StandardEmailTemplates.Layout”作为其布局.
"**Abp.StandardEmailTemplates.Layout**" 是一个提供 HTML 文档布局的简单模板:
````html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
{%{{{content}}}%}
</body>
</html>
````
最终呈现的消息如上所示。
> 这些模板名称是在 `Volo.Abp.Emailing.Templates.StandardEmailTemplates` 类中定义的。
### 覆盖/替换标准模板
You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](Virtual-File-System.md) (VFS) or replace them in your own template definition provider.
你希望用自己的模板替换标准模板,这样你就可以准备电子邮件模板文件。你可以使用 [虚拟文件系统](Virtual-File-System.md) (VFS),或在你自己的模板定义提供程序中替换它们。
虚拟文件系统中模板的路径如下图所示:
* `/Volo/Abp/Emailing/Templates/Layout.tpl`
* `/Volo/Abp/Emailing/Templates/Message.tpl`
如果你将文件添加到虚拟文件系统中的相同位置,你的文件将覆盖它们。
模板是内联本地化的,这意味着你可以利用 [本地化系统](Localization.md) 的强大功能使你的模板具有多元文化。
详见[文本模板系统](Text-Templating.md) 文档。
> 请注意,你可以为应用程序定义和使用自己的模板,而不是使用标准的简单模板。这些标准模板主要用于可重用的模块,它们不定义自己的模板,而是依赖内置的模板。只需覆盖标准的电子邮件布局模板,就可以轻松自定义使用的模块发送的电子邮件。
## NullEmailSender
`NullEmailSender` 是实现 `IEmailSender` 的内置类,但将电子邮件内容写入 [标准日志系统](Logging.md),而不是实际发送电子邮件。
这个类特别有用,尤其是在你开发是不想发送真实电子邮件。 [应用启动模板](Startup-Templates/Application.md)已经在**DEBUG模式**中使用了这个类,在领域层配置如下:
````csharp
#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
#endif
````
因此,你在 DEBUG 模式下不会收到电子邮件。电子邮件将在生产时按预期发送(RELEASE 模式)。如果你也想在 DEBUG 上发送电子邮件,请删除这些行。
## 其他
* [用于发送电子邮件的MailKit集成](MailKit.md)

49
docs/zh-Hans/MailKit.md

@ -1 +1,48 @@
TODO...
# MailKit 集成
[MailKit](http://www.mimekit.net/) 是一个用于 .net 的跨平台、流行的开源邮件客户端库.ABP 框架提供了一个集成包来使用 MailKit 作为[邮件发送系统](Emailing.md)的收发组件.
## 安装
建议使用 [ABP CLI](CLI.md) 安装包.在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令:
````bash
abp add-package Volo.Abp.MailKit
````
如果执行失败,你首先需要安装 ABP CLI.有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.MailKit).
## 发送电子邮件
### IEmailSender
[注入](Dependency-Injection.md) 标准的 `IEmailSender` 到任何服务并使用 `SendAsync` 方法发送电子邮件.详见 [邮件发送文档](Emailing.md).
> `IEmailSender` 是建议的发送邮件的方式,即使你使用MailKit,因为它使你的代码独立.
### IMailKitSmtpEmailSender
`BuildClientAsync()` 方法扩展了`IEmailSender`.此方法可用于获取可用于执行 MailKit 特定操作的`MailKit.Net.Smtp.SmtpClient`对象.
## 配置
MailKit 集成包使用电子邮件发送系统相同配置选项.请参阅[电子邮件发送文档](Emailing.md) 进行配置.
除了标准设置之外,这个包还定义了 `AbpMailKitOptions` 作为一个简单的 [选项](Options.md) 类.此类仅定义一个选项:
* **SecureSocketOption**:用于设置“SecureSocketOptions” . Default:`null`(使用默认值)
**示例: 使用 *SecureSocketOptions.SslOnConnect***
````csharp
Configure<AbpMailKitOptions>(options =>
{
options.SecureSocketOption = SecureSocketOptions.SslOnConnect;
});
````
请参阅 [MailKit 文档](http://www.mimekit.net/) 了解更多信息.
## 也可以看看
* [电子邮件发送系统](Emailing.md)

4
docs/zh-Hans/Tutorials/Part-1.md

@ -202,7 +202,7 @@ dotnet ef migrations add Created_Book_Entity
![bookstore-efcore-migration](./images/bookstore-efcore-migration.png)
> 如果你使用Visual Studio, 你也许想要在*包管理控制台(PMC)*中使用 `Add-Migration Created_Book_Entity -c BookStoreMigrationsDbContext``Update-Database -c BookStoreMigrationsDbContext` 命令. 确保 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 是 PMC 的*默认项目*.
> 如果你使用Visual Studio, 你也许想要在*包管理控制台(PMC)*中使用 `Add-Migration Created_Book_Entity -c BookStoreDbContext``Update-Database -Context BookStoreDbContext` 命令. 确保 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 是 PMC 的*默认项目*.
{{end}}
@ -290,7 +290,7 @@ namespace Acme.BookStore
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore
namespace Acme.BookStore.Books
{
public class BookDto : AuditedEntityDto<Guid>
{

2
docs/zh-Hans/Tutorials/Part-10.md

@ -126,7 +126,7 @@ migrationBuilder.AddForeignKey(
* 根据 `AuthorId` 字段新建一个索引.
* 声明到 `AppAuthors` 表的外键.
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_AuthorId_To_Book -c BookStoreMigrationsDbContext``Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore`*默认项目* .
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext``Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore`*默认项目* .
{{end}}

2
docs/zh-Hans/Tutorials/Part-2.md

@ -148,6 +148,8 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
}
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
* 本地化关键字名称是任意的. 你可以设置任何名称. 对于特定的文本类型,我们更喜欢遵循一些约定:
* 为按钮项添加 `Menu:` 前缀.
* 使用 `Enum:<enum-type>:<enum-value>` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.

4
docs/zh-Hans/Tutorials/Part-3.md

@ -129,7 +129,7 @@ namespace Acme.BookStore.Web.Pages.Books
</abp-dynamic-form>
````
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单.
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateUpdateBookDto` 类自动构建了表单.
* `abp-model` 指定了 `Book` 属性为模型对象.
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要).
@ -523,6 +523,8 @@ $(function () {
"SuccessfullyDeleted": "Successfully deleted!"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
`Index.js` 的内容最终如下所示:
````js

2
docs/zh-Hans/Tutorials/Part-5.md

@ -122,7 +122,7 @@ namespace Acme.BookStore.Permissions
"Permission:Books.Delete": "Deleting the books"
````
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定.
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定. 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 权限管理界面

2
docs/zh-Hans/Tutorials/Part-6.md

@ -231,6 +231,8 @@ namespace Acme.BookStore
"BookStore:00001": "There is already an author with the same name: {name}"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
`AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息.
## IAuthorRepository

2
docs/zh-Hans/Tutorials/Part-7.md

@ -93,7 +93,7 @@ dotnet ef migrations add Added_Authors
dotnet ef database update
````
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreMigrationsDbContext``Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore`*默认项目* .
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreDbContext``Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore`*默认项目* .
{{else if DB=="Mongo"}}

2
docs/zh-Hans/Tutorials/Part-8.md

@ -381,6 +381,8 @@ authorsPermission.AddChild(
"Permission:Authors.Delete": "Deleting the authors"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
## 对象到对象映射
`AuthorAppService` 使用 `ObjectMapper``Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射.

6
docs/zh-Hans/Tutorials/Part-9.md

@ -219,6 +219,8 @@ $(function () {
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
注意我们加入了额外的键. 它们会在下面的小节中被使用.
### 加入主菜单
@ -821,6 +823,8 @@ export class AuthorComponent implements OnInit {
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 运行应用程序
运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
@ -1228,6 +1232,8 @@ if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 运行应用程序
运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:

34
docs/zh-Hans/docs-nav.json

@ -71,16 +71,44 @@
"text": "应用开发",
"items": [
{
"text": "第一章: 创建新解决方案和列表页",
"text": "1: 创建应用服务端",
"path": "Tutorials/Part-1.md"
},
{
"text": "第二章: 增删改查操作",
"text": "2: 图书列表页面",
"path": "Tutorials/Part-2.md"
},
{
"text": "第三章: 集成测试",
"text": "3: 创建,更新和删除书籍",
"path": "Tutorials/Part-3.md"
},
{
"text": "4: 集成测试",
"path": "Tutorials/Part-4.md"
},
{
"text": "5: 授权",
"path": "Tutorials/Part-5.md"
},
{
"text": "6: 作者:领域层",
"path": "Tutorials/Part-6.md"
},
{
"text": "7: 作者:数据库集成",
"path": "Tutorials/Part-7.md"
},
{
"text": "8: 作者:应用服务层",
"path": "Tutorials/Part-8.md"
},
{
"text": "9: 作者:用户界面",
"path": "Tutorials/Part-9.md"
},
{
"text": "10: 图书-作者关系",
"path": "Tutorials/Part-10.md"
}
]
},

11
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs

@ -4,6 +4,8 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars;
using Volo.Abp.BlazoriseUI;
using System.Linq;
using System.Collections.ObjectModel;
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Layout;
@ -30,8 +32,8 @@ public partial class PageHeader : ComponentBase
[Parameter] // TODO: Consider removing this property in future and use only PageLayout.
public List<BreadcrumbItem> BreadcrumbItems {
get => PageLayout.BreadcrumbItems;
set => PageLayout.BreadcrumbItems = value;
get => PageLayout.BreadcrumbItems.ToList();
set => PageLayout.BreadcrumbItems = new ObservableCollection<BreadcrumbItem>(value);
}
[Parameter]
@ -53,7 +55,10 @@ public partial class PageHeader : ComponentBase
if (!Options.Value.RenderToolbar)
{
PageLayout.ToolbarItems.Clear();
PageLayout.ToolbarItems.AddRange(toolbarItems);
foreach (var item in toolbarItems)
{
PageLayout.ToolbarItems.Add(item);
}
return;
}

23
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs

@ -1,16 +1,29 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Volo.Abp.AspNetCore.Components.Web.Theming.PageToolbars;
using Volo.Abp.BlazoriseUI;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.Web.Theming.Layout;
public class PageLayout : IScopedDependency
public class PageLayout : IScopedDependency, INotifyPropertyChanged
{
private string title;
// TODO: Consider using this property for setting Page Title too.
public virtual string Title { get; set; }
public virtual string Title
{
get => title;
set
{
title = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
}
}
public virtual ObservableCollection<BreadcrumbItem> BreadcrumbItems { get; set; } = new();
public virtual List<BreadcrumbItem> BreadcrumbItems { get; set; } = new();
public virtual ObservableCollection<PageToolbarItem> ToolbarItems { get; set; } = new();
public virtual List<PageToolbarItem> ToolbarItems { get; set; } = new();
public event PropertyChangedEventHandler PropertyChanged;
}

4
framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor

@ -9,7 +9,9 @@
TotalItems="@TotalItems"
ShowPager="@ShowPager"
CurrentPage="@CurrentPage"
PageSize="@PageSize">
PageSize="@PageSize"
Responsive="@Responsive"
Class="@Class">
<LoadingTemplate>
<Row Class="w-100 align-items-center" Style="height: 150px;">
<Column>

12
framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs

@ -1,12 +1,10 @@
using System;
using Blazorise.Extensions;
using Blazorise.DataGrid;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Blazorise.DataGrid;
using Blazorise.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions;
using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns;
namespace Volo.Abp.BlazoriseUI.Components;
@ -34,6 +32,10 @@ public partial class AbpExtensibleDataGrid<TItem> : ComponentBase
[Parameter] public int CurrentPage { get; set; } = 1;
[Parameter] public string Class { get; set; }
[Parameter] public bool Responsive { get; set; }
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }

2
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs

@ -8,7 +8,7 @@ public class LocalizationResourceContributorList : List<ILocalizationResourceCon
{
public LocalizedString GetOrNull(string cultureName, string name)
{
foreach (var contributor in this.AsQueryable().Reverse())
foreach (var contributor in this.Select(x => x).Reverse())
{
var localString = contributor.GetOrNull(cultureName, name);
if (localString != null)

12
framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs

@ -0,0 +1,12 @@
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp.Swashbuckle;
namespace Microsoft.Extensions.DependencyInjection;
public static class AbpSwaggerGenOptionsExtensions
{
public static void HideAbpEndpoints(this SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.DocumentFilter<AbpSwashbuckleDocumentFilter>();
}
}

28
framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Volo.Abp.Swashbuckle;
public class AbpSwashbuckleDocumentFilter : IDocumentFilter
{
protected string[] ActionUrlPrefixes = new[] {"Volo.Abp"};
public virtual void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var actionUrls = context.ApiDescriptions
.Select(apiDescription => apiDescription.ActionDescriptor)
.Where(actionDescriptor => !string.IsNullOrEmpty(actionDescriptor.DisplayName) &&
ActionUrlPrefixes.Any(actionUrlPrefix => !actionDescriptor.DisplayName.Contains(actionUrlPrefix)))
.DistinctBy(actionDescriptor => actionDescriptor.AttributeRouteInfo?.Template)
.Select(actionDescriptor => actionDescriptor.AttributeRouteInfo?.Template.EnsureStartsWith('/'))
.Where(actionUrl => !string.IsNullOrEmpty(actionUrl))
.ToList();
swaggerDoc
.Paths
.RemoveAll(path => !actionUrls.Contains(path.Key));
}
}

2
modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/Default.cshtml

@ -5,7 +5,7 @@
var elementId = string.IsNullOrEmpty(menuItem.ElementId) ? string.Empty : $"id=\"{menuItem.ElementId}\"";
var cssClass = string.IsNullOrEmpty(menuItem.CssClass) ? string.Empty : menuItem.CssClass;
var disabled = menuItem.IsDisabled ? "disabled" : string.Empty;
var url = string.IsNullOrEmpty(menuItem.Url) ? "#" : Url.Content(menuItem.Url);
var url = string.IsNullOrEmpty(menuItem.Url) ? "#" : Url.IsLocalUrl(menuItem.Url) ? Url.Content(menuItem.Url.EnsureStartsWith('~')) : menuItem.Url;
if (menuItem.IsLeaf)
{
if (menuItem.Url != null)

2
modules/basic-theme/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Menu/_MenuItem.cshtml

@ -4,7 +4,7 @@
var elementId = string.IsNullOrEmpty(Model.ElementId) ? string.Empty : $"id=\"{Model.ElementId}\"";
var cssClass = string.IsNullOrEmpty(Model.CssClass) ? string.Empty : Model.CssClass;
var disabled = Model.IsDisabled ? "disabled" : string.Empty;
var url = string.IsNullOrEmpty(Model.Url) ? "#" : Url.Content(Model.Url);
var url = string.IsNullOrEmpty(Model.Url) ? "#" : Url.IsLocalUrl(Model.Url) ? Url.Content(Model.Url.EnsureStartsWith('~')) : Model.Url;
}
@if (Model.IsLeaf)
{

2
modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs

@ -29,7 +29,7 @@ public partial class EmailSettingGroupViewComponent
LocalizationResource = typeof(AbpSettingManagementResource);
}
protected override async Task OnInitializedAsync()
protected async override Task OnInitializedAsync()
{
try
{

2
modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor

@ -11,7 +11,7 @@
<Card>
<CardBody>
<Tabs @bind-SelectedTab="@SelectedGroup" TabPosition="TabPosition.Left" Pills="true">
<Tabs @bind-SelectedTab="@SelectedGroup" TabPosition="TabPosition.Left" Pills="true" Mode="TabsMode.LazyReload">
<Items>
@foreach (var group in SettingComponentCreationContext.Groups)
{

4
npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts

@ -4,7 +4,7 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { BOOTSTRAP, createLazyStyleHref, LazyStyleHandler } from '../handlers';
const languageChange$ = of({ payload: 'en' });
const currentLang$ = of({ payload: 'en' });
describe('LazyStyleHandler', () => {
let spectator: SpectatorService<LazyStyleHandler>;
@ -17,7 +17,7 @@ describe('LazyStyleHandler', () => {
DocumentDirHandlerService,
{
provide: LocalizationService,
useValue: { currentLang: 'en', languageChange$ },
useValue: { currentLang: 'en', currentLang$ },
},
],
});

Loading…
Cancel
Save