mirror of https://github.com/abpframework/abp.git
4350 changed files with 29573 additions and 794809 deletions
@ -1,3 +0,0 @@ |
|||||
## AutoMapper Integration |
|
||||
|
|
||||
TODO |
|
||||
@ -0,0 +1,201 @@ |
|||||
|
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications |
||||
|
|
||||
|
This guide demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from **Azure Active Directory**. |
||||
|
|
||||
|
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly. |
||||
|
|
||||
|
Two different **alternative approaches** for AzureAD integration will be demonstrated for better coverage. |
||||
|
|
||||
|
1. **AddAzureAD**: This approach uses Microsoft [AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/) which is very popular when users search the web about how to integrate AzureAD to their web application. |
||||
|
|
||||
|
2. **AddOpenIdConnect**: This approach uses default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for not only AzureAD but for all OpenId connections. |
||||
|
|
||||
|
> There is **no difference** in functionality between these approaches. AddAzureAD is an abstracted way of OpenIdConnection ([source](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122)) with predefined cookie settings. |
||||
|
> |
||||
|
> However there are key differences in integration to ABP applications because of default configurated signin schemes which will be explained below. |
||||
|
|
||||
|
## 1. AddAzureAD |
||||
|
|
||||
|
This approach uses the most common way to integrate AzureAD by using the [Microsoft AzureAD UI nuget package](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/). |
||||
|
|
||||
|
If you choose this approach, you will need to install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** project. Also, since AddAzureAD extension uses [configuration binding](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration), you need to update your appsettings.json file located in your **.Web** project. |
||||
|
|
||||
|
#### **Updating `appsettings.json`** |
||||
|
|
||||
|
You need to add a new section to your `appsettings.json` which will be binded to configuration when configuring the `OpenIdConnectOptions`: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> Important configuration here is the CallbackPath. This value must be the same with one of your Azure AD-> app registrations-> Authentication -> RedirectUri. |
||||
|
|
||||
|
Then, you need to configure the `OpenIdConnectOptions` to complete the integration. |
||||
|
|
||||
|
#### Configuring OpenIdConnectOptions |
||||
|
|
||||
|
In your **.Web** project, locate your **ApplicationWebModule** and modify `ConfigureAuthentication` method with the following: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "Acme.BookStore"; |
||||
|
}) |
||||
|
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
||||
|
|
||||
|
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
||||
|
{ |
||||
|
options.Authority = options.Authority + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
|
||||
|
options.TokenValidationParameters.ValidateIssuer = false; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
options.SaveTokens = true; |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> **Don't forget to:** |
||||
|
> |
||||
|
> * Add `.AddAzureAD(options => configuration.Bind("AzureAd", options))` after `.AddAuthentication()`. This binds your AzureAD appsettings and easy to miss out. |
||||
|
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. |
||||
|
> * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. |
||||
|
> * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
||||
|
> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
||||
|
|
||||
|
You are done and integration is completed. |
||||
|
|
||||
|
## 2. Alternative Approach: AddOpenIdConnect |
||||
|
|
||||
|
If you don't want to use an extra nuget package in your application, you can use the straight default [OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/) which can be used for all OpenId connections including AzureAD external authentication. |
||||
|
|
||||
|
You don't have to use `appsettings.json` configuration but it is a good practice to set AzureAD information in the `appsettings.json`. |
||||
|
|
||||
|
To get the AzureAD information from `appsettings.json`, which will be used in `OpenIdConnectOptions` configuration, simply add a new section to `appsettings.json` located in your **.Web** project: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Then, In your **.Web** project; you can modify the `ConfigureAuthentication` method located in your **ApplicationWebModule** with the following: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
|
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "BookStore"; |
||||
|
}) |
||||
|
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
||||
|
{ |
||||
|
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.SaveTokens = true; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
And that's it, integration is completed. Keep on mind that you can connect any other external authentication providers. |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
# FAQ |
||||
|
|
||||
|
* Help! `GetExternalLoginInfoAsync` returns `null`! |
||||
|
|
||||
|
* There can be 2 reasons for this; |
||||
|
|
||||
|
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`: |
||||
|
|
||||
|
````csharp |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
```` |
||||
|
|
||||
|
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping: |
||||
|
|
||||
|
````csharp |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error! |
||||
|
|
||||
|
* If you set your **CallbackPath** in appsettings as: |
||||
|
|
||||
|
````csharp |
||||
|
"AzureAd": { |
||||
|
... |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`. |
||||
|
|
||||
|
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error! |
||||
|
|
||||
|
|
||||
|
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add |
||||
|
|
||||
|
````csharp |
||||
|
options.Scope.Add("email"); |
||||
|
```` |
||||
|
|
||||
|
to your openid configuration. |
||||
|
|
||||
|
* How can I **debug/watch** which claims I get before they get mapped? |
||||
|
|
||||
|
* You can add a simple event under openid configuration to debug before mapping like: |
||||
|
|
||||
|
````csharp |
||||
|
options.Events.OnTokenValidated = (async context => |
||||
|
{ |
||||
|
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
||||
|
await Task.CompletedTask; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
||||
|
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). |
||||
@ -0,0 +1,113 @@ |
|||||
|
# How to Customize the Login Page for MVC / Razor Page Applications |
||||
|
|
||||
|
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. |
||||
|
|
||||
|
This document explains how to customize the login page for your own application. |
||||
|
|
||||
|
## Create a Login PageModel |
||||
|
|
||||
|
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module. |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomLoginModel : LoginModel |
||||
|
{ |
||||
|
public CustomLoginModel( |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
||||
|
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
||||
|
: base(schemeProvider, accountOptions) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. |
||||
|
|
||||
|
Then you can override any method you need and add new methods and properties needed by the UI. |
||||
|
|
||||
|
## Overriding the Login Page UI |
||||
|
|
||||
|
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md). |
||||
|
|
||||
|
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below: |
||||
|
|
||||
|
````xml |
||||
|
@page |
||||
|
@using Volo.Abp.Account.Settings |
||||
|
@using Volo.Abp.Settings |
||||
|
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
||||
|
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
||||
|
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
||||
|
@if (Model.EnableLocalLogin) |
||||
|
{ |
||||
|
<div class="card mt-3 shadow-sm rounded"> |
||||
|
<div class="card-body p-5"> |
||||
|
<h4>@L["Login"]</h4> |
||||
|
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
||||
|
{ |
||||
|
<strong> |
||||
|
@L["AreYouANewUser"] |
||||
|
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
||||
|
</strong> |
||||
|
} |
||||
|
<form method="post" class="mt-4"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
||||
|
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.Password"></label> |
||||
|
<input asp-for="LoginInput.Password" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-check"> |
||||
|
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
||||
|
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
||||
|
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
||||
|
</label> |
||||
|
</div> |
||||
|
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-footer text-center border-0"> |
||||
|
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="col-md-6"> |
||||
|
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
||||
|
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
@foreach (var provider in Model.VisibleExternalProviders) |
||||
|
{ |
||||
|
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
||||
|
} |
||||
|
</form> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="alert alert-warning"> |
||||
|
<strong>@L["InvalidLoginRequest"]</strong> |
||||
|
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
||||
|
</div> |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs. |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). |
||||
@ -0,0 +1,101 @@ |
|||||
|
# How to Customize the SignIn Manager for ABP Applications |
||||
|
|
||||
|
After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). |
||||
|
|
||||
|
To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. |
||||
|
|
||||
|
This document explains how to customize the SignIn Manager for your own application. |
||||
|
|
||||
|
## Create a CustomSignInManager |
||||
|
|
||||
|
Create a new class inheriting the [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) of Microsoft Identity package. |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
||||
|
{ |
||||
|
public CustomSignInManager( |
||||
|
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
||||
|
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
||||
|
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
||||
|
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
||||
|
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
||||
|
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
||||
|
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> It is important to use **Volo.Abp.Identity.IdentityUser** type for SignInManager to inherit, not the AppUser of your application. |
||||
|
|
||||
|
Afterwards you can override any of the SignIn Manager methods you need and add new methods and properties needed for your authentication or registration flow. |
||||
|
|
||||
|
## Overriding the GetExternalLoginInfoAsync Method |
||||
|
|
||||
|
In this case we'll be overriding the `GetExternalLoginInfoAsync` method which is invoked when a third party authentication is implemented. |
||||
|
|
||||
|
A good way to override a method is copying its [source code](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674). In this case, we will be using a minorly modified version of the source code which explicitly shows the namespaces of the methods and properties to help better understanding of the concept. |
||||
|
|
||||
|
````csharp |
||||
|
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
||||
|
{ |
||||
|
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
||||
|
var items = auth?.Properties?.Items; |
||||
|
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (expectedXsrf != null) |
||||
|
{ |
||||
|
if (!items.ContainsKey("XsrfKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
var userId = items[XsrfKey] as string; |
||||
|
if (userId != expectedXsrf) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
||||
|
var provider = items[LoginProviderKey] as string; |
||||
|
if (providerKey == null || provider == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
||||
|
?? provider; |
||||
|
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
||||
|
{ |
||||
|
AuthenticationTokens = auth.Properties.GetTokens() |
||||
|
}; |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). |
||||
|
|
||||
|
## Register to Dependency Injection |
||||
|
|
||||
|
Registering `CustomSignInManager` should be done with adding **AddSignInManager** extension method of the [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) of the [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs). |
||||
|
|
||||
|
Inside your `.Web` project, locate the `YourProjectNameWebModule` and add the following code under the `PreConfigureServices` method to replace the old `SignInManager` with your customized one: |
||||
|
|
||||
|
````csharp |
||||
|
PreConfigure<IdentityBuilder>(identityBuilder => |
||||
|
{ |
||||
|
identityBuilder.AddSignInManager<CustomSignInManager>(); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## The Source Code |
||||
|
|
||||
|
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization). |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). |
||||
|
* [Identity Management Module](../Modules/Identity.md). |
||||
@ -0,0 +1,9 @@ |
|||||
|
# "How To" Guides |
||||
|
|
||||
|
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. |
||||
|
|
||||
|
## Authentication |
||||
|
|
||||
|
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) |
||||
|
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) |
||||
|
* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) |
||||
@ -0,0 +1,365 @@ |
|||||
|
# Object Extensions |
||||
|
|
||||
|
ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module. |
||||
|
|
||||
|
> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes. |
||||
|
|
||||
|
## IHasExtraProperties Interface |
||||
|
|
||||
|
This is the interface to make a class extensible. It simply defines a `Dictionary` property: |
||||
|
|
||||
|
````csharp |
||||
|
Dictionary<string, object> ExtraProperties { get; } |
||||
|
```` |
||||
|
|
||||
|
Then you can add or get extra properties using this dictionary. |
||||
|
|
||||
|
### Base Classes |
||||
|
|
||||
|
`IHasExtraProperties` interface is implemented by several base classes by default: |
||||
|
|
||||
|
* Implemented by the `AggregateRoot` class (see [entities](Entities.md)). |
||||
|
* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes. |
||||
|
* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object. |
||||
|
|
||||
|
So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually. |
||||
|
|
||||
|
### Fundamental Extension Methods |
||||
|
|
||||
|
While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties. |
||||
|
|
||||
|
#### SetProperty |
||||
|
|
||||
|
Used to set the value of an extra property: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title"); |
||||
|
user.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
`SetProperty` returns the same object, so you can chain it: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title") |
||||
|
.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
#### GetProperty |
||||
|
|
||||
|
Used to read the value of an extra property: |
||||
|
|
||||
|
````csharp |
||||
|
var title = user.GetProperty<string>("Title"); |
||||
|
|
||||
|
if (user.GetProperty<bool>("IsSuperUser")) |
||||
|
{ |
||||
|
//... |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* `GetProperty` is a generic method and takes the object type as the generic parameter. |
||||
|
* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc). |
||||
|
|
||||
|
##### Non Primitive Property Types |
||||
|
|
||||
|
If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`. |
||||
|
|
||||
|
#### HasProperty |
||||
|
|
||||
|
Used to check if the object has a property set before. |
||||
|
|
||||
|
#### RemoveProperty |
||||
|
|
||||
|
Used to remove a property from the object. Use this methods instead of setting a `null` value for the property. |
||||
|
|
||||
|
### Some Best Practices |
||||
|
|
||||
|
Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead; |
||||
|
|
||||
|
* Define a constant for your extra property names |
||||
|
* Create extension methods to easily set your extra properties. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````csharp |
||||
|
public static class IdentityUserExtensions |
||||
|
{ |
||||
|
private const string TitlePropertyName = "Title"; |
||||
|
|
||||
|
public static void SetTitle(this IdentityUser user, string title) |
||||
|
{ |
||||
|
user.SetProperty(TitlePropertyName, title); |
||||
|
} |
||||
|
|
||||
|
public static string GetTitle(this IdentityUser user) |
||||
|
{ |
||||
|
return user.GetProperty<string>(TitlePropertyName); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
Then you can easily set or get the `Title` property: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetTitle("My Title"); |
||||
|
var title = user.GetTitle(); |
||||
|
```` |
||||
|
|
||||
|
## Object Extension Manager |
||||
|
|
||||
|
While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes. |
||||
|
|
||||
|
Explicitly defining an extra property has some use cases: |
||||
|
|
||||
|
* Allows to control how the extra property is handled on object to object mapping (see the section below). |
||||
|
* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md). |
||||
|
|
||||
|
> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside. |
||||
|
|
||||
|
### AddOrUpdate |
||||
|
|
||||
|
`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object. |
||||
|
|
||||
|
Example: Define extra properties for the `IdentityUser` entity: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdate<IdentityUser>(options => |
||||
|
{ |
||||
|
options.AddOrUpdateProperty<string>("SocialSecurityNumber"); |
||||
|
options.AddOrUpdateProperty<bool>("IsSuperUser"); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
### AddOrUpdateProperty |
||||
|
|
||||
|
While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber"); |
||||
|
```` |
||||
|
|
||||
|
Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<string>( |
||||
|
new[] |
||||
|
{ |
||||
|
typeof(IdentityUserDto), |
||||
|
typeof(IdentityUserCreateDto), |
||||
|
typeof(IdentityUserUpdateDto) |
||||
|
}, |
||||
|
"SocialSecurityNumber" |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
### Property Configuration |
||||
|
|
||||
|
`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
//Configure options... |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document. |
||||
|
|
||||
|
The following sections explain the fundamental property configuration options. |
||||
|
|
||||
|
#### CheckPairDefinitionOnMapping |
||||
|
|
||||
|
Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better. |
||||
|
|
||||
|
## Validation |
||||
|
|
||||
|
You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation: |
||||
|
|
||||
|
1. You can add **data annotation attributes** for a property. |
||||
|
2. You can write an action (code block) to perform a **custom validation**. |
||||
|
|
||||
|
Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated. |
||||
|
|
||||
|
### Data Annotation Attributes |
||||
|
|
||||
|
All of the standard data annotation attributes are valid for extra properties. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUserCreateDto, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
options.ValidationAttributes.Add( |
||||
|
new StringLengthAttribute(32) { |
||||
|
MinimumLength = 6 |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided. |
||||
|
|
||||
|
### Custom Validation |
||||
|
|
||||
|
If you need, you can add a custom action that is executed to validate the extra properties. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUserCreateDto, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.Validators.Add(context => |
||||
|
{ |
||||
|
var socialSecurityNumber = context.Value as string; |
||||
|
|
||||
|
if (socialSecurityNumber == null || |
||||
|
socialSecurityNumber.StartsWith("X")) |
||||
|
{ |
||||
|
context.ValidationErrors.Add( |
||||
|
new ValidationResult( |
||||
|
"Invalid social security number: " + socialSecurityNumber, |
||||
|
new[] { "SocialSecurityNumber" } |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios. |
||||
|
|
||||
|
In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdate<IdentityUserCreateDto>(objConfig => |
||||
|
{ |
||||
|
//Define two properties with their own validation rules |
||||
|
|
||||
|
objConfig.AddOrUpdateProperty<string>("Password", propertyConfig => |
||||
|
{ |
||||
|
propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
}); |
||||
|
|
||||
|
objConfig.AddOrUpdateProperty<string>("PasswordRepeat", propertyConfig => |
||||
|
{ |
||||
|
propertyConfig.ValidationAttributes.Add(new RequiredAttribute()); |
||||
|
}); |
||||
|
|
||||
|
//Write a common validation logic works on multiple properties |
||||
|
|
||||
|
objConfig.Validators.Add(context => |
||||
|
{ |
||||
|
if (context.ValidatingObject.GetProperty<string>("Password") != |
||||
|
context.ValidatingObject.GetProperty<string>("PasswordRepeat")) |
||||
|
{ |
||||
|
context.ValidationErrors.Add( |
||||
|
new ValidationResult( |
||||
|
"Please repeat the same password!", |
||||
|
new[] { "Password", "PasswordRepeat" } |
||||
|
) |
||||
|
); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## Object to Object Mapping |
||||
|
|
||||
|
Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. |
||||
|
|
||||
|
This section offers some **good practices** to control your extra properties on object mapping. |
||||
|
|
||||
|
### MapExtraPropertiesTo |
||||
|
|
||||
|
`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo(identityUserDto); |
||||
|
```` |
||||
|
|
||||
|
`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. |
||||
|
|
||||
|
#### MappingPropertyDefinitionChecks |
||||
|
|
||||
|
`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
MappingPropertyDefinitionChecks.None |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. |
||||
|
|
||||
|
If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
#### Ignored Properties |
||||
|
|
||||
|
You may want to ignore some properties on a specific mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
ignoredProperties: new[] {"MySensitiveProp"} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
Ignored properties are not copied to the target object. |
||||
|
|
||||
|
#### AutoMapper Integration |
||||
|
|
||||
|
If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. |
||||
|
|
||||
|
You can use the `MapExtraProperties()` method inside your mapping profile. |
||||
|
|
||||
|
````csharp |
||||
|
public class MyProfile : Profile |
||||
|
{ |
||||
|
public MyProfile() |
||||
|
{ |
||||
|
CreateMap<IdentityUser, IdentityUserDto>() |
||||
|
.MapExtraProperties(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
It has the same parameters with the `MapExtraPropertiesTo` method. |
||||
|
|
||||
|
## Entity Framework Core Database Mapping |
||||
|
|
||||
|
If you're using the EF Core, you can map an extra property to a table field in the database. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.MapEfCore(b => b.HasMaxLength(32)); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. |
||||
@ -0,0 +1,101 @@ |
|||||
|
# ContainerStrategy |
||||
|
|
||||
|
`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public containerRef: ViewContainerRef, |
||||
|
private index?: number, // works only in InsertIntoContainerStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. |
||||
|
|
||||
|
|
||||
|
### getIndex |
||||
|
|
||||
|
```js |
||||
|
getIndex(): number |
||||
|
``` |
||||
|
|
||||
|
This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. |
||||
|
|
||||
|
|
||||
|
### prepare |
||||
|
|
||||
|
```js |
||||
|
prepare(): void |
||||
|
``` |
||||
|
|
||||
|
This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ClearContainerStrategy |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## InsertIntoContainerStrategy |
||||
|
|
||||
|
`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Container Strategies |
||||
|
|
||||
|
Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Clear |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Clears given container before content projection. |
||||
|
|
||||
|
|
||||
|
### Append |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be appended to the container. |
||||
|
|
||||
|
|
||||
|
### Prepend |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be prepended to the container. |
||||
|
|
||||
|
|
||||
|
### Insert |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Insert( |
||||
|
containerRef: ViewContainerRef, |
||||
|
index: number, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,78 @@ |
|||||
|
# Content Projection |
||||
|
|
||||
|
You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { ContentProjectionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private contentProjectionService: ContentProjectionService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. |
||||
|
|
||||
|
### How to Project Components to Root Level |
||||
|
|
||||
|
If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.AppendComponentToBody( |
||||
|
SomeOverlayComponent, |
||||
|
{ someOverlayProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In the example above, `SomeOverlayComponent` component will placed at the **end** of `<body>` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. |
||||
|
|
||||
|
### How to Project Components and Templates into a Container |
||||
|
|
||||
|
If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( |
||||
|
SomeComponent, |
||||
|
viewContainerRefOfTarget, |
||||
|
{ someProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. |
||||
|
|
||||
|
Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### projectContent |
||||
|
|
||||
|
```js |
||||
|
projectContent<T extends Type<any> | TemplateRef<any>>( |
||||
|
projectionStrategy: ProjectionStrategy<T>, |
||||
|
injector = this.injector, |
||||
|
): ComponentRef<C> | EmbeddedViewRef<C> |
||||
|
``` |
||||
|
|
||||
|
- `projectionStrategy` parameter is the primary focus here and is explained above. |
||||
|
- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [TrackByService](./Track-By-Service.md) |
||||
@ -0,0 +1,74 @@ |
|||||
|
# ContentSecurityStrategy |
||||
|
|
||||
|
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public nonce?: string) |
||||
|
``` |
||||
|
|
||||
|
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives. |
||||
|
|
||||
|
|
||||
|
### applyCSP |
||||
|
|
||||
|
```js |
||||
|
applyCSP(element: HTMLScriptElement | HTMLStyleElement): void |
||||
|
``` |
||||
|
|
||||
|
This method maps the aforementioned properties to the given `element`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## LooseContentSecurityPolicy |
||||
|
|
||||
|
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## NoContentSecurityPolicy |
||||
|
|
||||
|
`NoContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Content Security Strategies |
||||
|
|
||||
|
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Loose |
||||
|
|
||||
|
```js |
||||
|
CONTENT_SECURITY_STRATEGY.Loose(nonce: string) |
||||
|
``` |
||||
|
|
||||
|
`nonce` will be set. |
||||
|
|
||||
|
|
||||
|
### None |
||||
|
|
||||
|
```js |
||||
|
CONTENT_SECURITY_STRATEGY.None() |
||||
|
``` |
||||
|
|
||||
|
Nothing will be done. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
|
- [ContentStrategy](./Content-Strategy.md) |
||||
|
|
||||
@ -0,0 +1,95 @@ |
|||||
|
# ContentStrategy |
||||
|
|
||||
|
`ContentStrategy` is an abstract class exposed by @abp/ng.core package. It helps you create inline scripts or styles. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public content: string, |
||||
|
protected domStrategy?: DomStrategy, |
||||
|
protected contentSecurityStrategy?: ContentSecurityStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `content` is set to `<script>` and `<style>` elements as `textContent` property. |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
||||
|
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_) |
||||
|
|
||||
|
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### createElement |
||||
|
|
||||
|
```js |
||||
|
createElement(): HTMLScriptElement | HTMLStyleElement |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns a `<script>` or `<style>` element with `content` set as `textContent`. |
||||
|
|
||||
|
|
||||
|
### insertElement |
||||
|
|
||||
|
```js |
||||
|
insertElement(): void |
||||
|
``` |
||||
|
|
||||
|
This method creates and inserts a `<script>` or `<style>` element. |
||||
|
|
||||
|
|
||||
|
## ScriptContentStrategy |
||||
|
|
||||
|
`ScriptContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<script>` element to the DOM**. |
||||
|
|
||||
|
## StyleContentStrategy |
||||
|
|
||||
|
`StyleContentStrategy` is a class that extends `ContentStrategy`. It lets you **insert a `<style>` element to the DOM**. |
||||
|
|
||||
|
|
||||
|
## Predefined Content Strategies |
||||
|
|
||||
|
Predefined content strategies are accessible via `CONTENT_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendScriptToBody |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendScriptToBody(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendScriptToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendScriptToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendStyleToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.AppendStyleToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependStyleToHead |
||||
|
|
||||
|
```js |
||||
|
CONTENT_STRATEGY.PrependStyleToHead(content: string) |
||||
|
``` |
||||
|
|
||||
|
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
@ -0,0 +1,117 @@ |
|||||
|
# ContextStrategy |
||||
|
|
||||
|
`ContextStrategy` is an abstract class exposed by @abp/ng.core package. There are three context strategies extending it: `ComponentContextStrategy`, `TemplateContextStrategy`, and `NoContextStrategy`. Implementing the same methods and properties, all of these strategies help you define how projected content will get their context. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ComponentContextStrategy |
||||
|
|
||||
|
`ComponentContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected component**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public context: Partial<InferredInstanceOf<T>>) {} |
||||
|
``` |
||||
|
|
||||
|
- `T` refers to component type here, i.e. `Type<C>`. |
||||
|
- `InferredInstanceOf` is a utility type exposed by @abp/ng.core package. It infers component shape. |
||||
|
- `context` will be mapped to properties of the projected component. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>> |
||||
|
``` |
||||
|
|
||||
|
This method maps each prop of the context to the component property with the same name and calls change detection. It returns the context after mapping. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## TemplateContextStrategy |
||||
|
|
||||
|
`TemplateContextStrategy` is a class that extends `ContextStrategy`. It lets you **pass context to a projected template**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor(public context: Partial<InferredContextOf<T>>) {} |
||||
|
``` |
||||
|
|
||||
|
- `T` refers to template context type here, i.e. `TemplateRef<C>`. |
||||
|
- `InferredContextOf` is a utility type exposed by @abp/ng.core package. It infers context shape. |
||||
|
- `context` will be mapped to properties of the projected template. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(): Partial<InferredContextOf<T>> |
||||
|
``` |
||||
|
|
||||
|
This method does nothing and only returns the context, because template context is not mapped but passed in as parameter to `createEmbeddedView` method. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## NoContextStrategy |
||||
|
|
||||
|
`NoContextStrategy` is a class that extends `ContextStrategy`. It lets you **skip passing any context to projected content**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor() |
||||
|
``` |
||||
|
|
||||
|
Unlike other context strategies, `NoContextStrategy` contructor takes no parameters. |
||||
|
|
||||
|
|
||||
|
### setContext |
||||
|
|
||||
|
```js |
||||
|
setContext(): undefined |
||||
|
``` |
||||
|
|
||||
|
Since there is no context, this method gets no parameters and will return `undefined`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Context Strategies |
||||
|
|
||||
|
Predefined context strategies are accessible via `CONTEXT_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### None |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.None() |
||||
|
``` |
||||
|
|
||||
|
This strategy will not pass any context to the projected content. |
||||
|
|
||||
|
|
||||
|
### Component |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.Component(context: Partial<InferredContextOf<T>>) |
||||
|
``` |
||||
|
|
||||
|
This strategy will help you pass the given context to the projected component. |
||||
|
|
||||
|
|
||||
|
### Template |
||||
|
|
||||
|
```js |
||||
|
CONTEXT_STRATEGY.Template(context: Partial<InferredContextOf<T>>) |
||||
|
``` |
||||
|
|
||||
|
This strategy will help you pass the given context to the projected template. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,60 @@ |
|||||
|
# CrossOriginStrategy |
||||
|
|
||||
|
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`. |
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public crossorigin: 'anonymous' | 'use-credentials', |
||||
|
public integrity?: string |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin). |
||||
|
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity). |
||||
|
|
||||
|
|
||||
|
### setCrossOrigin |
||||
|
|
||||
|
```js |
||||
|
setCrossOrigin(element: HTMLElement): void |
||||
|
``` |
||||
|
|
||||
|
This method maps the aforementioned properties to the given `element`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Cross-Origin Strategies |
||||
|
|
||||
|
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Anonymous |
||||
|
|
||||
|
```js |
||||
|
CROSS_ORIGIN_STRATEGY.Anonymous(integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
`crossorigin` will be set as `"anonymous"` and `integrity` is optional. |
||||
|
|
||||
|
|
||||
|
### UseCredentials |
||||
|
|
||||
|
```js |
||||
|
CROSS_ORIGIN_STRATEGY.UseCredentials(integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [LoadingStrategy](./Loading-Strategy.md) |
||||
@ -0,0 +1,87 @@ |
|||||
|
# Dom Insertion (of Scripts and Styles) |
||||
|
|
||||
|
You can use the `DomInsertionService` in @abp/ng.core package in order to insert scripts and styles in an easy and explicit way. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `DomInsertionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `insertContent` method of `DomInsertionService` to create a `<script>` or `<style>` element with given content in the DOM at the desired position. There is also the `projectContent` method for dynamically rendering components and templates. |
||||
|
|
||||
|
### How to Insert Scripts |
||||
|
|
||||
|
The first parameter of `insertContent` method expects a `ContentStrategy`. If you pass a `ScriptContentStrategy` instance, the `DomInsertionService` will create a `<script>` element with given `content` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.domInsertionService.insertContent( |
||||
|
CONTENT_STRATEGY.AppendScriptToBody('alert()') |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>`. |
||||
|
|
||||
|
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
||||
|
|
||||
|
### How to Insert Styles |
||||
|
|
||||
|
If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private domInsertionService: DomInsertionService) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.domInsertionService.insertContent( |
||||
|
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>`. |
||||
|
|
||||
|
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### insertContent |
||||
|
|
||||
|
```js |
||||
|
insertContent(contentStrategy: ContentStrategy): void |
||||
|
``` |
||||
|
|
||||
|
- `contentStrategy` parameter is the primary focus here and is explained above. |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [ContentProjectionService](./Content-Projection-Service.md) |
||||
@ -0,0 +1,90 @@ |
|||||
|
# DomStrategy |
||||
|
|
||||
|
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`. |
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public target?: HTMLElement, |
||||
|
public position?: InsertPosition |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `target` is an HTMLElement (_default: document.head_). |
||||
|
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_). |
||||
|
|
||||
|
|
||||
|
### insertElement |
||||
|
|
||||
|
```js |
||||
|
insertElement(element: HTMLElement): void |
||||
|
``` |
||||
|
|
||||
|
This method inserts given `element` to `target` based on the `position`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Dom Strategies |
||||
|
|
||||
|
Predefined dom strategies are accessible via `DOM_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendToBody |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AppendToBody() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the end of `<body>`. |
||||
|
|
||||
|
|
||||
|
### AppendToHead |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AppendToHead() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the end of `<head>`. |
||||
|
|
||||
|
|
||||
|
### PrependToHead |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.PrependToHead() |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` at the beginning of `<head>`. |
||||
|
|
||||
|
|
||||
|
### AfterElement |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.AfterElement(target: HTMLElement) |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` after (as a sibling to) the `target`. |
||||
|
|
||||
|
|
||||
|
### BeforeElement |
||||
|
|
||||
|
```js |
||||
|
DOM_STRATEGY.BeforeElement(target: HTMLElement) |
||||
|
``` |
||||
|
|
||||
|
`insertElement` will place the given `element` before (as a sibling to) the `target`. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
|
- [LazyLoadService](./Lazy-Load-Service.md) |
||||
|
- [LoadingStrategy](./Loading-Strategy.md) |
||||
|
- [ContentStrategy](./Content-Strategy.md) |
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,213 @@ |
|||||
|
# How to Lazy Load Scripts and Styles |
||||
|
|
||||
|
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Load Scripts |
||||
|
|
||||
|
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="libraryLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
libraryLoaded$ = this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**. |
||||
|
|
||||
|
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
||||
|
|
||||
|
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### How to Load Styles |
||||
|
|
||||
|
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position. |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="stylesLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
stylesLoaded$ = this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**. |
||||
|
|
||||
|
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once. |
||||
|
|
||||
|
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Advanced Usage |
||||
|
|
||||
|
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example: |
||||
|
|
||||
|
```js |
||||
|
const domStrategy = DOM_STRATEGY.PrependToHead(); |
||||
|
|
||||
|
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous( |
||||
|
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh', |
||||
|
); |
||||
|
|
||||
|
const loadingStrategy = new StyleLoadingStrategy( |
||||
|
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css', |
||||
|
domStrategy, |
||||
|
crossOriginStrategy, |
||||
|
); |
||||
|
|
||||
|
this.lazyLoad.load(loadingStrategy, 1, 2000); |
||||
|
``` |
||||
|
|
||||
|
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails. |
||||
|
|
||||
|
|
||||
|
A common usecase is **loading multiple scripts and/or styles before using a feature**: |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
import { frokJoin } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
private stylesLoaded$ = forkJoin( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
private scriptsLoaded$ = forkJoin( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available. |
||||
|
|
||||
|
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**. |
||||
|
|
||||
|
|
||||
|
Another frequent usecase is **loading dependent scripts in order**: |
||||
|
|
||||
|
```js |
||||
|
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; |
||||
|
import { concat } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<some-component *ngIf="scriptsLoaded$ | async"></some-component> |
||||
|
` |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
scriptsLoaded$ = concat( |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'), |
||||
|
), |
||||
|
this.lazyLoad.load( |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'), |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
constructor(private lazyLoadService: LazyLoadService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
|
||||
|
|
||||
|
### loaded |
||||
|
|
||||
|
```js |
||||
|
loaded: Set<string> |
||||
|
``` |
||||
|
|
||||
|
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set). |
||||
|
|
||||
|
|
||||
|
|
||||
|
### load |
||||
|
|
||||
|
```js |
||||
|
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event> |
||||
|
``` |
||||
|
|
||||
|
- `strategy` parameter is the primary focus here and is explained above. |
||||
|
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_). |
||||
|
- `retryDelay` defines how much delay there will be between retries (_default: 1000_). |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
File diff suppressed because it is too large
@ -0,0 +1,110 @@ |
|||||
|
# LoadingStrategy |
||||
|
|
||||
|
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work. |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public path: string, |
||||
|
protected domStrategy?: DomStrategy, |
||||
|
protected crossOriginStrategy?: CrossOriginStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute. |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_) |
||||
|
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_) |
||||
|
|
||||
|
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### createElement |
||||
|
|
||||
|
```js |
||||
|
createElement(): HTMLScriptElement | HTMLLinkElement |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`. |
||||
|
|
||||
|
|
||||
|
### createStream |
||||
|
|
||||
|
```js |
||||
|
createStream(): Observable<Event> |
||||
|
``` |
||||
|
|
||||
|
This method creates and returns an observable stream that emits on success and throws on error. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ScriptLoadingStrategy |
||||
|
|
||||
|
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## StyleLoadingStrategy |
||||
|
|
||||
|
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Loading Strategies |
||||
|
|
||||
|
Predefined loading strategies are accessible via `LOADING_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousScriptToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToHead(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependAnonymousScriptToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.PrependAnonymousScriptToHead(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousScriptToBody |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousScriptToBody(src: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendAnonymousStyleToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.AppendAnonymousStyleToHead(href: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### PrependAnonymousStyleToHead |
||||
|
|
||||
|
```js |
||||
|
LOADING_STRATEGY.PrependAnonymousStyleToHead(href: string, integrity?: string) |
||||
|
``` |
||||
|
|
||||
|
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [LazyLoadService](./Lazy-Load-Service.md) |
||||
@ -0,0 +1,200 @@ |
|||||
|
# ProjectionStrategy |
||||
|
|
||||
|
`ProjectionStrategy` is an abstract class exposed by @abp/ng.core package. There are three projection strategies extending it: `ComponentProjectionStrategy`, `RootComponentProjectionStrategy`, and `TemplateProjectionStrategy`. Implementing the same methods and properties, all of these strategies help you define how your content projection will work. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ComponentProjectionStrategy |
||||
|
|
||||
|
`ComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into a container**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `component` is class of the component you would like to project. |
||||
|
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
|
||||
|
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(injector: Injector): ComponentRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method prepares the container, resolves the component, sets its context, and projects it to the container. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## RootComponentProjectionStrategy |
||||
|
|
||||
|
`RootComponentProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a component into the document**, such as appending it to `<body>`. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
private domStrategy?: DomStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `component` is class of the component you would like to project. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
- `domStrategy` is the `DomStrategy` that will be used when inserting component. (_default: AppendToBody_) |
||||
|
|
||||
|
Please refer to [ContextStrategy](./Context-Strategy.md) and [DomStrategy](./Dom-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(injector: Injector): ComponentRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method resolves the component, sets its context, and projects it to the document. It returns a `ComponentRef` instance, which you should keep in order to clear projected components later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## TemplateProjectionStrategy |
||||
|
|
||||
|
`TemplateProjectionStrategy` is a class that extends `ProjectionStrategy`. It lets you **project a template into a container**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
template: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy?: ContextStrategy, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `template` is `TemplateRef` you would like to project. |
||||
|
- `containerStrategy` is the `ContainerStrategy` that will be used when projecting the component. |
||||
|
- `contextStrategy` is the `ContextStrategy` that will be used on the projected component. (_default: None_) |
||||
|
|
||||
|
Please refer to [ContainerStrategy](./Container-Strategy.md) and [ContextStrategy](./Context-Strategy.md) documentation for their usage. |
||||
|
|
||||
|
|
||||
|
### injectContent |
||||
|
|
||||
|
```js |
||||
|
injectContent(): EmbeddedViewRef<T> |
||||
|
``` |
||||
|
|
||||
|
This method prepares the container, and projects the template together with the defined context to it. It returns an `EmbeddedViewRef`, which you should keep in order to clear projected templates later on. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Projection Strategies |
||||
|
|
||||
|
Predefined projection strategies are accessible via `PROJECTION_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### AppendComponentToBody |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendComponentToBody( |
||||
|
component: T, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **end** of `<body>` tag in the document. |
||||
|
|
||||
|
|
||||
|
### AppendComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **end** of the container. |
||||
|
|
||||
|
|
||||
|
### AppendTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.AppendTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the template and places it at the **end** of the container. |
||||
|
|
||||
|
|
||||
|
### PrependComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.PrependComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the component and places it at the **beginning** of the container. |
||||
|
|
||||
|
|
||||
|
### PrependTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.PrependTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Sets given context to the template and places it at the **beginning** of the container. |
||||
|
|
||||
|
|
||||
|
### ProjectComponentToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.ProjectComponentToContainer( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Clears the container, sets given context to the component, and places it **in the cleared** the container. |
||||
|
|
||||
|
|
||||
|
### ProjectTemplateToContainer |
||||
|
|
||||
|
```js |
||||
|
PROJECTION_STRATEGY.ProjectTemplateToContainer( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
contextStrategy?: ComponentContextStrategy<T>, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Clears the container, sets given context to the template, and places it **in the cleared** the container. |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [DomInsertionService](./Dom-Insertion-Service.md) |
||||
@ -0,0 +1,42 @@ |
|||||
|
# Popovers |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
`abp-popover` is the abp tag for popover messages. |
||||
|
|
||||
|
Basic usage: |
||||
|
|
||||
|
````xml |
||||
|
<abp-button abp-popover="Hi, i'm popover content!"> |
||||
|
Popover Default |
||||
|
</abp-button> |
||||
|
```` |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Demo |
||||
|
|
||||
|
See the [popovers demo page](https://bootstrap-taghelpers.abp.io/Components/Popovers) to see it in action. |
||||
|
|
||||
|
## Attributes |
||||
|
|
||||
|
### disabled |
||||
|
|
||||
|
A value indicates if the element should be disabled for interaction. If this value is set to `true`, `dismissable` attribute will be ignored. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
|
|
||||
|
### dismissable |
||||
|
|
||||
|
A value indicates to dismiss the popovers on the user's next click of a different element than the toggle element. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
|
|
||||
|
### hoverable |
||||
|
|
||||
|
A value indicates if the popover content will be displayed on mouse hover. Should be one of the following values: |
||||
|
|
||||
|
* `false` (default value) |
||||
|
* `true` |
||||
File diff suppressed because it is too large
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 88 KiB |
@ -1,3 +0,0 @@ |
|||||
## AutoMapper Integration |
|
||||
|
|
||||
TODO |
|
||||
@ -0,0 +1,3 @@ |
|||||
|
## Getting Started With the Angular Application Template |
||||
|
|
||||
|
TODO... |
||||
@ -0,0 +1,6 @@ |
|||||
|
# 启动模板入门 |
||||
|
|
||||
|
参阅下面的教程来学习如何开始使用的ABP框架预构建的应用程序启动模板: |
||||
|
|
||||
|
* [ASP.NET Core MVC/Razor页面模板入门](Getting-Started-AspNetCore-MVC-Template.md) |
||||
|
* [Angular UI模板入门](Getting-Started-Angular-Template.md) |
||||
@ -0,0 +1,198 @@ |
|||||
|
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 |
||||
|
|
||||
|
本文介绍了如何将AzureAD集成到ABP应用程序中,用 **Azure Active Directory** 凭据使用 OAuth 2.0 登录. |
||||
|
|
||||
|
添加Azure Active Directory到ABP框架非常简单,只需要正确的完成几个配置. |
||||
|
|
||||
|
为了覆盖更多范围,我们演示两种不同的集成AzureAD的**方法**. |
||||
|
|
||||
|
1. **AddAzureAD**: 该方法使用微软[AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),在网络上搜索如何将AzureAD集成到应用程序时,这个包是最流行的. |
||||
|
|
||||
|
2. **AddOpenIdConnect**: 该方法使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/). 它不仅可用于AzureAD,还可用于所有OpenId连接. |
||||
|
|
||||
|
> 这些方法之间的功能**没有区别**,AddAzureAD是具有预定义Cookie设置的OpenIdConnection([源](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122))的抽象方法. |
||||
|
> |
||||
|
> 但是默认配置的登录方案在与ABP应用程序集成方面存在关键差异,下面将对此进行说明. |
||||
|
|
||||
|
## 1. AddAzureAD |
||||
|
|
||||
|
这个方法使用 [Microsoft AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),它是最常用的集成AzureAD方法. |
||||
|
|
||||
|
如果选择这种方法,需要将 `Microsoft.AspNetCore.Authentication.AzureAD.UI` 软件包安装到 **.Web** 项目中. 由于AddAzureAD扩展使用[配置绑定](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration),你需要更改 **.Web** 项目中的appsettings.json文件. |
||||
|
|
||||
|
#### **更改 `appsettings.json`** |
||||
|
|
||||
|
你添加向 `appsettings.json` 添加新的配置节,在配置 `OpenIdConnectOptions` 时绑定配置: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> 这里重要的配置是CallbackPath. 值必须与你的 Azure AD-> app registrations-> Authentication -> RedirectUri 之一相同. |
||||
|
|
||||
|
然后你需要配置 `OpenIdConnectOptions` 完成集成. |
||||
|
|
||||
|
#### 配置 OpenIdConnectOptions |
||||
|
|
||||
|
在你的 **.Web** 项目找到 **ApplicationWebModule** 使用以下代码修改 `ConfigureAuthentication` 方法: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "Acme.BookStore"; |
||||
|
}) |
||||
|
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
||||
|
|
||||
|
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
||||
|
{ |
||||
|
options.Authority = options.Authority + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
|
||||
|
options.TokenValidationParameters.ValidateIssuer = false; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
options.SaveTokens = true; |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> **不要忘记:** |
||||
|
> |
||||
|
> * 在 `AddAuthentication()` 之后添加 `.AddAzureAD(options => configuration.Bind("AzureAd", options))` . 它绑定了你的 AzureAD 配置并且容易忘记. |
||||
|
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. 它会禁用默认的 Microsoft claim type 映射. |
||||
|
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. 映射 [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) 很重要,因为默认SignIn Manager和行为使用这个claim type用于外部登录信息. |
||||
|
> * 添加 `options.SignInScheme = IdentityConstants.ExternalScheme` 因为 [默认登录方法为 `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
||||
|
> * 如果你使用的是 **v2.0** 端点,应添加 `options.Scope.Add("email")` 因为 v2.0 端点不会将 `email` 做为默认值返回. [账户模块](../Modules/Account.md) 使用 `email` claim 来 [注册外部账户](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
||||
|
|
||||
|
你已经完成了集成. |
||||
|
|
||||
|
## 2. 替代方法: AddOpenIdConnect |
||||
|
|
||||
|
如果你不想在应用程序安装一个额外的NuGet包,你可以使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/),它适用于所有的OpenId连接,包括AzureAD外部认证. |
||||
|
|
||||
|
你不必使用 `appsettings.json` 配置, 但将AzureAD信息放在 `appsettings.json` 是一个很好的做法. |
||||
|
|
||||
|
为了从 `appsettings.json` 获取AzureAD信息在 `OpenIdConnectOptions` 配置使用,只需要在你的 **.Web** 项目中的 `appsettings.json` 添加一个新的配置节: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
然后在你的 **.Web** 项目的 **ApplicationWebModule** 用以下代码修改 `ConfigureAuthentication` 方法: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
|
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "BookStore"; |
||||
|
}) |
||||
|
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
||||
|
{ |
||||
|
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.SaveTokens = true; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
集成结束. 请记住你可以连接任何其他外部认证供应商. |
||||
|
|
||||
|
## 本文的源代码 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
||||
|
|
||||
|
# FAQ |
||||
|
|
||||
|
* Help! `GetExternalLoginInfoAsync` 返回 `null`! |
||||
|
|
||||
|
* 有两方面的原因; |
||||
|
|
||||
|
1. 你在尝试验证错误的方案. 检查是否设置 **SignInScheme** 为 `IdentityConstants.ExternalScheme`: |
||||
|
|
||||
|
````csharp |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
```` |
||||
|
|
||||
|
2. 你的 `ClaimTypes.NameIdentifier` 为 `null`. 检查是否添加 claim 映射: |
||||
|
|
||||
|
````csharp |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
```` |
||||
|
|
||||
|
* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! |
||||
|
|
||||
|
* 如果你在appsettings设置 **CallbackPath** 为: |
||||
|
|
||||
|
````csharp |
||||
|
"AzureAd": { |
||||
|
... |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的<u>域</u>, 而不仅是 `/signin-azuread-oidc`. |
||||
|
|
||||
|
* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! |
||||
|
|
||||
|
* 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 |
||||
|
|
||||
|
````csharp |
||||
|
options.Scope.Add("email"); |
||||
|
```` |
||||
|
|
||||
|
到你的 openid 配置. |
||||
|
|
||||
|
* 如何**调试/监视**在映射之前获得的声明? |
||||
|
|
||||
|
* 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如: |
||||
|
|
||||
|
````csharp |
||||
|
options.Events.OnTokenValidated = (async context => |
||||
|
{ |
||||
|
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
||||
|
await Task.CompletedTask; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
||||
|
* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md). |
||||
@ -0,0 +1,113 @@ |
|||||
|
# 如何为MVC / Razor页面应用程序自定义登录页面 |
||||
|
|
||||
|
当你使用[应用程序启动模板](../Startup-Templates/Application.md)创建了一个新的应用程序, 登录页面的源代码并不在你的解决方案中,所以你不能直接更改. 它来自[账户模块](../Modules/Account.md),使用[NuGet包](https://www.nuget.org/packages/Volo.Abp.Account.Web)引用. |
||||
|
|
||||
|
本文介绍了如何为自己的应用程序自定义登录页面. |
||||
|
|
||||
|
## 创建登录 PageModel |
||||
|
|
||||
|
创建一个新的类继承账户模块的[LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs). |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomLoginModel : LoginModel |
||||
|
{ |
||||
|
public CustomLoginModel( |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider, |
||||
|
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions) |
||||
|
: base(schemeProvider, accountOptions) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> 在这里命令约定很重要. 如果你的类名不是以 `LoginModel` 结束,你需要手动在[依赖注入](../Dependency-Injection.md)系统替换 `LoginModel`. |
||||
|
|
||||
|
然后你可以覆盖任何方法并添加用户界面需要的新方法和属性. |
||||
|
|
||||
|
## 重写登录页面UI |
||||
|
|
||||
|
在 **Pages** 目录下创建名为 **Account** 的文件夹,并在这个文件夹中创建 `Login.cshtml` ,借助[虚拟文件系统](../Virtual-File-System.md)它会自动覆盖账户模块的页面文件. |
||||
|
|
||||
|
自定义页面一个很好的开始是复制它的源代码. [点击这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到登录页面的源码. 在编写本文档时,源代码如下: |
||||
|
|
||||
|
````xml |
||||
|
@page |
||||
|
@using Volo.Abp.Account.Settings |
||||
|
@using Volo.Abp.Settings |
||||
|
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel |
||||
|
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
||||
|
@inject Volo.Abp.Settings.ISettingProvider SettingProvider |
||||
|
@if (Model.EnableLocalLogin) |
||||
|
{ |
||||
|
<div class="card mt-3 shadow-sm rounded"> |
||||
|
<div class="card-body p-5"> |
||||
|
<h4>@L["Login"]</h4> |
||||
|
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) |
||||
|
{ |
||||
|
<strong> |
||||
|
@L["AreYouANewUser"] |
||||
|
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a> |
||||
|
</strong> |
||||
|
} |
||||
|
<form method="post" class="mt-4"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.UserNameOrEmailAddress"></label> |
||||
|
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label asp-for="LoginInput.Password"></label> |
||||
|
<input asp-for="LoginInput.Password" class="form-control" /> |
||||
|
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> |
||||
|
</div> |
||||
|
<div class="form-check"> |
||||
|
<label asp-for="LoginInput.RememberMe" class="form-check-label"> |
||||
|
<input asp-for="LoginInput.RememberMe" class="form-check-input" /> |
||||
|
@Html.DisplayNameFor(m => m.LoginInput.RememberMe) |
||||
|
</label> |
||||
|
</div> |
||||
|
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<div class="card-footer text-center border-0"> |
||||
|
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@ |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="col-md-6"> |
||||
|
<h4>@L["UseAnotherServiceToLogIn"]</h4> |
||||
|
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post"> |
||||
|
<input asp-for="ReturnUrl" /> |
||||
|
<input asp-for="ReturnUrlHash" /> |
||||
|
@foreach (var provider in Model.VisibleExternalProviders) |
||||
|
{ |
||||
|
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button> |
||||
|
} |
||||
|
</form> |
||||
|
</div> |
||||
|
} |
||||
|
|
||||
|
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) |
||||
|
{ |
||||
|
<div class="alert alert-warning"> |
||||
|
<strong>@L["InvalidLoginRequest"]</strong> |
||||
|
@L["ThereAreNoLoginSchemesConfiguredForThisClient"] |
||||
|
</div> |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
只需更改 `@model` 为 `Acme.BookStore.Web.Pages.Account.CustomLoginModel` 使用自定义的 `PageModel` 类. 你可以做任何应用程序需要的更改. |
||||
|
|
||||
|
## 本文的源代码 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
* [ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南](../UI/AspNetCore/Customization-User-Interface.md). |
||||
@ -0,0 +1,101 @@ |
|||||
|
# 如何为ABP应用程序定制SignIn Manager |
||||
|
|
||||
|
在使用[应用程序启动模板](../Startup-Templates/Application.md)创建新项目后,你可能想要扩展或更改SignIn Manager的默认行为,以满足你需要的身份验证和注册流程. ABP[账户模块](../Modules/Account.md)使用[身份管理模块](../Modules/Identity.md)做为SignIn Manager,而[身份管理模块](../Modules/Identity.md)使用默认的[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)([参阅此处]((https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17))). |
||||
|
|
||||
|
编写自定义SignIn Manager,你需要扩展[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)类并注入到DI容器. |
||||
|
|
||||
|
本文介绍了如何为你自己的应用程序自定义SignIn Manager. |
||||
|
|
||||
|
## 创建 CustomSignInManager |
||||
|
|
||||
|
创建一个类并继承自Microsoft Identity 包的 [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs). |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
||||
|
{ |
||||
|
public CustomSignInManager( |
||||
|
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
||||
|
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
||||
|
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
||||
|
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
||||
|
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
||||
|
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
||||
|
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> 重点是使用**Volo.Abp.Identity.IdentityUser**做为泛型参数,而不是应用程序的AppUser. |
||||
|
|
||||
|
然后你可以覆盖SignIn Manager的任何方法并且为你的身份验证和注册流程添加需要的方法和属性. |
||||
|
|
||||
|
## 重写 GetExternalLoginInfoAsync 方法 |
||||
|
|
||||
|
在这个用例中我们重写第三方身份验证时使用的 `GetExternalLoginInfoAsync` 方法实现. |
||||
|
|
||||
|
一个好的开始是从复制[源码](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674)而不是从零开始. 在这个用例中我们对源码进行较少的修改,为了帮助理解概念它显式显示了方法和属性的命名空间. |
||||
|
|
||||
|
````csharp |
||||
|
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
||||
|
{ |
||||
|
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
||||
|
var items = auth?.Properties?.Items; |
||||
|
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (expectedXsrf != null) |
||||
|
{ |
||||
|
if (!items.ContainsKey("XsrfKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
var userId = items[XsrfKey] as string; |
||||
|
if (userId != expectedXsrf) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
||||
|
var provider = items[LoginProviderKey] as string; |
||||
|
if (providerKey == null || provider == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
||||
|
?? provider; |
||||
|
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
||||
|
{ |
||||
|
AuthenticationTokens = auth.Properties.GetTokens() |
||||
|
}; |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
要使你自定义的SignIn Manager类生效,你需要将其注册[依赖注入系统](../Dependency-Injection.md)中. |
||||
|
|
||||
|
## 注册到依赖注入 |
||||
|
|
||||
|
应该使用 [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs) 的 [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) 类的 **AddSignInManager** 扩展方法注册 `CustomSignInManager`. |
||||
|
|
||||
|
在你的 `.Web` 项目找到 `YourProjectNameWebModule` 的 `PreConfigureServices` 方法添加以下代码替换老的 `SignInManager`: |
||||
|
|
||||
|
````csharp |
||||
|
PreConfigure<IdentityBuilder>(identityBuilder => |
||||
|
{ |
||||
|
identityBuilder.AddSignInManager<CustomSignInManager>(); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## 本文的源代码 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
||||
|
* [身份管理模块](../Modules/Identity.md). |
||||
@ -0,0 +1,9 @@ |
|||||
|
# "如何" 指南 |
||||
|
|
||||
|
本部分包含一些常见问题的 "如何" 指南. 尽管其中是一些常见的开发任务和ABP并不直接相关,但我们认为有一些具体的示例可以直接与基于ABP的应用程序一起使用. |
||||
|
|
||||
|
## Authentication |
||||
|
|
||||
|
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md) |
||||
|
* [如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证](Azure-Active-Directory-Authentication-MVC.md) |
||||
|
* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md) |
||||
@ -0,0 +1,267 @@ |
|||||
|
# 对象扩展 |
||||
|
|
||||
|
ABP框架提供了 **实体扩展系统** 允许你 **添加额外属性** 到已存在的对象 **无需修改相关类**. 它允许你扩展[应用程序依赖模块](Modules/Index.md)实现的功能,尤其是当你要扩展[模块定义的实体](Customizing-Application-Modules-Extending-Entities.md)和[DTO](Customizing-Application-Modules-Overriding-Services.md)时. |
||||
|
|
||||
|
> 你自己的对象通常不需要对象扩展系统,因为你可以轻松的添加常规属性到你的类中. |
||||
|
|
||||
|
## IHasExtraProperties 接口 |
||||
|
|
||||
|
这是一个使类可扩展的接口. 它定义了 `Dictionary` 属性: |
||||
|
|
||||
|
````csharp |
||||
|
Dictionary<string, object> ExtraProperties { get; } |
||||
|
```` |
||||
|
|
||||
|
然后你可以使用此字典添加或获取其他属性. |
||||
|
|
||||
|
### 基类 |
||||
|
|
||||
|
默认以下基类实现了 `IHasExtraProperties` 接口: |
||||
|
|
||||
|
* 由 `AggregateRoot` 类实现 (参阅 [entities](Entities.md)). |
||||
|
* 由 `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... [DTO](Data-Transfer-Objects.md)基类实现. |
||||
|
* 由 `ExtensibleObject` 实现, 它是一个简单的基类,任何类型的对象都可以继承. |
||||
|
|
||||
|
如果你的类从这些类继承,那么你的类也是可扩展的,如果没有,你也可以随时手动继承. |
||||
|
|
||||
|
### 基本扩展方法 |
||||
|
|
||||
|
虽然可以直接使用类的 `ExtraProperties` 属性,但建议使用以下扩展方法使用额外属性. |
||||
|
|
||||
|
#### SetProperty |
||||
|
|
||||
|
用于设置额外属性值: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title"); |
||||
|
user.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
`SetProperty` 返回相同的对象, 你可以使用链式编程: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetProperty("Title", "My Title") |
||||
|
.SetProperty("IsSuperUser", true); |
||||
|
```` |
||||
|
|
||||
|
#### GetProperty |
||||
|
|
||||
|
用于读取额外属性的值: |
||||
|
|
||||
|
````csharp |
||||
|
var title = user.GetProperty<string>("Title"); |
||||
|
|
||||
|
if (user.GetProperty<bool>("IsSuperUser")) |
||||
|
{ |
||||
|
//... |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* `GetProperty` 是一个泛型方法,对象类型做为泛型参数. |
||||
|
* 如果未设置给定的属性,则返回默认值 (`int` 的默认值为 `0` , `bool` 的默认值是 `false` ... 等). |
||||
|
|
||||
|
##### 非基本属性类型 |
||||
|
|
||||
|
如果您的属性类型不是原始类型(int,bool,枚举,字符串等),你需要使用 `GetProperty` 的非泛型版本,它会返回 `object`. |
||||
|
|
||||
|
#### HasProperty |
||||
|
|
||||
|
用于检查对象之前是否设置了属性. |
||||
|
|
||||
|
#### RemoveProperty |
||||
|
|
||||
|
用于从对象中删除属性. 使用此方法代替为属性设置 `null` 值. |
||||
|
|
||||
|
### 一些最佳实践 |
||||
|
|
||||
|
为属性名称使用魔术字符串很危险,因为你很容易输入错误的属性名称-这并不安全; |
||||
|
|
||||
|
* 为你的额外属性名称定义一个常量. |
||||
|
* 使用扩展方法轻松设置你的属性. |
||||
|
|
||||
|
示例: |
||||
|
|
||||
|
````csharp |
||||
|
public static class IdentityUserExtensions |
||||
|
{ |
||||
|
private const string TitlePropertyName = "Title"; |
||||
|
|
||||
|
public static void SetTitle(this IdentityUser user, string title) |
||||
|
{ |
||||
|
user.SetProperty(TitlePropertyName, title); |
||||
|
} |
||||
|
|
||||
|
public static string GetTitle(this IdentityUser user) |
||||
|
{ |
||||
|
return user.GetProperty<string>(TitlePropertyName); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
然后, 你可以很容易地设置或获取 `Title` 属性: |
||||
|
|
||||
|
````csharp |
||||
|
user.SetTitle("My Title"); |
||||
|
var title = user.GetTitle(); |
||||
|
```` |
||||
|
|
||||
|
## Object Extension Manager |
||||
|
|
||||
|
你可以为可扩展对象(实现 `IHasExtraProperties`接口)设置任意属性, `ObjectExtensionManager` 用于显式定义可扩展类的其他属性. |
||||
|
|
||||
|
显式定义额外的属性有一些用例: |
||||
|
|
||||
|
* 允许控制如何在对象到对象的映射上处理额外的属性 (参阅下面的部分). |
||||
|
* 允许定义属性的元数据. 例如你可以在使用[EF Core](Entity-Framework-Core.md)时将额外的属性映射到数据库中的表字段. |
||||
|
|
||||
|
> `ObjectExtensionManager` 实现单例模式 (`ObjectExtensionManager.Instance`) ,你应该在应用程序启动之前定义对象扩展. [应用程序启动模板](Startup-Templates/Application.md) 有一些预定义的静态类,可以安全在内部定义对象扩展. |
||||
|
|
||||
|
### AddOrUpdate |
||||
|
|
||||
|
`AddOrUpdate` 是定义对象额外属性或更新对象额外属性的主要方法. |
||||
|
|
||||
|
示例: 为 `IdentityUser` 实体定义额外属性: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdate<IdentityUser>(options => |
||||
|
{ |
||||
|
options.AddOrUpdateProperty<string>("SocialSecurityNumber"); |
||||
|
options.AddOrUpdateProperty<bool>("IsSuperUser"); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
### AddOrUpdateProperty |
||||
|
|
||||
|
虽然可以如上所示使用 `AddOrUpdateProperty`, 但如果要定义单个额外的属性,也可以使用快捷的扩展方法: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber"); |
||||
|
```` |
||||
|
|
||||
|
有时将单个额外属性定义为多种类型是可行的. 你可以使用以下代码,而不是一个一个地定义: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<string>( |
||||
|
new[] |
||||
|
{ |
||||
|
typeof(IdentityUserDto), |
||||
|
typeof(IdentityUserCreateDto), |
||||
|
typeof(IdentityUserUpdateDto) |
||||
|
}, |
||||
|
"SocialSecurityNumber" |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
#### 属性配置 |
||||
|
|
||||
|
`AddOrUpdateProperty` 还可以为属性定义执行其他配置的操作. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> 参阅 "对象到对象映射" 部分了解 `CheckPairDefinitionOnMapping` 选项. |
||||
|
|
||||
|
`options` 有一个名为 `Configuration` 的字典,该字典存储对象扩展定义甚至可以扩展. EF Core使用它来将其他属性映射到数据库中的表字段. 请参阅[扩展实体文档](Customizing-Application-Modules-Extending-Entities.md). |
||||
|
|
||||
|
## 对象到对象映射 |
||||
|
|
||||
|
假设你已向可扩展的实体对象添加了额外的属性并使用了自动[对象到对象的映射](Object-To-Object-Mapping.md)将该实体映射到可扩展的DTO类. 在这种情况下你需要格外小心,因为额外属性可能包含**敏感数据**,这些数据对于客户端不可用. |
||||
|
|
||||
|
本节提供了一些**好的做法**,可以控制对象映射的额外属性。 |
||||
|
|
||||
|
### MapExtraPropertiesTo |
||||
|
|
||||
|
`MapExtraPropertiesTo` 是ABP框架提供的扩展方法,用于以受控方式将额外的属性从一个对象复制到另一个对象. 示例: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo(identityUserDto); |
||||
|
```` |
||||
|
|
||||
|
`MapExtraPropertiesTo` 需要在**两侧**(本例中是`IdentityUser` 和 `IdentityUserDto`)**定义属性**. 以将值复制到目标对象. 否则即使源对象(在此示例中为 `identityUser` )中确实存在该值,它也不会复制. 有一些重载此限制的方法. |
||||
|
|
||||
|
#### MappingPropertyDefinitionChecks |
||||
|
|
||||
|
`MapExtraPropertiesTo` 获取一个附加参数来控制单个映射操作的定义检查: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
MappingPropertyDefinitionChecks.None |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
> 要小心,因为 `MappingPropertyDefinitionChecks.None` 会复制所有的额外属性而不进行任何检查. `MappingPropertyDefinitionChecks` 枚举还有其他成员. |
||||
|
|
||||
|
如果要完全禁用属性的定义检查,可以在定义额外的属性(或更新现有定义)时进行,如下所示: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
#### 忽略属性 |
||||
|
|
||||
|
你可能要在映射操作忽略某些属性: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
ignoredProperties: new[] {"MySensitiveProp"} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
忽略的属性不会复制到目标对象. |
||||
|
|
||||
|
#### AutoMapper集成 |
||||
|
|
||||
|
如果您使用的是[AutoMapper](https://automapper.org/)库,ABP框架还提供了一种扩展方法来利用上面定义的 `MapExtraPropertiesTo` 方法. |
||||
|
|
||||
|
你可以在映射配置文件中使用 `MapExtraProperties()` 方法. |
||||
|
|
||||
|
````csharp |
||||
|
public class MyProfile : Profile |
||||
|
{ |
||||
|
public MyProfile() |
||||
|
{ |
||||
|
CreateMap<IdentityUser, IdentityUserDto>() |
||||
|
.MapExtraProperties(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
它与 `MapExtraPropertiesTo()` 方法具有相同的参数。 |
||||
|
|
||||
|
## Entity Framework Core 数据库映射 |
||||
|
|
||||
|
如果你使用的是EF Core,可以将额外的属性映射到数据库中的表字段. 例: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.MapEfCore(b => b.HasMaxLength(32)); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
参阅 [Entity Framework Core 集成文档](Entity-Framework-Core.md) 了解更多内容. |
||||
@ -1,3 +1,469 @@ |
|||||
# ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南 |
# ASP.NET Core (MVC / Razor Pages) 用户界面自定义指南 |
||||
|
|
||||
TODO... |
本文档解释了如何重写ASP.NET Core MVC / Razor Page 应用程序依赖[应用模块](../../Modules/Index.md)的用户界面. |
||||
|
|
||||
|
## 重写页面 |
||||
|
|
||||
|
本节介绍了[Razor 页面](https://docs.microsoft.com/zh-cn/aspnet/core/razor-pages/)开发,它是ASP.NET Core推荐的服务端渲染用户页面的方法. 预构建的模块通常使用Razor页面替代经典的MVC方式(下一节也介绍MVC模式). |
||||
|
|
||||
|
你通过有三种重写页面的需求: |
||||
|
|
||||
|
* 仅**重写页面模型**(C#)端执行其他逻辑,不更改UI. |
||||
|
* 仅**重写Razor页面**(.cshtml文件),不更改逻辑. |
||||
|
* **完全重写** 页面. |
||||
|
|
||||
|
### 重写页面模型 (C#) |
||||
|
|
||||
|
````csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.Identity.Web.Pages.Identity.Users; |
||||
|
|
||||
|
namespace Acme.BookStore.Web.Pages.Identity.Users |
||||
|
{ |
||||
|
[Dependency(ReplaceServices = true)] |
||||
|
[ExposeServices(typeof(EditModalModel))] |
||||
|
public class MyEditModalModel : EditModalModel |
||||
|
{ |
||||
|
public MyEditModalModel( |
||||
|
IIdentityUserAppService identityUserAppService, |
||||
|
IIdentityRoleAppService identityRoleAppService |
||||
|
) : base( |
||||
|
identityUserAppService, |
||||
|
identityRoleAppService) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public override async Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
//TODO: Additional logic |
||||
|
await base.OnPostAsync(); |
||||
|
//TODO: Additional logic |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
* 这个类继承并替换 `EditModalModel` ,重写了 `OnPostAsync` 方法在基类代码的前后执行附加逻辑 |
||||
|
* 它使用 `ExposeServices` 和 `Dependency` attributes去替换这个类. |
||||
|
|
||||
|
### 重写Razor页面 (.CSHTML) |
||||
|
|
||||
|
使用[虚拟文件系统](../../Virtual-File-System.md)可以重写 `.cshtml` 文件(razor page, razor view, view component... 等.) |
||||
|
|
||||
|
虚拟文件系统允许我们将**资源嵌入到程序集中**. 通过这个方式,预构建的模块在Nuget包中定义了Razor页面. 当你依赖模块时,可以覆盖这个模块向虚拟文件系统添加的任何文件,包括页面/视图. |
||||
|
|
||||
|
#### 示例 |
||||
|
|
||||
|
这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面**UI |
||||
|
|
||||
|
物理文件可以覆盖相同位置的嵌入文件. 账户模块在 `Pages/Account` 文件夹下定义了 `Login.cshtml` 文件. 所以你可以在同一路径下创建文件覆盖它: |
||||
|
 |
||||
|
|
||||
|
通常你想要拷贝模块的 `.cshtml` 原文件,然后进行需要的更改. 你可以在[这里](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml)找到源文件. 不要拷贝 `Login.cshtml.cs` 文件,它是隐藏razor页面的代码,我们不希望覆盖它(见下节). |
||||
|
|
||||
|
这就够了,接下来你可以对文件内容做你想要的更改. |
||||
|
|
||||
|
### 完全重写Razo页面 |
||||
|
|
||||
|
也许你想要完全重写页面,Razor和页面相关的C#文件. |
||||
|
|
||||
|
在这种情况下; |
||||
|
|
||||
|
1. 像上面描述过的那术重写C#页面模型类,但不需要替换已存在的页面模型类. |
||||
|
2. 像上面描述过的那样重写Razor页面,并且更改@model指向新的页面模型 |
||||
|
|
||||
|
#### 示例 |
||||
|
|
||||
|
这个示例重写了[账户模块](../../Modules/Account.md)定义的**登录页面** |
||||
|
|
||||
|
创建一个继承自 `LoginModel`(定义在`Volo.Abp.Account.Web.Pages.Account`命名空间下)的页面模型类: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyLoginModel : LoginModel |
||||
|
{ |
||||
|
public MyLoginModel( |
||||
|
IAuthenticationSchemeProvider schemeProvider, |
||||
|
IOptions<AbpAccountOptions> accountOptions |
||||
|
) : base( |
||||
|
schemeProvider, |
||||
|
accountOptions) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public override Task<IActionResult> OnPostAsync(string action) |
||||
|
{ |
||||
|
//TODO: Add logic |
||||
|
return base.OnPostAsync(action); |
||||
|
} |
||||
|
|
||||
|
//TODO: Add new methods and properties... |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
如果需要,你可以重写任何方法或添加新的属性/方法 |
||||
|
|
||||
|
> 注意我们没有使用 `[Dependency(ReplaceServices = true)]` 或 `[ExposeServices(typeof(LoginModel))]`,因为我们不想替换依赖注入中已存在的类,我们定义了一个新的. |
||||
|
|
||||
|
拷贝 `Login.cshtml` 到你们解决方案,更改 **@model** 指定到 `MyLoginModel`: |
||||
|
|
||||
|
````xml |
||||
|
@page |
||||
|
... |
||||
|
@model Acme.BookStore.Web.Pages.Account.MyLoginModel |
||||
|
... |
||||
|
```` |
||||
|
|
||||
|
这就够了,接下来你可以做任何想要更改. |
||||
|
|
||||
|
#### 不使用继承替换页面模型 |
||||
|
|
||||
|
你不需要继承源页面模型类(像之前的示例). 你可以完全**重写实现**你自己的页面. 在这种事情下你可以从 `PageModel`,`AbpPageModel` 或任何你需要的合适的基类派生. |
||||
|
|
||||
|
## 重写视图组件 |
||||
|
|
||||
|
在ABP框架,预构建的模块和主题定义了一些**可重用的视图组件**. 这些视图组件可以像页面一样被替换. |
||||
|
|
||||
|
### 示例 |
||||
|
|
||||
|
下面是应用程序启动模板自带的 **基本主题** 的截图. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
[基本主题](../../Themes/Basic.md) 为layout定义了一些视图组件. 例如上面带有红色矩形的突出显示区域称为 **Brand组件**, 你可能想添加自己的**自己的应用程序logo**来自定义此组件. 让我们来看看如何去做. |
||||
|
|
||||
|
首先创建你的logo并且放到你的web应用程序文件夹中,我们使用 `wwwroot/logos/bookstore-logo.png` 路径. 然后在 `Themes/Basic/Components/Brand` 文件夹下复制[Brand组件视图](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/Default.cshtml). 结果应该是类似下面的图片: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
然后对 `Default.cshtml` 文件做你想要的更改. 例如内容可以是这样的: |
||||
|
|
||||
|
````xml |
||||
|
<a href="/"> |
||||
|
<img src="~/logos/bookstore-logo.png" width="250" height="60"/> |
||||
|
</a> |
||||
|
```` |
||||
|
|
||||
|
现在你可以运行应用程序看到结果: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
如果你需要,你也可以仅使用依赖注入系统替换组件[背后的C#类代码](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Brand/MainNavbarBrandViewComponent.cs) |
||||
|
|
||||
|
### 重写主题 |
||||
|
|
||||
|
正如上所解释的,你可以更改任何组件,layout或c#类. 参阅[主题文档]了解更多关于主题系统的信息. |
||||
|
|
||||
|
## 重写静态资源 |
||||
|
|
||||
|
重写模块的静态资源(像JavaScript,Css或图片文件)是很简单的. 只需要在解决方案的相同路径创建文件,虚拟文件系统会自动处理它. |
||||
|
|
||||
|
## 操作捆绑 |
||||
|
|
||||
|
[捆绑 & 压缩](Bundling-Minification.md) 系统提供了**动态可扩展的** 系统去创建**script**和**style**捆绑. 它允许你扩展和操作现有的包. |
||||
|
|
||||
|
### 示例: 添加全局CSS文件 |
||||
|
|
||||
|
例如APP框架定义了一个**全局样式捆绑**添加到所有的页面(事实上由主题添加layout). 让我们添加一个**自定义样式文件**到这个捆绑文件的最后,我们可以覆盖任何全局样式. |
||||
|
|
||||
|
创建在 `wwwroot` 文件夹下创建一个CSS文件 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
在CSS文件中定义一些规则. 例如: |
||||
|
|
||||
|
````css |
||||
|
.card-title { |
||||
|
color: orange; |
||||
|
font-size: 2em; |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
.btn-primary { |
||||
|
background-color: red; |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
然后在你的[模块](../../Module-Development-Basics.md) `ConfigureServices` 方法添加这个文件到标准的全局样式捆绑包: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpBundlingOptions>(options => |
||||
|
{ |
||||
|
options.StyleBundles.Configure( |
||||
|
StandardBundles.Styles.Global, //The bundle name! |
||||
|
bundleConfiguration => |
||||
|
{ |
||||
|
bundleConfiguration.AddFiles("/styles/my-global-styles.css"); |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
#### 全局脚本捆绑包 |
||||
|
|
||||
|
就像 `StandardBundles.Styles.Global` 一样,还有一个 `StandardBundles.Scripts.Global`,你可以添加文件或操作现有文件. |
||||
|
|
||||
|
### 示例: 操作捆绑包文件 |
||||
|
|
||||
|
上面的示例中添加了新文件到捆绑包. 如果你创建 **bundle contributor** 类则可以做到更多. 示例: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyGlobalStyleBundleContributor : BundleContributor |
||||
|
{ |
||||
|
public override void ConfigureBundle(BundleConfigurationContext context) |
||||
|
{ |
||||
|
context.Files.Clear(); |
||||
|
context.Files.Add("/styles/my-global-styles.css"); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
然后你可以添加这个contributor到已存在的捆绑中: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpBundlingOptions>(options => |
||||
|
{ |
||||
|
options.StyleBundles.Configure( |
||||
|
StandardBundles.Styles.Global, |
||||
|
bundleConfiguration => |
||||
|
{ |
||||
|
bundleConfiguration.AddContributors(typeof(MyGlobalStyleBundleContributor)); |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
示例中清除了所有的CSS文件,在现实中这并不是一个好主意,你可以找到某个特定的文件替换成你自己的文件. |
||||
|
|
||||
|
### 示例: 为特定页面添加JavaScript文件 |
||||
|
|
||||
|
上面的示例将全局包添加到布局中. 如果要在依赖模块中为特定页面定义添加CSS/JavaScript文件(或替换文件)怎么做? |
||||
|
|
||||
|
假设你想要用户进入身份模块的**角色管理**页面时运行**JavaScript代码**. |
||||
|
|
||||
|
首先在 `wwwroot`, `Pages` 或 `Views` 文件夹下创建一个标准的JavaScript文件(默认ABP支持这些文件夹下的静态文件). 根据约定我们推荐 `Pages/Identity/Roles` 文件夹: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
该文件的内容很简单: |
||||
|
|
||||
|
````js |
||||
|
$(function() { |
||||
|
abp.log.info('My custom role script file has been loaded!'); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
然后将这个文件添加到角色管页面理捆绑包中: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpBundlingOptions>(options => |
||||
|
{ |
||||
|
options.ScriptBundles |
||||
|
.Configure( |
||||
|
typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName, |
||||
|
bundleConfig => |
||||
|
{ |
||||
|
bundleConfig.AddFiles("/Pages/Identity/Roles/my-role-script.js"); |
||||
|
}); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
`typeof(Volo.Abp.Identity.Web.Pages.Identity.Roles.IndexModel).FullName` 是获取角色管理页面捆绑包名称的安全方式: |
||||
|
|
||||
|
> 请注意并非每个页面都定义了这个页面的捆绑包. 它们仅在需要时定义. |
||||
|
|
||||
|
除了添加新的CSS/JavaScript文件到页面,你也可以以替换(通过捆绑包contributor)已存在. |
||||
|
|
||||
|
## 布局定制 |
||||
|
|
||||
|
布局由主题([参阅主题](Theming.md))定义设计. 它们不包含在下载的应用程序解决方案中. 通过这种方式你可以轻松的**更改**主题并获取新的功能. 你不能**直接更改**应用程序中的布局代码,除非你用自己的布局替换它(在下一部分中说明). |
||||
|
|
||||
|
有一些通用的方法可以**自定义布局**,将在下一节中介绍. |
||||
|
|
||||
|
### 菜单贡献者 |
||||
|
|
||||
|
ABP框架定义了两个**标准菜单**: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
* `StandardMenus.Main`: 应用程序的主菜单. |
||||
|
* `StandardMenus.User`: 用户菜单 (通常在屏幕的右上方). |
||||
|
|
||||
|
显示菜单是主题的责任,但**菜单项**由模板和你的应用程序代码决定. 只需要实现 `IMenuContributor` 接口并在 `ConfigureMenuAsync` 方法操作菜单项. |
||||
|
|
||||
|
渲染菜单时需要执行菜单贡献者. **应用程序启动模板** 已经定义了菜单贡献者,所以你可以使用它. 参阅[导航菜单](Navigation-Menu.md)文档了解更多. |
||||
|
|
||||
|
### 工具栏贡献者 |
||||
|
|
||||
|
[工具栏系统](Toolbars.md)用于在用户界面定义 **工具栏** . 模块 (或你的应用程序)可以将 **项** 添加到工具栏, 随后主题将在**布局**上呈现工具栏. |
||||
|
|
||||
|
只有一个 **标准工具栏** (名称为 "Main" - 定义为常量: `StandardToolbars.Main`). 对于基本主题,按如下呈现: |
||||
|
|
||||
|
在上面的屏幕快照中,主工具栏添加了两个项目:语言开关组件和用户菜单. 你可以在此处添加自己的项. |
||||
|
|
||||
|
#### 示例: 添加通知图标 |
||||
|
|
||||
|
在这个示例中,我们会添加一个**通知(响铃)图标**到语言切换项的左侧. 工具栏的项项目是一个**视图组件**. 所以,在你的项目中创建一个新的视图组件: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
**NotificationViewComponent.cs** |
||||
|
|
||||
|
````csharp |
||||
|
public class NotificationViewComponent : AbpViewComponent |
||||
|
{ |
||||
|
public async Task<IViewComponentResult> InvokeAsync() |
||||
|
{ |
||||
|
return View("/Pages/Shared/Components/Notification/Default.cshtml"); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
**Default.cshtml** |
||||
|
|
||||
|
````xml |
||||
|
<div id="MainNotificationIcon" style="color: white; margin: 8px;"> |
||||
|
<i class="far fa-bell"></i> |
||||
|
</div> |
||||
|
```` |
||||
|
|
||||
|
现在,我们创建一个类实现 `IToolbarContributor` 接口: |
||||
|
|
||||
|
````csharp |
||||
|
public class MyToolbarContributor : IToolbarContributor |
||||
|
{ |
||||
|
public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) |
||||
|
{ |
||||
|
if (context.Toolbar.Name == StandardToolbars.Main) |
||||
|
{ |
||||
|
context.Toolbar.Items |
||||
|
.Insert(0, new ToolbarItem(typeof(NotificationViewComponent))); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
这个类向 `Main` 工具栏的第一项添加了 `NotificationViewComponent`. |
||||
|
|
||||
|
最后你需要将这个贡献者添加到 `AbpToolbarOptions`,在你模块类的 `ConfigureServices` 方法: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpToolbarOptions>(options => |
||||
|
{ |
||||
|
options.Contributors.Add(new MyToolbarContributor()); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
这就够了,当你运行应用程序后会看到工具栏上的通知图标: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
示例中的 `NotificationViewComponent` 返回没有任何数据的视图. 在实际场景中,你可能想**查询数据库**(或调用HTTP API)获取通知并传递给视图. 如果需要可以将 `JavaScript` 或 `CSS` 文件添加到工具栏的全局捆绑包中(如前所述). |
||||
|
|
||||
|
参阅[工具栏文档](Toolbars.md)了解更多关于工具栏系统. |
||||
|
|
||||
|
### 布局钩子 |
||||
|
|
||||
|
[布局钩子](Layout-Hooks.md) 系统允许你在布局页面的某些特定部分 **添加代码** . 所有主题的所有布局都应该实现这些钩子. 然后你可以将**视图组件**添加到钩子. |
||||
|
|
||||
|
#### 示例: 添加谷歌统计 |
||||
|
|
||||
|
假设你想要添加谷歌统计脚本到布局(将适用所有的页面). 首先在你的项目中**创建一个视图组件**: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
**NotificationViewComponent.cs** |
||||
|
|
||||
|
````csharp |
||||
|
public class GoogleAnalyticsViewComponent : AbpViewComponent |
||||
|
{ |
||||
|
public IViewComponentResult Invoke() |
||||
|
{ |
||||
|
return View("/Pages/Shared/Components/GoogleAnalytics/Default.cshtml"); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
**Default.cshtml** |
||||
|
|
||||
|
````html |
||||
|
<script> |
||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ |
||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), |
||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) |
||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); |
||||
|
|
||||
|
ga('create', 'UA-xxxxxx-1', 'auto'); |
||||
|
ga('send', 'pageview'); |
||||
|
</script> |
||||
|
```` |
||||
|
|
||||
|
在你自己的代码中更改 `UA-xxxxxx-1` . |
||||
|
|
||||
|
然后你可以在你模块的 `ConfigureServices` 方法将这个组件添加到任何的钩子点: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpLayoutHookOptions>(options => |
||||
|
{ |
||||
|
options.Add( |
||||
|
LayoutHooks.Head.Last, //The hook name |
||||
|
typeof(GoogleAnalyticsViewComponent) //The component to add |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
现在谷歌统计代码将在页面的 `head` 所为最后一项插入. 你(或你在使用的模块)可以将多个项添加到相同的钩子,它们都会添加到布局. |
||||
|
|
||||
|
在上面我们添加 `GoogleAnalyticsViewComponent` 到所有的布局,你可能只想添加到指定的布局: |
||||
|
|
||||
|
````csharp |
||||
|
Configure<AbpLayoutHookOptions>(options => |
||||
|
{ |
||||
|
options.Add( |
||||
|
LayoutHooks.Head.Last, |
||||
|
typeof(GoogleAnalyticsViewComponent), |
||||
|
layout: StandardLayouts.Application //Set the layout to add |
||||
|
); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
参阅下面的布局部分,以了解有关布局系统的更多信息. |
||||
|
|
||||
|
### 布局 |
||||
|
|
||||
|
布局系统允许主题定义标准,命名布局并且允许任何页面选择使用合适的布局. 有三种预定义的布局: |
||||
|
|
||||
|
* "**Application**": 应用程序的主要(和默认)布局. 它通常包含页眉,菜单(侧栏),页脚,工具栏等. |
||||
|
* "**Account**": 登录,注册和其他类似页面使用此布局. 默认它用于 `/Pages/Account` 文件夹下的页面. |
||||
|
* "**Empty**": 空的最小的布局. |
||||
|
|
||||
|
这些名称在 `StandardLayouts` 类定义为常量. 这是标准的布局名称,所有的主题开箱即用的实现. 你也可以创建自己的布局. |
||||
|
|
||||
|
#### 布局位置 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts)找到基本主题的布局文件. 你可以将它们作用构建自己的布局的参考,也可以在必要时覆盖它们. |
||||
|
|
||||
|
#### ITheme |
||||
|
|
||||
|
ABP框架使用 `ITheme` 服务通过局部名称获取布局位置. 你可以替换此服务动态的选择布局位置. |
||||
|
|
||||
|
#### IThemeManager |
||||
|
|
||||
|
`IThemeManager` 用于获取当前主题,并得到了布局路径. 任何页面可以都决定自己的布局. 例: |
||||
|
|
||||
|
````html |
||||
|
@using Volo.Abp.AspNetCore.Mvc.UI.Theming |
||||
|
@inject IThemeManager ThemeManager |
||||
|
@{ |
||||
|
Layout = ThemeManager.CurrentTheme.GetLayout(StandardLayouts.Empty); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
此页面将使用空白布局. 它使用 `ThemeManager.CurrentTheme.GetEmptyLayout()` 扩展方法. |
||||
|
|
||||
|
如果你设置特定目录下所有页面的布局,可以在该文件夹下的 `_ViewStart.cshtml` 文件编写以上代码. |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
# ABP ASP.NET Core UI Datatables.Net 集成 |
||||
|
|
||||
|
TODO |
||||
File diff suppressed because it is too large
|
After Width: | Height: | Size: 52 KiB |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue