mirror of https://github.com/abpframework/abp.git
5009 changed files with 60438 additions and 797398 deletions
@ -0,0 +1,140 @@ |
|||
# Auto API Controllers |
|||
|
|||
Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. |
|||
|
|||
ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. |
|||
|
|||
## Configuration |
|||
|
|||
Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: |
|||
|
|||
````csharp |
|||
[DependsOn(BookStoreApplicationModule)] |
|||
public class BookStoreWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options |
|||
.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). |
|||
|
|||
 |
|||
|
|||
### Examples |
|||
|
|||
Some example method names and the corresponding routes calculated by convention: |
|||
|
|||
| Service Method Name | HTTP Method | Route | |
|||
| ----------------------------------------------------- | ----------- | -------------------------- | |
|||
| GetAsync(Guid id) | GET | /api/app/book/{id} | |
|||
| GetListAsync() | GET | /api/app/book | |
|||
| CreateAsync(CreateBookDto input) | POST | /api/app/book | |
|||
| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | |
|||
| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | |
|||
| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | |
|||
| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | |
|||
|
|||
### HTTP Method |
|||
|
|||
ABP uses a naming convention while determining the HTTP method for a service method (action): |
|||
|
|||
- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. |
|||
- **Put**: Used if the method name starts with 'Put' or 'Update'. |
|||
- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. |
|||
- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. |
|||
- **Patch**: Used if the method name starts with 'Patch'. |
|||
- Otherwise, **Post** is used **by default**. |
|||
|
|||
If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. |
|||
|
|||
### Route |
|||
|
|||
Route is calculated based on some conventions: |
|||
|
|||
* It always starts with '**/api**'. |
|||
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: |
|||
|
|||
````csharp |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.RootPath = "volosoft/book-store"; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. |
|||
|
|||
* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. |
|||
* If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. |
|||
* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. |
|||
* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; |
|||
* Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. |
|||
* Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. |
|||
* Converting the result to **camelCase**. |
|||
* If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. |
|||
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. |
|||
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). |
|||
|
|||
## Service Selection |
|||
|
|||
Creating conventional HTTP API controllers are not unique to application services actually. |
|||
|
|||
### IRemoteService Interface |
|||
|
|||
If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. |
|||
|
|||
### RemoteService Attribute |
|||
|
|||
`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
### TypePredicate Option |
|||
|
|||
You can further filter classes to become an API controller by providing the `TypePredicate` option: |
|||
|
|||
````csharp |
|||
services.Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.TypePredicate = type => { return true; }; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. |
|||
|
|||
## API Explorer |
|||
|
|||
API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. |
|||
|
|||
API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsMetadataEnabled = false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. |
|||
@ -0,0 +1,165 @@ |
|||
# Dynamic C# API Clients |
|||
|
|||
ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. |
|||
|
|||
## Service Interface |
|||
|
|||
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: |
|||
|
|||
````csharp |
|||
public interface IBookAppService : IApplicationService |
|||
{ |
|||
Task<List<BookDto>> GetListAsync(); |
|||
} |
|||
```` |
|||
|
|||
Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. |
|||
|
|||
Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. |
|||
|
|||
## Client Proxy Generation |
|||
|
|||
First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Http.Client |
|||
```` |
|||
|
|||
Then add `AbpHttpClientModule` dependency to your module: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpHttpClientModule))] //add the dependency |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
Now, it's ready to create the client proxies. Example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule), //used to create client proxies |
|||
typeof(BookStoreApplicationModule) //contains the application service interfaces |
|||
)] |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//Create dynamic client proxies |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. |
|||
|
|||
### Endpoint Configuration |
|||
|
|||
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: |
|||
|
|||
```` |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See the "AbpRemoteServiceOptions" section below for more detailed configuration. |
|||
|
|||
## Usage |
|||
|
|||
It's straightforward to use. Just inject the service interface in the client application code: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBookAppService _bookService; |
|||
|
|||
public MyService(IBookAppService bookService) |
|||
{ |
|||
_bookService = bookService; |
|||
} |
|||
|
|||
public async Task DoIt() |
|||
{ |
|||
var books = await _bookService.GetListAsync(); |
|||
foreach (var book in books) |
|||
{ |
|||
Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. |
|||
|
|||
### IHttpClientProxy Interface |
|||
|
|||
While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy<IBookAppService>` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy<T>` interface. |
|||
|
|||
## Configuration |
|||
|
|||
### AbpRemoteServiceOptions |
|||
|
|||
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: |
|||
|
|||
````csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<AbpRemoteServiceOptions>(options => |
|||
{ |
|||
options.RemoteServices.Default = |
|||
new RemoteServiceConfiguration("http://localhost:53929/"); |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### Multiple Remote Service Endpoints |
|||
|
|||
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: |
|||
|
|||
````json |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
}, |
|||
"BookStore": { |
|||
"BaseUrl": "http://localhost:48392/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
remoteServiceName: "BookStore" |
|||
); |
|||
```` |
|||
|
|||
`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. |
|||
|
|||
### As Default Services |
|||
|
|||
When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
asDefaultServices: false |
|||
); |
|||
```` |
|||
|
|||
Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. |
|||
|
|||
> If you disable `asDefaultServices`, you can only use `IHttpClientProxy<T>` interface to use the client proxies (see the related section above). |
|||
@ -0,0 +1,3 @@ |
|||
# abp.auth JavaScript API |
|||
|
|||
TODO |
|||
@ -0,0 +1,24 @@ |
|||
# JavaScript API |
|||
|
|||
ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. |
|||
|
|||
## APIs |
|||
|
|||
* abp.ajax |
|||
* [abp.auth](Auth.md) |
|||
* abp.currentUser |
|||
* abp.dom |
|||
* abp.event |
|||
* abp.features |
|||
* abp.localization |
|||
* abp.log |
|||
* abp.ModalManager |
|||
* abp.notify |
|||
* abp.security |
|||
* abp.setting |
|||
* abp.ui |
|||
* abp.utils |
|||
* abp.ResourceLoader |
|||
* abp.WidgetManager |
|||
* Other APIs |
|||
|
|||
@ -1,140 +1,3 @@ |
|||
# Auto API Controllers |
|||
This document has moved. |
|||
|
|||
Once you create an [application service](../Application-Services.md), you generally want to create an API controller to expose this service as an HTTP (REST) API endpoint. A typical API controller does nothing but redirects method calls to the application service and configures the REST API using attributes like [HttpGet], [HttpPost], [Route]... etc. |
|||
|
|||
ABP can **automagically** configure your application services as API Controllers by convention. Most of time you don't care about its detailed configuration, but it's possible to fully customize it. |
|||
|
|||
## Configuration |
|||
|
|||
Basic configuration is simple. Just configure `AbpAspNetCoreMvcOptions` and use `ConventionalControllers.Create` method as shown below: |
|||
|
|||
````csharp |
|||
[DependsOn(BookStoreApplicationModule)] |
|||
public class BookStoreWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options |
|||
.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example code configures all the application services in the assembly containing the class `BookStoreApplicationModule`. The figure below shows the resulting API on the [Swagger UI](https://swagger.io/tools/swagger-ui/). |
|||
|
|||
 |
|||
|
|||
### Examples |
|||
|
|||
Some example method names and the corresponding routes calculated by convention: |
|||
|
|||
| Service Method Name | HTTP Method | Route | |
|||
| ----------------------------------------------------- | ----------- | -------------------------- | |
|||
| GetAsync(Guid id) | GET | /api/app/book/{id} | |
|||
| GetListAsync() | GET | /api/app/book | |
|||
| CreateAsync(CreateBookDto input) | POST | /api/app/book | |
|||
| UpdateAsync(Guid id, UpdateBookDto input) | PUT | /api/app/book/{id} | |
|||
| DeleteAsync(Guid id) | DELETE | /api/app/book/{id} | |
|||
| GetEditorsAsync(Guid id) | GET | /api/app/book/{id}/editors | |
|||
| CreateEditorAsync(Guid id, BookEditorCreateDto input) | POST | /api/app/book/{id}/editor | |
|||
|
|||
### HTTP Method |
|||
|
|||
ABP uses a naming convention while determining the HTTP method for a service method (action): |
|||
|
|||
- **Get**: Used if the method name starts with 'GetList', 'GetAll' or 'Get'. |
|||
- **Put**: Used if the method name starts with 'Put' or 'Update'. |
|||
- **Delete**: Used if the method name starts with 'Delete' or 'Remove'. |
|||
- **Post**: Used if the method name starts with 'Create', 'Add', 'Insert' or 'Post'. |
|||
- **Patch**: Used if the method name starts with 'Patch'. |
|||
- Otherwise, **Post** is used **by default**. |
|||
|
|||
If you need to customize HTTP method for a particular method, then you can use one of the standard ASP.NET Core attributes ([HttpPost], [HttpGet], [HttpPut]... etc.). This requires to add [Microsoft.AspNetCore.Mvc.Core](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Core) nuget package to your project that contains the service. |
|||
|
|||
### Route |
|||
|
|||
Route is calculated based on some conventions: |
|||
|
|||
* It always starts with '**/api**'. |
|||
* Continues with a **route path**. Default value is '**/app**' and can be configured as like below: |
|||
|
|||
````csharp |
|||
Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.RootPath = "volosoft/book-store"; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}**'. This sample uses two-level root path, but you generally use a single level depth. |
|||
|
|||
* Continues with the **normalized controller/service name**. Normalization removes 'AppService', 'ApplicationService' and 'Service' postfixes and converts it to **camelCase**. If your application service class name is 'BookAppService' then it becomes only '/book'. |
|||
* If you want to customize naming, then set the `UrlControllerNameNormalizer` option. It's a func delegate which allows you to determine the name per controller/service. |
|||
* If the method has an '**id**' parameter then it adds '**/{id}**' ro the route. |
|||
* Then it adds the action name if necessary. Action name is obtained from the method name on the service and normalized by; |
|||
* Removing '**Async**' postfix. If the method name is 'GetPhonesAsync' then it becomes 'GetPhones'. |
|||
* Removing **HTTP method prefix**. 'GetList', 'GetAll', 'Get', 'Put', 'Update', 'Delete', 'Remove', 'Create', 'Add', 'Insert', 'Post' and 'Patch' prefixes are removed based on the selected HTTP method. So, 'GetPhones' becomes 'Phones' since 'Get' prefix is a duplicate for a GET request. |
|||
* Converting the result to **camelCase**. |
|||
* If the resulting action name is **empty** then it's not added to the route. If it's not empty, it's added to the route (like '/phones'). For 'GetAllAsync' method name it will be empty, for 'GetPhonesAsync' method name it will be 'phones'. |
|||
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method. |
|||
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId'). |
|||
|
|||
## Service Selection |
|||
|
|||
Creating conventional HTTP API controllers are not unique to application services actually. |
|||
|
|||
### IRemoteService Interface |
|||
|
|||
If a class implements the `IRemoteService` interface then it's automatically selected to be a conventional API controller. Since application services inherently implement it, they are considered as natural API controllers. |
|||
|
|||
### RemoteService Attribute |
|||
|
|||
`RemoteService` attribute can be used to mark a class as a remote service or disable for a particular class that inherently implements the `IRemoteService` interface. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsEnabled = false)] //or simply [RemoteService(false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
### TypePredicate Option |
|||
|
|||
You can further filter classes to become an API controller by providing the `TypePredicate` option: |
|||
|
|||
````csharp |
|||
services.Configure<AbpAspNetCoreMvcOptions>(options => |
|||
{ |
|||
options.ConventionalControllers |
|||
.Create(typeof(BookStoreApplicationModule).Assembly, opts => |
|||
{ |
|||
opts.TypePredicate = type => { return true; }; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Instead of returning `true` for every type, you can check it and return `false` if you don't want to expose this type as an API controller. |
|||
|
|||
## API Explorer |
|||
|
|||
API Exploring a service that makes possible to investigate API structure by the clients. Swagger uses it to create a documentation and test UI for an endpoint. |
|||
|
|||
API Explorer is automatically enabled for conventional HTTP API controllers by default. Use `RemoteService` attribute to control it per class or method level. Example: |
|||
|
|||
````csharp |
|||
[RemoteService(IsMetadataEnabled = false)] |
|||
public class PersonAppService : ApplicationService |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Disabled `IsMetadataEnabled` which hides this service from API explorer and it will not be discoverable. However, it still can be usable for the clients know the exact API path/route. |
|||
[Click to navigate to Auto API Controllers document](../API/Auto-API-Controllers.md) |
|||
@ -1,352 +1,4 @@ |
|||
|
|||
# ASP.NET Core MVC Bundling & Minification |
|||
This document has moved. |
|||
|
|||
There are many ways of bundling & minification of client side resources (JavaScript and CSS files). Most common ways are: |
|||
|
|||
* Using the [Bundler & Minifier](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BundlerMinifier) Visual Studio extension or the [NuGet package](https://www.nuget.org/packages/BuildBundlerMinifier/). |
|||
* Using [Gulp](https://gulpjs.com/)/[Grunt](https://gruntjs.com/) task managers and their plugins. |
|||
|
|||
ABP offers a simple, dynamic, powerful, modular and built-in way. |
|||
|
|||
## Volo.Abp.AspNetCore.Mvc.UI.Bundling Package |
|||
|
|||
> This package is already installed by default with the startup templates. So, most of the time, you don't need to install it manually. |
|||
|
|||
Install the `Volo.Abp.AspNetCore.Mvc.UI.Bundling` nuget package to your project: |
|||
|
|||
```` |
|||
install-package Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
```` |
|||
|
|||
Then you can add the `AbpAspNetCoreMvcUiBundlingModule` dependency to your module: |
|||
|
|||
````C# |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Razor Bundling Tag Helpers |
|||
|
|||
The simplest way of creating a bundle is to use `abp-script-bundle` or `abp-style-bundle` tag helpers. Example: |
|||
|
|||
````html |
|||
<abp-style-bundle name="MyGlobalBundle"> |
|||
<abp-style src="/libs/bootstrap/css/bootstrap.css" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
<abp-style src="/styles/my-global-style.css" /> |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
This bundle defines a style bundle with a **unique name**: `MyGlobalBundle`. It's very easy to understand how to use it. Let's see how it *works*: |
|||
|
|||
* ABP creates the bundle as **lazy** from the provided files when it's **first requested**. For the subsequent calls, it's returned from the **cache**. That means if you conditionally add the files to the bundle, it's executed only once and any changes of the condition will not effect the bundle for the next requests. |
|||
* ABP adds bundle files **individually** to the page for the `development` environment. It automatically bundles & minifies for other environments (`staging`, `production`...). |
|||
* The bundle files may be **physical** files or [**virtual/embedded** files](../Virtual-File-System.md). |
|||
* ABP automatically adds **version query string** to the bundle file URL to prevent browsers from caching when the bundle is being updated. (like ?_v=67872834243042 - generated from last change date of the related files). The versioning works even if the bundle files are individually added to the page (on the development environment). |
|||
|
|||
### Importing The Bundling Tag Helpers |
|||
|
|||
> This is already imported by default with the startup templates. So, most of the time, you don't need to add it manually. |
|||
|
|||
In order to use bundle tag helpers, you need to add it into your `_ViewImports.cshtml` file or into your page: |
|||
|
|||
```` |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
```` |
|||
|
|||
### Unnamed Bundles |
|||
|
|||
The `name` is **optional** for the razor bundle tag helpers. If you don't define a name, it's automatically **calculated** based on the used bundle file names (they are **concatenated** and **hashed**). Example: |
|||
|
|||
````html |
|||
<abp-style-bundle> |
|||
<abp-style src="/libs/bootstrap/css/bootstrap.css" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
@if (ViewBag.IncludeCustomStyles != false) |
|||
{ |
|||
<abp-style src="/styles/my-global-style.css" /> |
|||
} |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
This will potentially create **two different bundles** (one incudes the `my-global-style.css` and other does not). |
|||
|
|||
Advantages of **unnamed** bundles: |
|||
|
|||
* Can **conditionally add items** to the bundle. But this may lead to multiple variations of the bundle based on the conditions. |
|||
|
|||
Advantages of **named** bundles: |
|||
|
|||
* Other **modules can contribute** to the bundle by its name (see the sections below). |
|||
|
|||
### Single File |
|||
|
|||
If you need to just add a single file to the page, you can use the `abp-script` or `abp-style` tag without a wrapping in the `abp-script-bundle` or `abp-style-bundle` tag. Example: |
|||
|
|||
````xml |
|||
<abp-script src="/scripts/my-script.js" /> |
|||
```` |
|||
|
|||
The bundle name will be *scripts.my-scripts* for the example above ("/" is replaced by "."). All bundling features are work as expected for single file bundles too. |
|||
|
|||
## Bundling Options |
|||
|
|||
If you need to use same bundle in **multiple pages** or want to use some more **powerful features**, you can configure bundles **by code** in your [module](../Module-Development-Basics.md) class. |
|||
|
|||
### Creating A New Bundle |
|||
|
|||
Example usage: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiBundlingModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Add("MyGlobalBundle", bundle => { |
|||
bundle.AddFiles( |
|||
"/libs/jquery/jquery.js", |
|||
"/libs/bootstrap/js/bootstrap.js", |
|||
"/libs/toastr/toastr.min.js", |
|||
"/scripts/my-global-scripts.js" |
|||
); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> You can use the same name (*MyGlobalBundle* here) for a script & style bundle since they are added to different collections (`ScriptBundles` and `StyleBundles`). |
|||
|
|||
After defining such a bundle, it can be included into a page using the same tag helpers defined above. Example: |
|||
|
|||
````html |
|||
<abp-script-bundle name="MyGlobalBundle" /> |
|||
```` |
|||
|
|||
This time, no file defined in the tag helper definition because the bundle files are defined by the code. |
|||
|
|||
### Configuring An Existing Bundle |
|||
|
|||
ABP supports [modularity](../Module-Development-Basics.md) for bundling as well. A module can modify an existing bundle that is created by a dependant module. Example: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(MyWebModule))] |
|||
public class MyWebExtensionModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Configure("MyGlobalBundle", bundle => { |
|||
bundle.AddFiles( |
|||
"/scripts/my-extension-script.js" |
|||
); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> It's not possible to configure unnamed bundle tag helpers by code, because their name are not known at the development time. It's suggested to always use a name for a bundle tag helper. |
|||
|
|||
## Bundle Contributors |
|||
|
|||
Adding files to an existing bundle seems useful. What if you need to **replace** a file in the bundle or you want to **conditionally** add files? Defining a bundle contributor provides extra power for such cases. |
|||
|
|||
An example bundle contributor that replaces bootstrap.css with a customized version: |
|||
|
|||
````C# |
|||
public class MyExtensionGlobalStyleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.ReplaceOne( |
|||
"/libs/bootstrap/css/bootstrap.css", |
|||
"/styles/extensions/bootstrap-customized.css" |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can use this contributor as like below: |
|||
|
|||
````C# |
|||
services.Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.ScriptBundles |
|||
.Configure("MyGlobalBundle", bundle => { |
|||
bundle.AddContributors(typeof(MyExtensionGlobalStyleContributor)); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> You can also add contributors while creating a new bundle. |
|||
|
|||
Contributors can also be used in the bundle tag helpers. Example: |
|||
|
|||
````xml |
|||
<abp-style-bundle> |
|||
<abp-style type="@typeof(BootstrapStyleContributor)" /> |
|||
<abp-style src="/libs/font-awesome/css/font-awesome.css" /> |
|||
<abp-style src="/libs/toastr/toastr.css" /> |
|||
</abp-style-bundle> |
|||
```` |
|||
|
|||
`abp-style` and `abp-script` tags can get `type` attributes (instead of `src` attributes) as shown in this sample. When you add a bundle contributor, its dependencies are also automatically added to the bundle. |
|||
|
|||
### Contributor Dependencies |
|||
|
|||
A bundle contributor can have one or more dependencies to other contributors. |
|||
Example: |
|||
|
|||
````C# |
|||
[DependsOn(typeof(MyDependedBundleContributor))] //Define the dependency |
|||
public class MyExtensionStyleBundleContributor : BundleContributor |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
When a bundle contributor is added, its dependencies are **automatically and recursively** added. Dependencies added by the **dependency order** by preventing **duplicates**. Duplicates are prevented even if they are in separated bundles. ABP organizes all bundles in a page and eliminates duplications. |
|||
|
|||
Creating contributors and defining dependencies is a way of organizing bundle creation across different modules. |
|||
|
|||
### Contributor Extensions |
|||
|
|||
In some advanced scenarios, you may want to do some additional configuration whenever a bundle contributor is used. Contributor extensions works seamlessly when the extended contributor is used. |
|||
|
|||
The example below adds some styles for prism.js library: |
|||
|
|||
````csharp |
|||
public class MyPrismjsStyleExtension : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files.AddIfNotContains("/libs/prismjs/plugins/toolbar/prism-toolbar.css"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can configure `BundleContributorOptions` to extend existing `PrismjsStyleBundleContributor`. |
|||
|
|||
````csharp |
|||
Configure<BundleContributorOptions>(options => |
|||
{ |
|||
options |
|||
.Extensions<PrismjsStyleBundleContributor>() |
|||
.Add<MyPrismjsStyleExtension>(); |
|||
}); |
|||
```` |
|||
|
|||
Whenever `PrismjsStyleBundleContributor` is added into a bundle, `MyPrismjsStyleExtension` will also be automatically added. |
|||
|
|||
### Accessing to the IServiceProvider |
|||
|
|||
While it is rarely needed, `BundleConfigurationContext` has a `ServiceProvider` property that you can resolve service dependencies inside the `ConfigureBundle` method. |
|||
|
|||
### Standard Package Contributors |
|||
|
|||
Adding a specific NPM package resource (js, css files) into a bundle is pretty straight forward for that package. For example you always add the `bootstrap.css` file for the bootstrap NPM package. |
|||
|
|||
There are built-in contributors for all [standard NPM packages](Client-Side-Package-Management.md). For example, if your contributor depends on the bootstrap, you can just declare it, instead of adding the bootstrap.css yourself. |
|||
|
|||
````C# |
|||
[DependsOn(typeof(BootstrapStyleContributor))] //Define the bootstrap style dependency |
|||
public class MyExtensionStyleBundleContributor : BundleContributor |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
Using the built-in contributors for standard packages; |
|||
|
|||
* Prevents you typing **the invalid resource paths**. |
|||
* Prevents changing your contributor if the resource **path changes** (the dependant contributor will handle it). |
|||
* Prevents multiple modules adding the **duplicate files**. |
|||
* Manages **dependencies recursively** (adds dependencies of dependencies, if necessary). |
|||
|
|||
#### Volo.Abp.AspNetCore.Mvc.UI.Packages Package |
|||
|
|||
> This package is already installed by default in the startup templates. So, most of the time, you don't need to install it manually. |
|||
|
|||
Standard package contributors are defined in the `Volo.Abp.AspNetCore.Mvc.UI.Packages` NuGet package. |
|||
To install it to your project: |
|||
|
|||
```` |
|||
install-package Volo.Abp.AspNetCore.Mvc.UI.Packages |
|||
```` |
|||
|
|||
Then add the `AbpAspNetCoreMvcUiPackagesModule` module dependency to your own module; |
|||
|
|||
````C# |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
|
|||
namespace MyCompany.MyProject |
|||
{ |
|||
[DependsOn(typeof(AbpAspNetCoreMvcUiPackagesModule))] |
|||
public class MyWebModule : AbpModule |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Bundle Inheritance |
|||
|
|||
In some specific cases, it may be needed to create a **new** bundle **inherited** from other bundle(s). Inheriting from a bundle (recursively) inherits all files/contributors of that bundle. Then the derived bundle can add or modify files/contributors **without modifying** the original bundle. |
|||
Example: |
|||
|
|||
````c# |
|||
services.Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options |
|||
.StyleBundles |
|||
.Add("MyTheme.MyGlobalBundle", bundle => { |
|||
bundle |
|||
.AddBaseBundles("MyGlobalBundle") //Can add multiple |
|||
.AddFiles( |
|||
"/styles/mytheme-global-styles.css" |
|||
); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
## Themes |
|||
|
|||
Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. |
|||
|
|||
## Best Practices & Suggestions |
|||
|
|||
It's suggested to define multiple bundles for an application, each one is used for different purposes. |
|||
|
|||
* **Global bundle**: Global style/script bundles are included to every page in the application. Themes already defines global style & script bundles. Your module can contribute to them. |
|||
* **Layout bundles**: This is a specific bundle to an individual layout. Only contains resources shared among all the pages use the layout. Use the bundling tag helpers to create the bundle as a good practice. |
|||
* **Module bundles**: For shared resources among the pages of an individual module. |
|||
* **Page bundles**: Specific bundles created for each page. Use the bundling tag helpers to create the bundle as a best practice. |
|||
|
|||
Establish a balance between performance, network bandwidth usage and count of many bundles. |
|||
|
|||
## See Also |
|||
|
|||
* [Client Side Package Management](Client-Side-Package-Management.md) |
|||
* [Theming](Theming.md) |
|||
[Click to navigate to ASP.NET Core MVC Bundling & Minification document](../UI/AspNetCore/Bundling-Minification.md) |
|||
@ -1,116 +1,4 @@ |
|||
|
|||
## ASP.NET Core MVC Client Side Package Management |
|||
This document has moved. |
|||
|
|||
ABP framework can work with any type of client side package management systems. You can even decide to use no package management system and manage your dependencies manually. |
|||
|
|||
However, ABP framework works best with **NPM/Yarn**. By default, built-in modules are configured to work with NPM/Yarn. |
|||
|
|||
Finally, we suggest the [**Yarn**](https://classic.yarnpkg.com/) over the NPM since it's faster, stable and also compatible with the NPM. |
|||
|
|||
### @ABP NPM Packages |
|||
|
|||
ABP is a modular platform. Every developer can create modules and the modules should work together in a **compatible** and **stable** state. |
|||
|
|||
One challenge is the **versions of the dependant NPM packages**. What if two different modules use the same JavaScript library but its different (and potentially incompatible) versions. |
|||
|
|||
To solve the versioning problem, we created a **standard set of packages** those depends on some common third-party libraries. Some example packages are [@abp/jquery](https://www.npmjs.com/package/@abp/jquery), [@abp/bootstrap](https://www.npmjs.com/package/@abp/bootstrap) and [@abp/font-awesome](https://www.npmjs.com/package/@abp/font-awesome). You can see the **list of packages** from the [Github repository](https://github.com/volosoft/abp/tree/master/npm/packs). |
|||
|
|||
The benefit of a **standard package** is: |
|||
|
|||
* It depends on a **standard version** of a package. Depending on this package is **safe** because all modules depend on the same version. |
|||
* It contains the gulp task to copy library resources (js, css, img... files) from the **node_modules** folder to **wwwroot/libs** folder. See the *Mapping The Library Resources* section for more. |
|||
|
|||
Depending on a standard package is easy. Just add it to your **package.json** file like you normally do. Example: |
|||
|
|||
```` |
|||
{ |
|||
... |
|||
"dependencies": { |
|||
"@abp/bootstrap": "^1.0.0" |
|||
} |
|||
} |
|||
```` |
|||
|
|||
It's suggested to depend on a standard package instead of directly depending on a third-party package. |
|||
|
|||
#### Package Installation |
|||
|
|||
After depending on a NPM package, all you should do is to run the **yarn** command from the command line to install all the packages and their dependencies: |
|||
|
|||
```` |
|||
yarn |
|||
```` |
|||
|
|||
Alternatively, you can use `npm install` but [Yarn](https://classic.yarnpkg.com/) is suggested as mentioned before. |
|||
|
|||
#### Package Contribution |
|||
|
|||
If you need a third-party NPM package that is not in the standard set of packages, you can create a Pull Request on the Github [repository](https://github.com/volosoft/abp). A pull request that follows these rules is accepted: |
|||
|
|||
* Package name should be named as `@abp/package-name` for a `package-name` on NPM (example: `@abp/bootstrap` for the `bootstrap` package). |
|||
* It should be the **latest stable** version of the package. |
|||
* It should only depend a **single** third-party package. It can depend on multiple `@abp/*` packages. |
|||
* The package should include a `abp.resourcemapping.js` file formatted as defined in the *Mapping The Library Resources* section. This file should only map resources for the depended package. |
|||
* You also need to create [bundle contributor(s)](Bundling-Minification.md) for the package you have created. |
|||
|
|||
See current standard packages for examples. |
|||
|
|||
### Mapping The Library Resources |
|||
|
|||
Using NPM packages and NPM/Yarn tool is the de facto standard for client side libraries. NPM/Yarn tool creates a **node_modules** folder in the root folder of your web project. |
|||
|
|||
Next challenge is copying needed resources (js, css, img... files) from the `node_modules` into a folder inside the **wwwroot** folder to make it accessible to the clients/browsers. |
|||
|
|||
ABP defines a [Gulp](https://gulpjs.com/) based task to **copy resources** from **node_modules** to **wwwroot/libs** folder. Each **standard package** (see the *@ABP NPM Packages* section) defines the mapping for its own files. So, most of the time, you only configure dependencies. |
|||
|
|||
The **startup templates** are already configured to work all these out of the box. This section will explain the configuration options. |
|||
|
|||
#### Resource Mapping Definition File |
|||
|
|||
A module should define a JavaScript file named `abp.resourcemapping.js` which is formatted as in the example below: |
|||
|
|||
````js |
|||
module.exports = { |
|||
aliases: { |
|||
"@node_modules": "./node_modules", |
|||
"@libs": "./wwwroot/libs" |
|||
}, |
|||
clean: [ |
|||
"@libs" |
|||
], |
|||
mappings: { |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
* **aliases** section defines standard aliases (placeholders) that can be used in the mapping paths. **@node_modules** and **@libs** are required (by the standard packages), you can define your own aliases to reduce duplication. |
|||
* **clean** section is a list of folders to clean before copying the files. |
|||
* **mappings** section is a list of mappings of files/folders to copy. This example does not copy any resource itself, but depends on a standard package. |
|||
|
|||
An example mapping configuration is shown below: |
|||
|
|||
````js |
|||
mappings: { |
|||
"@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", |
|||
"@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", |
|||
"@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" |
|||
} |
|||
```` |
|||
|
|||
#### Using The Gulp |
|||
|
|||
Once you properly configure the `abp.resourcemapping.js` file, you can run the gulp command from the command line: |
|||
|
|||
```` |
|||
gulp |
|||
```` |
|||
|
|||
When you run the `gulp`, all packages will copy their own resources into the **wwwroot/libs** folder. Running `yarn & gulp` is only necessary if you make a change in your dependencies in the **package.json** file. |
|||
|
|||
> When you run the Gulp command, dependencies of the application are resolved using the package.json file. The Gulp task automatically discovers and maps all resources from all dependencies (recursively). |
|||
|
|||
#### See Also |
|||
|
|||
* [Bundling & Minification](Bundling-Minification.md) |
|||
* [Theming](Theming.md) |
|||
[Click to navigate to ASP.NET Core MVC Client Side Package Management document](../UI/AspNetCore/Client-Side-Package-Management.md) |
|||
|
|||
@ -1,165 +1,3 @@ |
|||
# Dynamic C# API Clients |
|||
This document has moved. |
|||
|
|||
ABP can dynamically create C# API client proxies to call remote HTTP services (REST APIs). In this way, you don't need to deal with `HttpClient` and other low level HTTP features to call remote services and get results. |
|||
|
|||
## Service Interface |
|||
|
|||
Your service/controller should implement an interface that is shared between the server and the client. So, first define a service interface in a shared library project. Example: |
|||
|
|||
````csharp |
|||
public interface IBookAppService : IApplicationService |
|||
{ |
|||
Task<List<BookDto>> GetListAsync(); |
|||
} |
|||
```` |
|||
|
|||
Your interface should implement the `IRemoteService` interface to be automatically discovered. Since the `IApplicationService` inherits the `IRemoteService` interface, the `IBookAppService` above satisfies this condition. |
|||
|
|||
Implement this class in your service application. You can use [auto API controller system](Auto-API-Controllers.md) to expose the service as a REST API endpoint. |
|||
|
|||
## Client Proxy Generation |
|||
|
|||
First, add [Volo.Abp.Http.Client](https://www.nuget.org/packages/Volo.Abp.Http.Client) nuget package to your client project: |
|||
|
|||
```` |
|||
Install-Package Volo.Abp.Http.Client |
|||
```` |
|||
|
|||
Then add `AbpHttpClientModule` dependency to your module: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpHttpClientModule))] //add the dependency |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
} |
|||
```` |
|||
|
|||
Now, it's ready to create the client proxies. Example: |
|||
|
|||
````csharp |
|||
[DependsOn( |
|||
typeof(AbpHttpClientModule), //used to create client proxies |
|||
typeof(BookStoreApplicationModule) //contains the application service interfaces |
|||
)] |
|||
public class MyClientAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
//Create dynamic client proxies |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, creates and registers proxy classes. |
|||
|
|||
### Endpoint Configuration |
|||
|
|||
`RemoteServices` section in the `appsettings.json` file is used to get remote service address by default. Simplest configuration is shown below: |
|||
|
|||
```` |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See the "RemoteServiceOptions" section below for more detailed configuration. |
|||
|
|||
## Usage |
|||
|
|||
It's straightforward to use. Just inject the service interface in the client application code: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBookAppService _bookService; |
|||
|
|||
public MyService(IBookAppService bookService) |
|||
{ |
|||
_bookService = bookService; |
|||
} |
|||
|
|||
public async Task DoIt() |
|||
{ |
|||
var books = await _bookService.GetListAsync(); |
|||
foreach (var book in books) |
|||
{ |
|||
Console.WriteLine($"[BOOK {book.Id}] Name={book.Name}"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This sample injects the `IBookAppService` service interface defined above. The dynamic client proxy implementation makes an HTTP call whenever a service method is called by the client. |
|||
|
|||
### IHttpClientProxy Interface |
|||
|
|||
While you can inject `IBookAppService` like above to use the client proxy, you could inject `IHttpClientProxy<IBookAppService>` for a more explicit usage. In this case you will use the `Service` property of the `IHttpClientProxy<T>` interface. |
|||
|
|||
## Configuration |
|||
|
|||
### RemoteServiceOptions |
|||
|
|||
`AbpRemoteServiceOptions` is automatically set from the `appsettings.json` by default. Alternatively, you can use `Configure` method to set or override it. Example: |
|||
|
|||
````csharp |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
context.Services.Configure<AbpRemoteServiceOptions>(options => |
|||
{ |
|||
options.RemoteServices.Default = |
|||
new RemoteServiceConfiguration("http://localhost:53929/"); |
|||
}); |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
### Multiple Remote Service Endpoints |
|||
|
|||
The examples above have configured the "Default" remote service endpoint. You may have different endpoints for different services (as like in a microservice approach where each microservice has different endpoints). In this case, you can add other endpoints to your configuration file: |
|||
|
|||
````json |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://localhost:53929/" |
|||
}, |
|||
"BookStore": { |
|||
"BaseUrl": "http://localhost:48392/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`AddHttpClientProxies` method can get an additional parameter for the remote service name. Example: |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
remoteServiceName: "BookStore" |
|||
); |
|||
```` |
|||
|
|||
`remoteServiceName` parameter matches the service endpoint configured via `AbpRemoteServiceOptions`. If the `BookStore` endpoint is not defined then it fallbacks to the `Default` endpoint. |
|||
|
|||
### As Default Services |
|||
|
|||
When you create a service proxy for `IBookAppService`, you can directly inject the `IBookAppService` to use the proxy client (as shown in the usage section). You can pass `asDefaultServices: false` to the `AddHttpClientProxies` method to disable this feature. |
|||
|
|||
````csharp |
|||
context.Services.AddHttpClientProxies( |
|||
typeof(BookStoreApplicationModule).Assembly, |
|||
asDefaultServices: false |
|||
); |
|||
```` |
|||
|
|||
Using `asDefaultServices: false` may only be needed if your application has already an implementation of the service and you do not want to override/replace the other implementation by your client proxy. |
|||
|
|||
> If you disable `asDefaultServices`, you can only use `IHttpClientProxy<T>` interface to use the client proxies (see the related section above). |
|||
[Click to navigate to Dynamic C# API Clients document](../API/Dynamic-CSharp-API-Clients.md) |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
# abp.auth JavaScript API |
|||
This document has moved. |
|||
|
|||
TODO |
|||
[Click to navigate to JavaScript Auth document](../../API/JavaScript-API/Auth.md) |
|||
@ -1,24 +1,3 @@ |
|||
# JavaScript API |
|||
|
|||
ABP provides some JavaScript APIs for ASP.NET Core MVC / Razor Pages applications. They can be used to perform some common application requirements in the client side. |
|||
|
|||
## APIs |
|||
|
|||
* abp.ajax |
|||
* [abp.auth](Auth.md) |
|||
* abp.currentUser |
|||
* abp.dom |
|||
* abp.event |
|||
* abp.features |
|||
* abp.localization |
|||
* abp.log |
|||
* abp.ModalManager |
|||
* abp.notify |
|||
* abp.security |
|||
* abp.setting |
|||
* abp.ui |
|||
* abp.utils |
|||
* abp.ResourceLoader |
|||
* abp.WidgetManager |
|||
* Other APIs |
|||
This document has moved. |
|||
|
|||
[Click to navigate to JavaScript API document](../../API/JavaScript-API/Index.md) |
|||
@ -1,94 +0,0 @@ |
|||
# Buttons |
|||
|
|||
ABP framework has a special Tag Helper to create bootstrap button easily. |
|||
|
|||
`<abp-button>` |
|||
|
|||
## Attributes |
|||
|
|||
`<abp-button>` has 7 different attribute. |
|||
|
|||
* [`button-type`](#button-type) |
|||
* [`size`](#size) |
|||
* [`busy-text`](#busy-text) |
|||
* [`text`](#text) |
|||
* [`icon`](#icon) |
|||
* [`disabled`](#disabled) |
|||
* [`icon-type`](#icon-type) |
|||
|
|||
### `button-type` |
|||
|
|||
`button-type` is a selectable parameter. It's default value is `Default`. |
|||
|
|||
`<abp-button button-type="Primary">Button</abp-button>` |
|||
|
|||
You can choose one of the button type listed below. |
|||
|
|||
* `Default` |
|||
* `Primary` |
|||
* `Secondary` |
|||
* `Success` |
|||
* `Danger` |
|||
* `Warning` |
|||
* `Info` |
|||
* `Light` |
|||
* `Dark` |
|||
* `Outline_Primary` |
|||
* `Outline_Secondary` |
|||
* `Outline_Success` |
|||
* `Outline_Danger` |
|||
* `Outline_Warning` |
|||
* `Outline_Info` |
|||
* `Outline_Light` |
|||
* `Outline_Dark` |
|||
* `Link` |
|||
|
|||
### `size` |
|||
|
|||
`size` is a selectable parameter. It's default value is `Default`. |
|||
|
|||
`<abp-button size="Default">Button</abp-button>` |
|||
|
|||
You can choose one of the size type listed below. |
|||
|
|||
* `Default` |
|||
* `Small` |
|||
* `Medium` |
|||
* `Large` |
|||
* `Block` |
|||
* `Block_Small` |
|||
* `Block_Medium` |
|||
* `Block_Large` |
|||
|
|||
### `busy-text` |
|||
|
|||
`busy-text` is a string parameter. It shows the text while the button is busy. |
|||
|
|||
### `text` |
|||
|
|||
`text` is a string parameter that displaying on button. |
|||
|
|||
### `icon` |
|||
|
|||
`icon` is a string parameter. It is depending to [`icon-type`](#`icon-type`). For default, we use [Font Awesome](https://fontawesome.com/) for icons. To use it, you need to set `icon` parameter as a icon name. |
|||
|
|||
##### Example |
|||
|
|||
[fa-address-card](https://fontawesome.com/icons/address-card):  |
|||
|
|||
`<abp-button icon="address-card" text="Address" />` |
|||
|
|||
> Don't forget: You dont need to write prefix! It will add automatically "fa" prefix for [Font Awesome](https://fontawesome.com/) icons while you did not change `icon-type`. |
|||
|
|||
### `disabled` |
|||
|
|||
`disabled` is a boolean parameter. If you set it `true`, your button will be disabled. |
|||
|
|||
### `icon-type` |
|||
|
|||
`icon-type` is a selectable parameter. It's default value is `FontAwesome`. You can create your own icon type provider and change it. |
|||
|
|||
You can choose one of the icon type listed below. |
|||
|
|||
* `FontAwesome` |
|||
* `Other` |
|||
@ -1,3 +1,3 @@ |
|||
## Dynamic Forms |
|||
This document has moved. |
|||
|
|||
This is not documented yet. You can see a [demo](http://bootstrap-taghelpers.abp.io/Components/DynamicForms) for now. |
|||
[Click to navigate to Dynamic Forms document](../../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) |
|||
@ -1,3 +1,3 @@ |
|||
## ABP Tag Helpers |
|||
This document has moved. |
|||
|
|||
"ABP tag helpers" documentation is creating now. You can see a [demo of components](http://bootstrap-taghelpers.abp.io/) for now. |
|||
[Click to navigate to ABP Tag Helpers document](../../UI/AspNetCore/Tag-Helpers/Index.md) |
|||
|
|||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,3 +1,4 @@ |
|||
# Theming |
|||
|
|||
TODO |
|||
This document has moved. |
|||
|
|||
[Click to navigate to Theming document](../UI/AspNetCore/Theming.md) |
|||
@ -1,505 +1,4 @@ |
|||
# Widgets |
|||
|
|||
ABP provides a model and infrastructure to create **reusable widgets**. Widget system is an extension to [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components). Widgets are especially useful when you want to; |
|||
This document has moved. |
|||
|
|||
* Have **scripts & styles** dependencies for your widget. |
|||
* Create **dashboards** with widgets used inside. |
|||
* Define widgets in reusable **[modules](../Module-Development-Basics.md)**. |
|||
* Co-operate widgets with **[authorization](../Authorization.md)** and **[bundling](Bundling-Minification.md)** systems. |
|||
|
|||
## Basic Widget Definition |
|||
|
|||
### Create a View Component |
|||
|
|||
As the first step, create a new regular ASP.NET Core View Component: |
|||
|
|||
 |
|||
|
|||
**MySimpleWidgetViewComponent.cs**: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Inheriting from `AbpViewComponent` is not required. You could inherit from ASP.NET Core's standard `ViewComponent`. `AbpViewComponent` only defines some base useful properties. |
|||
|
|||
You can inject a service and use in the `Invoke` method to get some data from the service. You may need to make Invoke method async, like `public async Task<IViewComponentResult> InvokeAsync()`. See [ASP.NET Core's ViewComponents](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components) document fore all different usages. |
|||
|
|||
**Default.cshtml**: |
|||
|
|||
```xml |
|||
<div class="my-simple-widget"> |
|||
<h2>My Simple Widget</h2> |
|||
<p>This is a simple widget!</p> |
|||
</div> |
|||
``` |
|||
|
|||
### Define the Widget |
|||
|
|||
Add a `Widget` attribute to the `MySimpleWidgetViewComponent` class to mark this view component as a widget: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Rendering a Widget |
|||
|
|||
Rendering a widget is pretty standard. Use the `Component.InvokeAsync` method in a razor view/page as you do for any view component. Examples: |
|||
|
|||
````xml |
|||
@await Component.InvokeAsync("MySimpleWidget") |
|||
@await Component.InvokeAsync(typeof(MySimpleWidgetViewComponent)) |
|||
```` |
|||
|
|||
First approach uses the widget name while second approach uses the view component type. |
|||
|
|||
### Widgets with Arguments |
|||
|
|||
ASP.NET Core's view component system allows you to accept arguments for view components. The sample view component below accepts `startDate` and `endDate` and uses these arguments to retrieve data from a service. |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Shared.Components.CountersWidget |
|||
{ |
|||
[Widget] |
|||
public class CountersWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
private readonly IDashboardAppService _dashboardAppService; |
|||
|
|||
public CountersWidgetViewComponent(IDashboardAppService dashboardAppService) |
|||
{ |
|||
_dashboardAppService = dashboardAppService; |
|||
} |
|||
|
|||
public async Task<IViewComponentResult> InvokeAsync( |
|||
DateTime startDate, DateTime endDate) |
|||
{ |
|||
var result = await _dashboardAppService.GetCountersWidgetAsync( |
|||
new CountersWidgetInputDto |
|||
{ |
|||
StartDate = startDate, |
|||
EndDate = endDate |
|||
} |
|||
); |
|||
|
|||
return View(result); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Now, you need to pass an anonymous object to pass arguments as shown below: |
|||
|
|||
````xml |
|||
@await Component.InvokeAsync("CountersWidget", new |
|||
{ |
|||
startDate = DateTime.Now.Subtract(TimeSpan.FromDays(7)), |
|||
endDate = DateTime.Now |
|||
}) |
|||
```` |
|||
|
|||
## Widget Name |
|||
|
|||
Default name of the view components are calculated based on the name of the view component type. If your view component type is `MySimpleWidgetViewComponent` then the widget name will be `MySimpleWidget` (removes `ViewComponent` postfix). This is how ASP.NET Core calculates a view component's name. |
|||
|
|||
To customize widget's name, just use the standard `ViewComponent` attribute of ASP.NET Core: |
|||
|
|||
```csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget] |
|||
[ViewComponent(Name = "MyCustomNamedWidget")] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View("~/Pages/Components/MySimpleWidget/Default.cshtml"); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
ABP will respect to the custom name by handling the widget. |
|||
|
|||
> If the view component name and the folder name of the view component don't match, you may need to manually write the view path as done in this example. |
|||
|
|||
### Display Name |
|||
|
|||
You can also define a human-readable, localizable display name for the widget. This display name then can be used on the UI when needed. Display name is optional and can be defined using properties of the `Widget` attribute: |
|||
|
|||
````csharp |
|||
using DashboardDemo.Localization; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
DisplayName = "MySimpleWidgetDisplayName", //Localization key |
|||
DisplayNameResource = typeof(DashboardDemoResource) //localization resource |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See [the localization document](../Localization.md) to learn about localization resources and keys. |
|||
|
|||
## Style & Script Dependencies |
|||
|
|||
There are some challenges when your widget has script and style files; |
|||
|
|||
* Any page uses the widget should also include the **its script & styles** files into the page. |
|||
* The page should also care about **depended libraries/files** of the widget. |
|||
|
|||
ABP solves these issues when you properly relate the resources with the widget. You don't care about dependencies of the widget while using it. |
|||
|
|||
### Defining as Simple File Paths |
|||
|
|||
The example widget below adds a style and a script file: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
StyleFiles = new[] { "/Pages/Components/MySimpleWidget/Default.css" }, |
|||
ScriptFiles = new[] { "/Pages/Components/MySimpleWidget/Default.js" } |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
ABP takes account these dependencies and properly adds to the view/page when you use the widget. Style/script files can be **physical or virtual**. It is completely integrated to the [Virtual File System](../Virtual-File-System.md). |
|||
|
|||
### Defining Bundle Contributors |
|||
|
|||
All resources for used widgets in a page are added as a **bundle** (bundled & minified in production if you don't configure otherwise). In addition to adding a simple file, you can take full power of the bundle contributors. |
|||
|
|||
The sample code below does the same with the code above, but defines and uses bundle contributors: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget( |
|||
StyleTypes = new []{ typeof(MySimpleWidgetStyleBundleContributor) }, |
|||
ScriptTypes = new[]{ typeof(MySimpleWidgetScriptBundleContributor) } |
|||
)] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
|
|||
public class MySimpleWidgetStyleBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files |
|||
.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.css"); |
|||
} |
|||
} |
|||
|
|||
public class MySimpleWidgetScriptBundleContributor : BundleContributor |
|||
{ |
|||
public override void ConfigureBundle(BundleConfigurationContext context) |
|||
{ |
|||
context.Files |
|||
.AddIfNotContains("/Pages/Components/MySimpleWidget/Default.js"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
```` |
|||
|
|||
Bundle contribution system is very powerful. If your widget uses a JavaScript library to render a chart, then you can declare it as a dependency, so the JavaScript library is automatically added to the page if it wasn't added before. In this way, the page using your widget doesn't care about the dependencies. |
|||
|
|||
See the [bundling & minification](Bundling-Minification.md) documentation for more information about that system. |
|||
|
|||
## RefreshUrl |
|||
|
|||
A widget may design a `RefreshUrl` that is used whenever the widget needs to be refreshed. If it is defined, the widget is re-rendered on the server side on every refresh (see the refresh `method` of the `WidgetManager` below). |
|||
|
|||
````csharp |
|||
[Widget(RefreshUrl = "Widgets/Counters")] |
|||
public class CountersWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
Once you define a `RefreshUrl` for your widget, you need to provide an endpoint to render and return it: |
|||
|
|||
````csharp |
|||
[Route("Widgets")] |
|||
public class CountersWidgetController : AbpController |
|||
{ |
|||
[HttpGet] |
|||
[Route("Counters")] |
|||
public IActionResult Counters(DateTime startDate, DateTime endDate) |
|||
{ |
|||
return ViewComponent("CountersWidget", new {startDate, endDate}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`Widgets/Counters` route matches to the `RefreshUrl` declared before. |
|||
|
|||
> A widget supposed to be refreshed in two ways: In the first way, when you use a `RefreshUrl`, it re-rendered on the server and replaced by the HTML returned from server. In the second way the widget gets data (generally a JSON object) from server and refreshes itself in the client side (see the refresh method in the Widget JavaScript API section). |
|||
|
|||
## JavaScript API |
|||
|
|||
A widget may need to be rendered and refreshed in the client side. In such cases, you can use ABP's `WidgetManager` and define APIs for your widgets. |
|||
|
|||
### WidgetManager |
|||
|
|||
`WidgetManager` is used to initialize and refresh one or more widgets. Create a new `WidgetManager` as shown below: |
|||
|
|||
````js |
|||
$(function() { |
|||
var myWidgetManager = new abp.WidgetManager('#MyDashboardWidgetsArea'); |
|||
}) |
|||
```` |
|||
|
|||
`MyDashboardWidgetsArea` may contain one or more widgets inside. |
|||
|
|||
> Using the `WidgetManager` inside document.ready (like above) is a good practice since its functions use the DOM and need DOM to be ready. |
|||
|
|||
#### WidgetManager.init() |
|||
|
|||
`init` simply initializes the `WidgetManager` and calls `init` methods of the related widgets if they define (see Widget JavaScript API section below) |
|||
|
|||
```js |
|||
myWidgetManager.init(); |
|||
``` |
|||
|
|||
#### WidgetManager.refresh() |
|||
|
|||
`refresh` method refreshes all widgets related to this `WidgetManager`: |
|||
|
|||
```` |
|||
myWidgetManager.refresh(); |
|||
```` |
|||
|
|||
#### WidgetManager Options |
|||
|
|||
WidgetManager has some additional options. |
|||
|
|||
##### Filter Form |
|||
|
|||
If your widgets require parameters/filters then you will generally have a form to filter the widgets. In such cases, you can create a form that has some form elements and a dashboard area with some widgets inside. Example: |
|||
|
|||
````xml |
|||
<form method="get" id="MyDashboardFilterForm"> |
|||
...form elements |
|||
</form> |
|||
|
|||
<div id="MyDashboardWidgetsArea" data-widget-filter="#MyDashboardFilterForm"> |
|||
...widgets |
|||
</div> |
|||
```` |
|||
|
|||
`data-widget-filter` attribute relates the form with the widgets. Whenever the form is submitted, all the widgets are automatically refreshed with the form fields as the filter. |
|||
|
|||
Instead of the `data-widget-filter` attribute, you can use the `filterForm` parameter of the `WidgetManager` constructor. Example: |
|||
|
|||
````js |
|||
var myWidgetManager = new abp.WidgetManager({ |
|||
wrapper: '#MyDashboardWidgetsArea', |
|||
filterForm: '#MyDashboardFilterForm' |
|||
}); |
|||
```` |
|||
|
|||
##### Filter Callback |
|||
|
|||
You may want to have a better control to provide filters while initializing and refreshing the widgets. In this case, you can use the `filterCallback` option: |
|||
|
|||
````js |
|||
var myWidgetManager = new abp.WidgetManager({ |
|||
wrapper: '#MyDashboardWidgetsArea', |
|||
filterCallback: function() { |
|||
return $('#MyDashboardFilterForm').serializeFormToObject(); |
|||
} |
|||
}); |
|||
```` |
|||
|
|||
This example shows the default implementation of the `filterCallback`. You can return any JavaScript object with fields. Example: |
|||
|
|||
````js |
|||
filterCallback: function() { |
|||
return { |
|||
'startDate': $('#StartDateInput').val(), |
|||
'endDate': $('#EndDateInput').val() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
The returning filters are passed to all widgets on `init` and `refresh`. |
|||
|
|||
### Widget JavaScript API |
|||
|
|||
A widget can define a JavaScript API that is invoked by the `WidgetManager` when needed. The code sample below can be used to start to define an API for a widget. |
|||
|
|||
````js |
|||
(function () { |
|||
abp.widgets.NewUserStatisticWidget = function ($wrapper) { |
|||
|
|||
var getFilters = function () { |
|||
return { |
|||
... |
|||
}; |
|||
} |
|||
|
|||
var refresh = function (filters) { |
|||
... |
|||
}; |
|||
|
|||
var init = function (filters) { |
|||
... |
|||
}; |
|||
|
|||
return { |
|||
getFilters: getFilters, |
|||
init: init, |
|||
refresh: refresh |
|||
}; |
|||
}; |
|||
})(); |
|||
```` |
|||
|
|||
`NewUserStatisticWidget` is the name of the widget here. It should match the widget name defined in the server side. All of the functions are optional. |
|||
|
|||
#### getFilters |
|||
|
|||
If the widget has internal custom filters, this function should return the filter object. Example: |
|||
|
|||
````js |
|||
var getFilters = function() { |
|||
return { |
|||
frequency: $wrapper.find('.frequency-filter option:selected').val() |
|||
}; |
|||
} |
|||
```` |
|||
|
|||
This method is used by the `WidgetManager` while building filters. |
|||
|
|||
#### init |
|||
|
|||
Used to initialize the widget when needed. It has a filter argument that can be used while getting data from server. `init` method is used when `WidgetManager.init()` function is called. It is also called if your widget requires a full re-load on refresh. See the `RefreshUrl` widget option. |
|||
|
|||
#### refresh |
|||
|
|||
Used to refresh the widget when needed. It has a filter argument that can be used while getting data from server. `refresh` method is used whenever `WidgetManager.refresh()` function is called. |
|||
|
|||
## Authorization |
|||
|
|||
Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute: |
|||
|
|||
* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application). |
|||
* `RequiredPolicies` (`List<string>`): A list of policy names to authorize the user. See [the authorization document](../Authorization.md) for more info about policies. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace DashboardDemo.Web.Pages.Components.MySimpleWidget |
|||
{ |
|||
[Widget(RequiredPolicies = new[] { "MyPolicyName" })] |
|||
public class MySimpleWidgetViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## WidgetOptions |
|||
|
|||
As alternative to the `Widget` attribute, you can use the `AbpWidgetOptions` to configure widgets: |
|||
|
|||
```csharp |
|||
Configure<AbpWidgetOptions>(options => |
|||
{ |
|||
options.Widgets.Add<MySimpleWidgetViewComponent>(); |
|||
}); |
|||
``` |
|||
|
|||
Write this into the `ConfigureServices` method of your [module](../Module-Development-Basics.md). All the configuration done with the `Widget` attribute is also possible with the `AbpWidgetOptions`. Example configuration that adds a style for the widget: |
|||
|
|||
````csharp |
|||
Configure<AbpWidgetOptions>(options => |
|||
{ |
|||
options.Widgets |
|||
.Add<MySimpleWidgetViewComponent>() |
|||
.WithStyles("/Pages/Components/MySimpleWidget/Default.css"); |
|||
}); |
|||
```` |
|||
|
|||
> Tip: `AbpWidgetOptions` can also be used to get an existing widget and change its configuration. This is especially useful if you want to modify the configuration of a widget inside a module used by your application. Use `options.Widgets.Find` to get an existing `WidgetDefinition`. |
|||
|
|||
## See Also |
|||
|
|||
* [Example project (source code)](https://github.com/abpframework/abp/tree/dev/samples/DashboardDemo). |
|||
[Click to navigate to Widgets document](../UI/AspNetCore/Widgets.md) |
|||
|
|||
@ -1,3 +0,0 @@ |
|||
## AutoMapper Integration |
|||
|
|||
TODO |
|||
@ -1,3 +1,140 @@ |
|||
# Background Workers |
|||
|
|||
TODO |
|||
## Introduction |
|||
|
|||
Background workers are simple independent threads in the application running in the background. Generally, they run periodically to perform some tasks. Examples; |
|||
|
|||
* A background worker can run periodically to **delete old logs**. |
|||
* A background worker can run periodically to **determine inactive users** and **send emails** to get users to return to your application. |
|||
|
|||
|
|||
## Create a Background Worker |
|||
|
|||
A background worker should directly or indirectly implement the `IBackgroundWorker` interface. |
|||
|
|||
> A background worker is inherently [singleton](Dependency-Injection.md). So, only a single instance of your worker class is instantiated and run. |
|||
|
|||
### BackgroundWorkerBase |
|||
|
|||
`BackgroundWorkerBase` is an easy way to create a background worker. |
|||
|
|||
````csharp |
|||
public class MyWorker : BackgroundWorkerBase |
|||
{ |
|||
public override Task StartAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
//... |
|||
} |
|||
|
|||
public override Task StopAsync(CancellationToken cancellationToken = default) |
|||
{ |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Start your worker in the `StartAsync` (which is called when the application begins) and stop in the `StopAsync` (which is called when the application shuts down). |
|||
|
|||
> You can directly implement the `IBackgroundWorker`, but `BackgroundWorkerBase` provides some useful properties like `Logger`. |
|||
|
|||
### AsyncPeriodicBackgroundWorkerBase |
|||
|
|||
Assume that we want to make a user passive, if the user has not logged in to the application in last 30 days. `AsyncPeriodicBackgroundWorkerBase` class simplifies to create periodic workers, so we will use it for the example below: |
|||
|
|||
````csharp |
|||
public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
public PassiveUserCheckerWorker( |
|||
AbpTimer timer, |
|||
IServiceScopeFactory serviceScopeFactory |
|||
) : base( |
|||
timer, |
|||
serviceScopeFactory) |
|||
{ |
|||
Timer.Period = 600000; //10 minutes |
|||
} |
|||
|
|||
protected override async Task DoWorkAsync( |
|||
PeriodicBackgroundWorkerContext workerContext) |
|||
{ |
|||
Logger.LogInformation("Starting: Setting status of inactive users..."); |
|||
|
|||
//Resolve dependencies |
|||
var userRepository = workerContext |
|||
.ServiceProvider |
|||
.GetRequiredService<IUserRepository>(); |
|||
|
|||
//Do the work |
|||
await userRepository.UpdateInactiveUserStatusesAsync(); |
|||
|
|||
Logger.LogInformation("Completed: Setting status of inactive users..."); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `AsyncPeriodicBackgroundWorkerBase` uses the `AbpTimer` (a thread-safe timer) object to determine **the period**. We can set its `Period` property in the constructor. |
|||
* It required to implement the `DoWorkAsync` method to **execute** the periodic work. |
|||
* It is a good practice to **resolve dependencies** from the `PeriodicBackgroundWorkerContext` instead of constructor injection. Because `AsyncPeriodicBackgroundWorkerBase` uses a `IServiceScope` that is **disposed** when your work finishes. |
|||
* `AsyncPeriodicBackgroundWorkerBase` **catches and logs exceptions** thrown by the `DoWorkAsync` method. |
|||
|
|||
|
|||
## Register Background Worker |
|||
|
|||
After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class: |
|||
|
|||
````csharp |
|||
[DependsOn(typeof(AbpBackgroundWorkersModule))] |
|||
public class MyModule : AbpModule |
|||
{ |
|||
public override void OnApplicationInitialization( |
|||
ApplicationInitializationContext context) |
|||
{ |
|||
context.AddBackgroundWorker<PassiveUserCheckerWorker>(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`context.AddBackgroundWorker(...)` is a shortcut extension method for the expression below: |
|||
|
|||
````csharp |
|||
context.ServiceProvider |
|||
.GetRequiredService<IBackgroundWorkerManager>() |
|||
.Add( |
|||
context |
|||
.ServiceProvider |
|||
.GetRequiredService<PassiveUserCheckerWorker>() |
|||
); |
|||
```` |
|||
|
|||
So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`. |
|||
|
|||
While we generally add workers in `OnApplicationInitialization`, there are no restrictions on that. You can inject `IBackgroundWorkerManager` anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down. |
|||
|
|||
## Options |
|||
|
|||
`AbpBackgroundWorkerOptions` class is used to [set options](Options.md) for the background workers. Currently, there is only one option: |
|||
|
|||
* `IsEnabled` (default: true): Used to **enable/disable** the background worker system for your application. |
|||
|
|||
> See the [Options](Options.md) document to learn how to set options. |
|||
|
|||
## Making Your Application Always Run |
|||
|
|||
Background workers only work if your application is running. If you host the background job execution in your web application (this is the default behavior), you should ensure that your web application is configured to always be running. Otherwise, background jobs only work while your application is in use. |
|||
|
|||
## Running On a Cluster |
|||
|
|||
Be careful if you run multiple instances of your application simultaneously in a clustered environment. In that case, every application runs the same worker which may create conflicts if your workers are running on the same resources (processing the same data, for example). |
|||
|
|||
If that's a problem for your workers, you have two options; |
|||
|
|||
* Disable the background worker system using the `AbpBackgroundWorkerOptions` described above, for all the application instances, except one of them. |
|||
* Disable the background worker system for all the application instances and create another special application that runs on a single server and execute the workers. |
|||
|
|||
## Quartz Integration |
|||
|
|||
ABP Framework's background worker system is good to implement periodic tasks. However, you may want to use an advanced task scheduler like [Quartz](https://www.quartz-scheduler.net/). See the community contributed [quartz integration](Background-Workers-Quartz.md) for the background workers. |
|||
|
|||
## See Also |
|||
* [Quartz Integration for the background workers](Background-Workers-Quartz.md) |
|||
* [Background Jobs](Background-Jobs.md) |
|||
@ -0,0 +1,142 @@ |
|||
# ABP Framework v2.3.0 Has Been Released! |
|||
|
|||
In the days of **coronavirus**, we have released **ABP Framework v2.3** and this post will explain **what's new** with this release and **what we've done** in the last two weeks. |
|||
|
|||
## About the Coronavirus & Our Team |
|||
|
|||
**We are very sad** about the coronavirus case. As [Volosoft](https://volosoft.com/) team, we have **remote workers** working in their home in different countries. Beginning from the last week, we've **completely started to work remotely** from home including our main office employees. |
|||
|
|||
We believe in and pray for that the humanity will overcome this issue in a short time. |
|||
|
|||
## About the Release Cycle |
|||
|
|||
Beginning from the ABP v2.1.0, we have started to release feature versions once **in two weeks**, on Thursdays. This is the 3rd release after that decision and we see that it works fine for now and improved our agility. |
|||
|
|||
We will continue to release **feature versions** (like v2.4, v2.5) in every two weeks. In addition, we may release **hotfix versions** (like v2.3.1, v2.3.2) whenever needed. |
|||
|
|||
## What's New in ABP Framework v2.3.0 |
|||
|
|||
We've completed & merged **[104](https://github.com/abpframework/abp/milestone/30?closed=1) issues and pull requests** with **393 commits** in this two weeks development period. |
|||
|
|||
I will introduce some new features and enhancements introduced with this release. |
|||
|
|||
### React Native Mobile Application |
|||
|
|||
We have finally completed the **react native mobile application**. It currently allows you to **login**, manage your **users** and **tenants**. It utilizes the same setting, authorization and localization systems of the ABP Framework. |
|||
|
|||
A few screenshots from the application: |
|||
|
|||
 |
|||
|
|||
It doesn't have much functionality but it is a **perfect starting point** for your own mobile application since it is completely integrated to the backend and supports multi-tenancy. |
|||
|
|||
### Angular TypeScript Proxy Generator |
|||
|
|||
It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on he server side) and **model objects** (matches to [DTOs](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) in the server side). |
|||
|
|||
In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced: |
|||
|
|||
* It generates a **big, single** .ts file which has some problems; |
|||
* It get **too large** when your application grows. |
|||
* It doesn't fit into the **[modular](https://docs.abp.io/en/abp/latest/Module-Development-Basics) approach** of the ABP framework. |
|||
* It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). |
|||
* It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. |
|||
|
|||
So, we've decided to create an ABP CLI command to automatically generate the typescript client proxies ([#2222](https://github.com/abpframework/abp/issues/2222)) for your REST API developed with the ABP Framework. |
|||
|
|||
It is easy to use. Just run the following command in the **root folder** of the angular application: |
|||
|
|||
````bash |
|||
abp generate-proxy |
|||
```` |
|||
|
|||
It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](https://docs.abp.io/en/abp/latest/CLI). |
|||
|
|||
### CRUD Application Services for Entities with Composite Keys |
|||
|
|||
` CrudAppService ` is a useful base class to create CRUD application services for your entities. But it doesn't support entities with **composite primary keys**. `AbstractKeyCrudAppService` is the new base class that is developed to support entities with composite primary keys. See [the documentation](https://docs.abp.io/en/abp/latest/Application-Services#abstractkeycrudappservice) for more. |
|||
|
|||
### Add Source Code of the Modules |
|||
|
|||
The application startup template comes with some [application modules](https://docs.abp.io/en/abp/latest/Modules/Index) **pre-installed** as **NuGet & NPM packages**. This have a few important advantages: |
|||
|
|||
* You can **easily [upgrade](https://docs.abp.io/en/abp/latest/CLI#update)** these modules when a new version is available. |
|||
* Your solution becomes **cleaner**, so you can focus on your own code. |
|||
|
|||
However, when you need to make **major customizations** for a depended module, it is not easy as its source code is in your applications. To solve this problem, we've introduces a new command to the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) that **replaces** NuGet packages with their **source code** in your solution. The usage is simple: |
|||
|
|||
````bash |
|||
abp add-module --with-source-code |
|||
```` |
|||
|
|||
This command adds a module with source code or replaces with its source code if it is already added as package references. |
|||
|
|||
> It is suggested to **save your changes** to your source control system before using this command since it makes a lot of changes in your source code. |
|||
|
|||
In addition, we've documented how to customize depended modules without changing their source code (see the section below). It is suggested to use modules as packages to easily upgrade them in the future. |
|||
|
|||
> Source code of the free modules are licensed under **MIT**, so you can freely change them and add into your solution. |
|||
|
|||
### Switch to Preview |
|||
|
|||
ABP Framework is rapidly evolving and we are frequently releasing new versions. However, if you want to follow it closer, you can use the **daily preview packages**. |
|||
|
|||
We've created an ABP CLI command to easily **update to the latest preview packages** for your solution. Run the following command in the root folder of your solution: |
|||
|
|||
````bash |
|||
abp switch-to-preview |
|||
```` |
|||
|
|||
It will change the versions of all ABP related NuGet and NPM packages. You can **switch back to the latest stable** when you want: |
|||
|
|||
````bash |
|||
abp switch-to-stable |
|||
```` |
|||
|
|||
See the [ABP CLI document](https://docs.abp.io/en/abp/latest/CLI#switch-to-preview) fore more. |
|||
|
|||
### Documentation Improvements |
|||
|
|||
#### Extending/Customizing Depended Application Modules |
|||
|
|||
We've created a huge documentation that explains how to customize a depended module without changing its source code. See [the documentation](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide). |
|||
|
|||
In addition to the documentation, we've revised all the modules ([#3166](https://github.com/abpframework/abp/issues/3166)) to make their services easily extensible & customizable. |
|||
|
|||
#### EF Core Migration Guide |
|||
|
|||
We've recently created a guide to explain the migration system that is used by the ABP startup templates. [This guide](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations) also explains how to customize the migration structure, split your modules across multiple databases, reusing a module's table and son on. |
|||
|
|||
#### Migration from the ASP.NET Boilerplate |
|||
|
|||
If you have a solution built on the ASP.NET Boilerplate, we've [created a guide](https://docs.abp.io/en/abp/latest/AspNet-Boilerplate-Migration-Guide) that tries to help you if you want to migrate your solution to the new ABP Framework. |
|||
|
|||
### Some Other Features |
|||
|
|||
#### The Framework |
|||
|
|||
* Add `IRepository.GetAsync` and `IRepository.FindAsync` methods ([#3184](https://github.com/abpframework/abp/issues/3148)). |
|||
|
|||
#### Modules |
|||
|
|||
* Get password & email address of the admin while creating a new tenant, for the tenant management module ([#3088](https://github.com/abpframework/abp/issues/3088)). |
|||
* Elastic search integrated full text search for the docs module ([#2901](https://github.com/abpframework/abp/pull/2901)). |
|||
* New Quartz background worker module ([#2762](https://github.com/abpframework/abp/issues/2762)) |
|||
|
|||
#### Samples |
|||
|
|||
* Add multi-tenancy support to the microservice demo ([#3032](https://github.com/abpframework/abp/pull/3032)). |
|||
|
|||
See [the release notes](https://github.com/abpframework/abp/releases/tag/2.3.0) for all feature, enhancement and bugfixes. |
|||
|
|||
## What's Next? |
|||
|
|||
We have the following goals for the next few months: |
|||
|
|||
* Complete the **documentation and samples**, write more tutorials. |
|||
* Make the framework and existing modules more **customizable and extensible**. |
|||
* Integrate to **gRPC** & implement gRPC endpoint for pre-built modules ([#2882](https://github.com/abpframework/abp/issues/2882)). |
|||
* Create a **Blazor UI** for the ABP Framework & implement it for all the modules and startup templates ([#394](https://github.com/abpframework/abp/issues/394)). |
|||
* Add **new features** to pre-built modules and create new modules for the [ABP Commercial](https://commercial.abp.io/). |
|||
|
|||
See [the GitHub milestones](https://github.com/abpframework/abp/milestones) for details. |
|||
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 179 KiB |
@ -0,0 +1,177 @@ |
|||
# Customizing the Application Modules: Extending Entities |
|||
|
|||
In some cases, you may want to add some additional properties (and database fields) for an entity defined in a depended module. This section will cover some different approaches to make this possible. |
|||
|
|||
## Extra Properties |
|||
|
|||
[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
//SET AN EXTRA PROPERTY |
|||
var user = await _identityUserRepository.GetAsync(userId); |
|||
user.SetProperty("Title", "My custom title value!"); |
|||
await _identityUserRepository.UpdateAsync(user); |
|||
|
|||
//GET AN EXTRA PROPERTY |
|||
var user = await _identityUserRepository.GetAsync(userId); |
|||
return user.GetProperty<string>("Title"); |
|||
```` |
|||
|
|||
This approach is very easy to use and available out of the box. No extra code needed. You can store more than one property at the same time by using different property names (like `Title` here). |
|||
|
|||
Extra properties are stored as a single `JSON` formatted string value in the database for the EF Core. For MongoDB, they are stored as separate fields of the document. |
|||
|
|||
See the [entities document](Entities.md) for more about the extra properties system. |
|||
|
|||
> It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above. |
|||
|
|||
## Entity Extensions (EF Core) |
|||
|
|||
As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to; |
|||
|
|||
* Create **indexes** and **foreign keys** for an extra property. |
|||
* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example). |
|||
* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more). |
|||
|
|||
To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table. |
|||
|
|||
Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `ObjectExtensionManager`: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.MapEfCoreProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
b => { b.HasMaxLength(32); } |
|||
); |
|||
```` |
|||
|
|||
* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table). |
|||
* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties). |
|||
|
|||
> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEntityExtensions`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself. |
|||
|
|||
Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database. |
|||
|
|||
You can then use the same extra properties system defined in the previous section to manipulate the property over the entity. |
|||
|
|||
## Creating a New Entity Maps to the Same Database Table/Collection |
|||
|
|||
Another approach can be **creating your own entity** mapped to **the same database table** (or collection for a MongoDB database). |
|||
|
|||
`AppUser` entity in the [application startup template](Startup-Templates/Application.md) already implements this approach. [EF Core Migrations document](Entity-Framework-Core-Migrations.md) describes how to implement it and manage **EF Core database migrations** in such a case. It is also possible for MongoDB, while this time you won't deal with the database migration problems. |
|||
|
|||
## Creating a New Entity with Its Own Database Table/Collection |
|||
|
|||
Mapping your entity to an **existing table** of a depended module has a few disadvantages; |
|||
|
|||
* You deal with the **database migration structure** for EF Core. While it is possible, you should extra care about the migration code especially when you want to add **relations** between entities. |
|||
* Your application database and the module database will be the **same physical database**. Normally, a module database can be separated if needed, but using the same table restricts it. |
|||
|
|||
If you want to **loose couple** your entity with the entity defined by the module, you can create your own database table/collection and map your entity to your own table in your own database. |
|||
|
|||
In this case, you need to deal with the **synchronization problems**, especially if you want to **duplicate** some properties/fields of the related entity. There are a few solutions; |
|||
|
|||
* If you are building a **monolithic** application (or managing your entity and the related module entity within the same process), you can use the [local event bus](Local-Event-Bus.md) to listen changes. |
|||
* If you are building a **distributed** system where the module entity is managed (created/updated/deleted) on a different process/service than your entity is managed, then you can subscribe to the [distributed event bus](Distributed-Event-Bus.md) for change events. |
|||
|
|||
Once you handle the event, you can update your own entity in your own database. |
|||
|
|||
### Subscribing to Local Events |
|||
|
|||
[Local Event Bus](Local-Event-Bus.md) system is a way to publish and subscribe to events occurring in the same application. |
|||
|
|||
Assume that you want to get informed when a `IdentityUser` entity changes (created, updated or deleted). You can create a class that implements the `ILocalEventHandler<EntityChangedEventData<IdentityUser>>` interface. |
|||
|
|||
````csharp |
|||
public class MyLocalIdentityUserChangeEventHandler : |
|||
ILocalEventHandler<EntityChangedEventData<IdentityUser>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityChangedEventData<IdentityUser> eventData) |
|||
{ |
|||
var userId = eventData.Entity.Id; |
|||
var userName = eventData.Entity.UserName; |
|||
|
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `EntityChangedEventData<T>` covers create, update and delete events for the given entity. If you need, you can subscribe to create, update and delete events individually (in the same class or different classes). |
|||
* This code will be executed **out of the local transaction**, because it listens the `EntityChanged` event. You can subscribe to the `EntityChangingEventData<T>` to perform your event handler in **the same local (in-process) transaction** if the current [unit of work](Unit-Of-Work.md) is transactional. |
|||
|
|||
> Reminder: This approach needs to change the `IdentityUser` entity in the same process contains the handler class. It perfectly works even for a clustered environment (when multiple instances of the same application are running on multiple servers). |
|||
|
|||
### Subscribing to Distributed Events |
|||
|
|||
[Distributed Event Bus](Distributed-Event-Bus.md) system is a way to publish an event in one application and receive the event in the same or different application running on the same or different server. |
|||
|
|||
Assume that you want to get informed when a `IdentityUser` entity created, updated or deleted. You can create a class like below: |
|||
|
|||
````csharp |
|||
public class MyDistributedIdentityUserChangeEventHandler : |
|||
IDistributedEventHandler<EntityCreatedEto<EntityEto>>, |
|||
IDistributedEventHandler<EntityUpdatedEto<EntityEto>>, |
|||
IDistributedEventHandler<EntityDeletedEto<EntityEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityCreatedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "created" event |
|||
} |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityUpdatedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "updated" event |
|||
} |
|||
} |
|||
|
|||
public async Task HandleEventAsync(EntityDeletedEto<EntityEto> eventData) |
|||
{ |
|||
if (eventData.Entity.EntityType == "Volo.Abp.Identity.IdentityUser") |
|||
{ |
|||
var userId = Guid.Parse(eventData.Entity.KeysAsString); |
|||
//...handle the "deleted" event |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* It implements multiple `IDistributedEventHandler` interfaces: **Created**, **Updated** and **Deleted**. Because, the distributed event bus system publishes events individually. There is no "Changed" event like the local event bus. |
|||
* It subscribes to `EntityEto`, which is a generic event class that is **automatically published** for all type of entities by the ABP framework. This is why it checks the **entity type** (checking the entity type as string since we assume that there is no type safe reference to the `IdentityUser` entity). |
|||
|
|||
Pre-built application modules do not define specialized event types yet (like `IdentityUserEto` - "ETO" means "Event Transfer Object"). This feature is on the road map and will be available in a short term ([follow this issue](https://github.com/abpframework/abp/issues/3033)). Once it is implemented, you will be able to subscribe to individual entity types. Example: |
|||
|
|||
````csharp |
|||
public class MyDistributedIdentityUserCreatedEventHandler : |
|||
IDistributedEventHandler<EntityCreatedEto<IdentityUserEto>>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(EntityCreatedEto<IdentityUserEto> eventData) |
|||
{ |
|||
var userId = eventData.Entity.Id; |
|||
var userName = eventData.Entity.UserName; |
|||
//...handle the "created" event |
|||
} |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
* This handler is executed only when a new user has been created. |
|||
|
|||
> The only pre-defined specialized event class is the `UserEto`. For example, you can subscribe to the `EntityCreatedEto<UserEto>` to get notified when a user has created. This event also works for the Identity module. |
|||
|
|||
## See Also |
|||
|
|||
* [Migration System for the EF Core](Entity-Framework-Core-Migrations.md) |
|||
* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md) |
|||
@ -0,0 +1,265 @@ |
|||
# Customizing the Application Modules: Overriding Services |
|||
|
|||
You may need to **change behavior (business logic)** of a depended module for your application. In this case, you can use the power of the [dependency injection system](Dependency-Injection.md) to replace a service, controller or even a page model of the depended module by your own implementation. |
|||
|
|||
**Replacing a service** is possible for any type of class registered to the dependency injection, including services of the ABP Framework. |
|||
|
|||
You have different options can be used based on your requirement those will be explained in the next sections. |
|||
|
|||
> Notice that some service methods may not be virtual, so you may not be able to override. We make all virtual by design. If you find any method that is not overridable, please [create an issue](https://github.com/abpframework/abp/issues/new) or do it yourself and send a **pull request** on GitHub. |
|||
|
|||
## Replacing an Interface |
|||
|
|||
If given service defines an interface, like the `IdentityUserAppService` class implements the `IIdentityUserAppService`, you can re-implement the same interface and replace the current implementation by your class. Example: |
|||
|
|||
````csharp |
|||
public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
`MyIdentityUserAppService` replaces the `IIdentityUserAppService` by naming convention (since both ends with `IdentityUserAppService`). If your class name doesn't match, you need to manually expose the service interface: |
|||
|
|||
````csharp |
|||
[ExposeServices(typeof(IIdentityUserAppService))] |
|||
public class TestAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
The dependency injection system allows to register multiple services for the same interface. The last registered one is used when the interface is injected. It is a good practice to explicitly replace the service. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(IIdentityUserAppService))] |
|||
public class TestAppService : IIdentityUserAppService, ITransientDependency |
|||
{ |
|||
//... |
|||
} |
|||
```` |
|||
|
|||
In this way, there will be a single implementation of the `IIdentityUserAppService` interface, while it doesn't change the result for this case. Replacing a service is also possible by code: |
|||
|
|||
````csharp |
|||
context.Services.Replace( |
|||
ServiceDescriptor.Transient<IIdentityUserAppService, MyIdentityUserAppService>() |
|||
); |
|||
```` |
|||
|
|||
You can write this inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). |
|||
|
|||
## Overriding a Service Class |
|||
|
|||
In most cases, you will want to change one or a few methods of the current implementation for a service. Re-implementing the complete interface would not be efficient in this case. As a better approach, inherit from the original class and override the desired method. |
|||
|
|||
### Example: Overriding an Application Service |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
public class MyIdentityUserAppService : IdentityUserAppService |
|||
{ |
|||
//... |
|||
public MyIdentityUserAppService( |
|||
IdentityUserManager userManager, |
|||
IIdentityUserRepository userRepository, |
|||
IGuidGenerator guidGenerator |
|||
) : base( |
|||
userManager, |
|||
userRepository, |
|||
guidGenerator) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input) |
|||
{ |
|||
if (input.PhoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
throw new AbpValidationException( |
|||
"Phone number is required for new users!", |
|||
new List<ValidationResult> |
|||
{ |
|||
new ValidationResult( |
|||
"Phone number can not be empty!", |
|||
new []{"PhoneNumber"} |
|||
) |
|||
} |
|||
); } |
|||
|
|||
return await base.CreateAsync(input); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This class **overrides** the `CreateAsync` method of the `IdentityUserAppService` [application service](Application-Services.md) to check the phone number. Then calls the base method to continue to the **underlying business logic**. In this way, you can perform additional business logic **before** and **after** the base logic. |
|||
|
|||
You could completely **re-write** the entire business logic for a user creation without calling the base method. |
|||
|
|||
### Example: Overriding a Domain Service |
|||
|
|||
````csharp |
|||
[Dependency(ReplaceServices = true)] |
|||
[ExposeServices(typeof(IdentityUserManager))] |
|||
public class MyIdentityUserManager : IdentityUserManager |
|||
{ |
|||
public MyIdentityUserManager( |
|||
IdentityUserStore store, |
|||
IOptions<IdentityOptions> optionsAccessor, |
|||
IPasswordHasher<IdentityUser> passwordHasher, |
|||
IEnumerable<IUserValidator<IdentityUser>> userValidators, |
|||
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators, |
|||
ILookupNormalizer keyNormalizer, |
|||
IdentityErrorDescriber errors, |
|||
IServiceProvider services, |
|||
ILogger<IdentityUserManager> logger, |
|||
ICancellationTokenProvider cancellationTokenProvider |
|||
) : base( |
|||
store, |
|||
optionsAccessor, |
|||
passwordHasher, |
|||
userValidators, |
|||
passwordValidators, |
|||
keyNormalizer, |
|||
errors, |
|||
services, |
|||
logger, |
|||
cancellationTokenProvider) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<IdentityResult> CreateAsync(IdentityUser user) |
|||
{ |
|||
if (user.PhoneNumber.IsNullOrWhiteSpace()) |
|||
{ |
|||
throw new AbpValidationException( |
|||
"Phone number is required for new users!", |
|||
new List<ValidationResult> |
|||
{ |
|||
new ValidationResult( |
|||
"Phone number can not be empty!", |
|||
new []{"PhoneNumber"} |
|||
) |
|||
} |
|||
); |
|||
} |
|||
|
|||
return await base.CreateAsync(user); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example class inherits from the `IdentityUserManager` [domain service](Domain-Services.md) and overrides the `CreateAsync` method to perform the same phone number check implemented above. The result is same, but this time we've implemented it inside the domain service assuming that this is a **core domain logic** for our system. |
|||
|
|||
> `[ExposeServices(typeof(IdentityUserManager))]` attribute is **required** here since `IdentityUserManager` does not define an interface (like `IIdentityUserManager`) and dependency injection system doesn't expose services for inherited classes (like it does for the implemented interfaces) by convention. |
|||
|
|||
Check the [localization system](Localization.md) to learn how to localize the error messages. |
|||
|
|||
### Overriding Other Classes |
|||
|
|||
Overriding controllers, framework services, view component classes and any other type of classes registered to dependency injection can be overridden just like the examples above. |
|||
|
|||
## Extending Data Transfer Objects |
|||
|
|||
**Extending [entities](Entities.md)** is possible as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md). In this way, you can add **custom properties** to entities and perform **additional business logic** by overriding the related services as described above. |
|||
|
|||
It is also possible to extend Data Transfer Objects (**DTOs**) used by the application services. In this way, you can get extra properties from the UI (or client) and return extra properties from the service. |
|||
|
|||
### Example |
|||
|
|||
Assuming that you've already added a `SocialSecurityNumber` as described in the [Extending Entities document](Customizing-Application-Modules-Extending-Entities.md) and want to include this information while getting the list of users from the `GetListAsync` method of the `IdentityUserAppService`. |
|||
|
|||
You can use the [object extension system](Object-Extensions.md) to add the property to the `IdentityUserDto`. Write this code inside the `YourProjectNameDtoExtensions` class comes with the application startup template: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUserDto, string>( |
|||
"SocialSecurityNumber" |
|||
); |
|||
```` |
|||
|
|||
This code defines a `SocialSecurityNumber` to the `IdentityUserDto` class as a `string` type. That's all. Now, if you call the `/api/identity/users` HTTP API (which uses the `IdentityUserAppService` internally) from a REST API client, you will see the `SocialSecurityNumber` value in the `extraProperties` section. |
|||
|
|||
````json |
|||
{ |
|||
"totalCount": 1, |
|||
"items": [{ |
|||
"tenantId": null, |
|||
"userName": "admin", |
|||
"name": "admin", |
|||
"surname": null, |
|||
"email": "admin@abp.io", |
|||
"emailConfirmed": false, |
|||
"phoneNumber": null, |
|||
"phoneNumberConfirmed": false, |
|||
"twoFactorEnabled": false, |
|||
"lockoutEnabled": true, |
|||
"lockoutEnd": null, |
|||
"concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c", |
|||
"isDeleted": false, |
|||
"deleterId": null, |
|||
"deletionTime": null, |
|||
"lastModificationTime": "2020-04-09T21:25:47.0740706", |
|||
"lastModifierId": null, |
|||
"creationTime": "2020-04-09T21:25:46.8308744", |
|||
"creatorId": null, |
|||
"id": "8edecb8f-1894-a9b1-833b-39f4725db2a3", |
|||
"extraProperties": { |
|||
"SocialSecurityNumber": "123456789" |
|||
} |
|||
}] |
|||
} |
|||
```` |
|||
|
|||
Manually added the `123456789` value to the database for now. |
|||
|
|||
All pre-built modules support extra properties in their DTOs, so you can configure easily. |
|||
|
|||
### Definition Check |
|||
|
|||
When you [define](Customizing-Application-Modules-Extending-Entities.md) an extra property for an entity, it doesn't automatically appear in all the related DTOs, because of the security. The extra property may contain a sensitive data and you may not want to expose it to the clients by default. |
|||
|
|||
So, you need to explicitly define the same property for the corresponding DTO if you want to make it available for the DTO (as just done above). If you want to allow to set it on user creation, you also need to define it for the `IdentityUserCreateDto`. |
|||
|
|||
If the property is not so secure, this can be tedious. Object extension system allows you to ignore this definition check for a desired property. See the example below: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<IdentityUser, string>( |
|||
"SocialSecurityNumber", |
|||
options => |
|||
{ |
|||
options.MapEfCore(b => b.HasMaxLength(32)); |
|||
options.CheckPairDefinitionOnMapping = false; |
|||
} |
|||
); |
|||
```` |
|||
|
|||
This is another approach to define a property for an entity (`ObjectExtensionManager` has more, see [its document](Object-Extensions.md)). This time, we set `CheckPairDefinitionOnMapping` to false to skip definition check while mapping entities to DTOs and vice verse. |
|||
|
|||
If you don't like this approach but want to add a single property to multiple objects (DTOs) easier, `AddOrUpdateProperty` can get an array of types to add the extra property: |
|||
|
|||
````csharp |
|||
ObjectExtensionManager.Instance |
|||
.AddOrUpdateProperty<string>( |
|||
new[] |
|||
{ |
|||
typeof(IdentityUserDto), |
|||
typeof(IdentityUserCreateDto), |
|||
typeof(IdentityUserUpdateDto) |
|||
}, |
|||
"SocialSecurityNumber" |
|||
); |
|||
```` |
|||
|
|||
### About the User Interface |
|||
|
|||
This system allows you to add extra properties to entities and DTOs and execute custom business code, however it does nothing related to the User Interface. |
|||
|
|||
See [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) guide for the UI part. |
|||
|
|||
## How to Find the Services? |
|||
|
|||
[Module documents](Modules/Index.md) includes the list of the major services they define. In addition, you can investigate [their source code](https://github.com/abpframework/abp/tree/dev/modules) to explore all the services. |
|||
@ -0,0 +1,6 @@ |
|||
# Overriding the User Interface |
|||
|
|||
You may want to override a page, a component, a JavaScript, CSS or an image file of your depended module. Overriding the UI completely depends on the UI framework you're using. Select the UI framework to continue: |
|||
|
|||
* [ASP.NET Core (MVC / Razor Pages)](UI/AspNetCore/Customization-User-Interface.md) |
|||
* [Angular](UI/Angular/Customization-User-Interface.md) |
|||
@ -0,0 +1,6 @@ |
|||
# Getting Started with the Startup Templates |
|||
|
|||
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates: |
|||
|
|||
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started-AspNetCore-MVC-Template.md) |
|||
* [Getting Started with the Angular UI](Getting-Started-Angular-Template.md) |
|||
@ -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,3 @@ |
|||
# IdentityServer Integration |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# Account Module |
|||
|
|||
TODO |
|||
@ -1,3 +1,3 @@ |
|||
# IdentityServer Module |
|||
# Blogging Module |
|||
|
|||
TODO |
|||
@ -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,3 @@ |
|||
## Basic Theme |
|||
|
|||
TODO |
|||
@ -0,0 +1,85 @@ |
|||
## Component Replacement |
|||
|
|||
You can replace some ABP components with your custom components. |
|||
|
|||
The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_. |
|||
|
|||
### How to Replace a Component |
|||
|
|||
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. |
|||
|
|||
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: |
|||
|
|||
```js |
|||
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action |
|||
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum |
|||
import { Store } from '@ngxs/store'; // imported Store |
|||
//... |
|||
export class AppComponent { |
|||
constructor(..., private store: Store) {} // injected Store |
|||
|
|||
ngOnInit() { |
|||
this.store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: YourNewRoleComponent, |
|||
key: eIdentityComponents.Roles, |
|||
}), |
|||
); |
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
 |
|||
|
|||
|
|||
### How to Replace a Layout |
|||
|
|||
Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced with the same way. |
|||
|
|||
> A layout component template should contain `<router-outlet></router-outlet>` element. |
|||
|
|||
The below example describes how to replace the `ApplicationLayoutComponent`: |
|||
|
|||
Run the following command to generate a layout in `angular` folder: |
|||
|
|||
```bash |
|||
yarn ng generate component shared/my-application-layout --export --entryComponent |
|||
|
|||
# You don't need the --entryComponent option in Angular 9 |
|||
``` |
|||
|
|||
Add the following code in your layout template (`my-layout.component.html`) where you want the page to be loaded. |
|||
|
|||
```html |
|||
<router-outlet></router-outlet> |
|||
``` |
|||
|
|||
Open the `app.component.ts` and add the below content: |
|||
|
|||
```js |
|||
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent |
|||
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys |
|||
import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent |
|||
import { Store } from '@ngxs/store'; // imported Store |
|||
//... |
|||
export class AppComponent { |
|||
constructor(..., private store: Store) {} // injected Store |
|||
|
|||
ngOnInit() { |
|||
// added below content |
|||
this.store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: MyApplicationLayoutComponent, |
|||
key: eThemeBasicComponents.ApplicationLayout, |
|||
}), |
|||
); |
|||
|
|||
//... |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## What's Next? |
|||
|
|||
- [Custom Setting Page](./Custom-Setting-Page.md) |
|||
@ -0,0 +1,294 @@ |
|||
# Config State |
|||
|
|||
`ConfigStateService` is a singleton service, i.e. provided in root level of your application, and is actually a façade for interacting with application configuration state in the `Store`. |
|||
|
|||
## Before Use |
|||
|
|||
In order to use the `ConfigStateService` you must inject it in your class as a dependency. |
|||
|
|||
```js |
|||
import { ConfigStateService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
constructor(private config: ConfigStateService) {} |
|||
} |
|||
``` |
|||
|
|||
You do not have to provide the `ConfigStateService` at module or component/directive level, because it is already **provided in root**. |
|||
|
|||
## Selector Methods |
|||
|
|||
`ConfigStateService` has numerous selector methods which allow you to get a specific configuration or all configurations from the `Store`. |
|||
|
|||
### How to Get All Configurations From the Store |
|||
|
|||
You can use the `getAll` method of `ConfigStateService` to get all of the configuration object from the store. It is used as follows: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const config = this.config.getAll(); |
|||
``` |
|||
|
|||
### How to Get a Specific Configuration From the Store |
|||
|
|||
You can use the `getOne` method of `ConfigStateService` to get a specific configuration property from the store. For that, the property name should be passed to the method as parameter. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const currentUser = this.config.getOne("currentUser"); |
|||
``` |
|||
|
|||
On occasion, you will probably want to be more specific than getting just the current user. For example, here is how you can get the `tenantId`: |
|||
|
|||
```js |
|||
const tenantId = this.config.getDeep("currentUser.tenantId"); |
|||
``` |
|||
|
|||
or by giving an array of keys as parameter: |
|||
|
|||
```js |
|||
const tenantId = this.config.getDeep(["currentUser", "tenantId"]); |
|||
``` |
|||
|
|||
FYI, `getDeep` is able to do everything `getOne` does. Just keep in mind that `getOne` is slightly faster. |
|||
|
|||
#### Config State Properties |
|||
|
|||
Please refer to `Config.State` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L7). |
|||
|
|||
### How to Get the Application Information From the Store |
|||
|
|||
The `getApplicationInfo` method is used to get the application information from the environment variables stored as the config state. This is how you can use it: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const appInfo = this.config.getApplicationInfo(); |
|||
``` |
|||
|
|||
This method never returns `undefined` or `null` and returns an empty object literal (`{}`) instead. In other words, you will never get an error when referring to the properties of `appInfo` above. |
|||
|
|||
#### Application Information Properties |
|||
|
|||
Please refer to `Config.Application` type for all the properties you can get with `getApplicationInfo`. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L21). |
|||
|
|||
### How to Get API URL From the Store |
|||
|
|||
The `getApplicationInfo` method is used to get a specific API URL from the environment variables stored as the config state. This is how you can use it: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const apiUrl = this.config.getApiUrl(); |
|||
// environment.apis.default.url |
|||
|
|||
const searchUrl = this.config.getApiUrl("search"); |
|||
// environment.apis.search.url |
|||
``` |
|||
|
|||
This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used. |
|||
|
|||
### How to Get All Settings From the Store |
|||
|
|||
You can use the `getSettings` method of `ConfigStateService` to get all of the settings object from the configuration state. Here is how you get all settings: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const settings = this.config.getSettings(); |
|||
``` |
|||
|
|||
In addition, the method lets you search settings by **passing a keyword** to it. |
|||
|
|||
```js |
|||
const localizationSettings = this.config.getSettings("Localization"); |
|||
/* |
|||
{ |
|||
'Abp.Localization.DefaultLanguage': 'en' |
|||
} |
|||
*/ |
|||
``` |
|||
|
|||
Beware though, **settings search is case sensitive**. |
|||
|
|||
### How to Get a Specific Setting From the Store |
|||
|
|||
You can use the `getSetting` method of `ConfigStateService` to get a specific setting from the configuration state. Here is an example: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const defaultLang = this.config.getSetting("Abp.Localization.DefaultLanguage"); |
|||
// 'en' |
|||
``` |
|||
|
|||
### How to Get a Specific Permission From the Store |
|||
|
|||
You can use the `getGrantedPolicy` method of `ConfigStateService` to get a specific permission from the configuration state. For that, you should pass a policy key as parameter to the method. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const hasIdentityPermission = this.config.getGrantedPolicy("Abp.Identity"); |
|||
// true |
|||
``` |
|||
|
|||
You may also **combine policy keys** to fine tune your selection: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const hasIdentityAndAccountPermission = this.config.getGrantedPolicy( |
|||
"Abp.Identity && Abp.Account" |
|||
); |
|||
// false |
|||
|
|||
const hasIdentityOrAccountPermission = this.config.getGrantedPolicy( |
|||
"Abp.Identity || Abp.Account" |
|||
); |
|||
// true |
|||
``` |
|||
|
|||
Please consider the following **rules** when creating your permission selectors: |
|||
|
|||
- Maximum 2 keys can be combined. |
|||
- `&&` operator looks for both keys. |
|||
- `||` operator looks for either key. |
|||
- Empty string `''` as key will return `true` |
|||
- Using an operator without a second key will return `false` |
|||
|
|||
### How to Get Translations From the Store |
|||
|
|||
The `getLocalization` method of `ConfigStateService` is used for translations. Here are some examples: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const identity = this.config.getLocalization("AbpIdentity::Identity"); |
|||
// 'identity' |
|||
|
|||
const notFound = this.config.getLocalization("AbpIdentity::IDENTITY"); |
|||
// 'AbpIdentity::IDENTITY' |
|||
|
|||
const defaultValue = this.config.getLocalization({ |
|||
key: "AbpIdentity::IDENTITY", |
|||
defaultValue: "IDENTITY" |
|||
}); |
|||
// 'IDENTITY' |
|||
``` |
|||
|
|||
Please check out the [localization documentation](./Localization.md) for details. |
|||
|
|||
## Dispatch Methods |
|||
|
|||
`ConfigStateService` has several dispatch methods which allow you to conveniently dispatch predefined actions to the `Store`. |
|||
|
|||
### How to Get Application Configuration From Server |
|||
|
|||
The `dispatchGetAppConfiguration` triggers a request to an endpoint that responds with the application state and then places this response to the `Store` as configuration state. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
this.config.dispatchGetAppConfiguration(); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
Note that **you do not have to call this method at application initiation**, because the application configuration is already being received from the server at start. |
|||
|
|||
### How to Patch Route Configuration |
|||
|
|||
The `dispatchPatchRouteByName` finds a route by its name and replaces its configuration in the `Store` with the new configuration passed as the second parameter. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const newRouteConfig: Partial<ABP.Route> = { |
|||
name: "Home", |
|||
path: "home", |
|||
children: [ |
|||
{ |
|||
name: "Dashboard", |
|||
path: "dashboard" |
|||
} |
|||
] |
|||
}; |
|||
|
|||
this.config.dispatchPatchRouteByName("::Menu:Home", newRouteConfig); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
### How to Add a New Route Configuration |
|||
|
|||
The `dispatchAddRoute` adds a new route to the configuration state in the `Store`. For this, the route config should be passed as the parameter of the method. |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const newRoute: ABP.Route = { |
|||
name: "My New Page", |
|||
iconClass: "fa fa-dashboard", |
|||
path: "page", |
|||
invisible: false, |
|||
order: 2, |
|||
requiredPolicy: "MyProjectName::MyNewPage" |
|||
}; |
|||
|
|||
this.config.dispatchAddRoute(newRoute); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
The `newRoute` will be placed as at root level, i.e. without any parent routes and its url will be stored as `'/path'`. |
|||
|
|||
If you want **to add a child route, you can do this:** |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
const newRoute: ABP.Route = { |
|||
parentName: "AbpAccount::Login", |
|||
name: "My New Page", |
|||
iconClass: "fa fa-dashboard", |
|||
path: "page", |
|||
invisible: false, |
|||
order: 2, |
|||
requiredPolicy: "MyProjectName::MyNewPage" |
|||
}; |
|||
|
|||
this.config.dispatchAddRoute(newRoute); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
The `newRoute` will then be placed as a child of the parent route named `'AbpAccount::Login'` and its url will be set as `'/account/login/page'`. |
|||
|
|||
#### Route Configuration Properties |
|||
|
|||
Please refer to `ABP.Route` type for all the properties you can pass to `dispatchSetEnvironment` in its parameter. It can be found in the [common.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/common.ts#L27). |
|||
|
|||
### How to Set the Environment |
|||
|
|||
The `dispatchSetEnvironment` places environment variables passed to it in the `Store` under the configuration state. Here is how it is used: |
|||
|
|||
```js |
|||
// this.config is instance of ConfigStateService |
|||
|
|||
this.config.dispatchSetEnvironment({ |
|||
/* environment properties here */ |
|||
}); |
|||
// returns a state stream which emits after dispatch action is complete |
|||
``` |
|||
|
|||
Note that **you do not have to call this method at application initiation**, because the environment variables are already being stored at start. |
|||
|
|||
#### Environment Properties |
|||
|
|||
Please refer to `Config.Environment` type for all the properties you can pass to `dispatchSetEnvironment` as parameter. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13). |
|||
|
|||
## What's Next? |
|||
|
|||
* [Component Replacement](./Component-Replacement.md) |
|||
@ -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,46 @@ |
|||
# Custom Setting Page |
|||
|
|||
There are several settings tabs from different modules. You can add custom settings page to your project in 3 steps. |
|||
|
|||
1. Create a Component |
|||
|
|||
```js |
|||
import { Select } from '@ngxs/store'; |
|||
import { Component } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'app-your-custom-settings', |
|||
template: ` |
|||
custom-settings works! |
|||
`, |
|||
}) |
|||
export class YourCustomSettingsComponent { |
|||
// Your component logic |
|||
} |
|||
``` |
|||
|
|||
2. Add the `YourCustomSettingsComponent` to `declarations` and the `entryComponents` arrays in the `AppModule`. |
|||
|
|||
3. Open the `app.component.ts` and add the below content to the `ngOnInit` |
|||
|
|||
```js |
|||
import { addSettingTab } from '@abp/ng.theme.shared'; |
|||
// ... |
|||
|
|||
ngOnInit() { |
|||
addSettingTab({ |
|||
component: YourCustomSettingsComponent, |
|||
name: 'Type here the setting tab title (you can type a localization key, e.g: AbpAccount::Login', |
|||
order: 4, |
|||
requiredPolicy: 'type here a policy key' |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
Navigate to `/setting-management` route to see the changes: |
|||
|
|||
 |
|||
|
|||
## What's Next? |
|||
|
|||
- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md) |
|||
@ -0,0 +1,3 @@ |
|||
# Angular User Interface Customization Guide |
|||
|
|||
* [Replacing a component](Component-Replacement.md) |
|||
@ -0,0 +1,130 @@ |
|||
# 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() { |
|||
const scriptElement = this.domInsertionService.insertContent( |
|||
CONTENT_STRATEGY.AppendScriptToBody('alert()') |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>` and `scriptElement` will be an `HTMLScriptElement`. |
|||
|
|||
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
|||
|
|||
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. |
|||
|
|||
### 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() { |
|||
const styleElement = 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>` and `styleElement` will be an `HTMLStyleElement`. |
|||
|
|||
Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy. |
|||
|
|||
> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method. |
|||
|
|||
### How to Remove Inserted Scripts & Styles |
|||
|
|||
If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element. |
|||
|
|||
```js |
|||
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
private styleElement: HTMLStyleElement; |
|||
|
|||
constructor(private domInsertionService: DomInsertionService) {} |
|||
|
|||
ngOnInit() { |
|||
this.styleElement = this.domInsertionService.insertContent( |
|||
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}') |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.domInsertionService.removeContent(this.styleElement); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
In the example above, `<style>body {margin: 0;}</style>` element **will be removed** from `<head>` when the component is destroyed. |
|||
|
|||
## API |
|||
|
|||
### insertContent |
|||
|
|||
```js |
|||
insertContent<T extends HTMLScriptElement | HTMLStyleElement>( |
|||
contentStrategy: ContentStrategy<T>, |
|||
): T |
|||
``` |
|||
|
|||
- `contentStrategy` parameter is the primary focus here and is explained above. |
|||
- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy. |
|||
|
|||
### removeContent |
|||
|
|||
```js |
|||
removeContent(element: HTMLScriptElement | HTMLStyleElement): void |
|||
``` |
|||
|
|||
- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method. |
|||
|
|||
## 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,209 @@ |
|||
# How to Make HTTP Requests |
|||
|
|||
|
|||
|
|||
## About HttpClient |
|||
|
|||
Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project. |
|||
|
|||
However, `HttpClient` leaves error handling to the caller (method). In other words, HTTP errors are handled manually and by hooking into the observer of the `Observable` returned. |
|||
|
|||
```js |
|||
getConfig() { |
|||
this.http.get(this.configUrl).subscribe( |
|||
config => this.updateConfig(config), |
|||
error => { |
|||
// Handle error here |
|||
}, |
|||
); |
|||
} |
|||
``` |
|||
|
|||
Although clear and flexible, handling errors this way is repetitive work, even when error processing is delegated to the store or any other injectable. |
|||
|
|||
An `HttpInterceptor` is able to catch `HttpErrorResponse` and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details. |
|||
|
|||
|
|||
|
|||
## RestService |
|||
|
|||
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult. |
|||
|
|||
|
|||
|
|||
### Getting Started with RestService |
|||
|
|||
In order to use the `RestService`, you must inject it in your class as a dependency. |
|||
|
|||
```js |
|||
import { RestService } from '@abp/ng.core'; |
|||
|
|||
@Injectable({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoService { |
|||
constructor(private rest: RestService) {} |
|||
} |
|||
``` |
|||
|
|||
You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**. |
|||
|
|||
|
|||
|
|||
### How to Make a Request with RestService |
|||
|
|||
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example: |
|||
|
|||
```js |
|||
getFoo(id: number) { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/some/path/to/foo/' + id, |
|||
}; |
|||
|
|||
return this.rest.request<null, FooResponse>(request); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
The `request` method always returns an `Observable<T>`. Therefore you can do the following wherever you use `getFoo` method: |
|||
|
|||
```js |
|||
doSomethingWithFoo(id: number) { |
|||
this.demoService.getFoo(id).subscribe( |
|||
foo => { |
|||
// Do something with foo. |
|||
} |
|||
) |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error. |
|||
|
|||
|
|||
|
|||
As you see, `request` method gets a request options object with `Rest.Request<T>` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one: |
|||
|
|||
```js |
|||
postFoo(body: Foo) { |
|||
const request: Rest.Request<Foo> = { |
|||
method: 'POST', |
|||
url: '/api/some/path/to/foo', |
|||
body |
|||
}; |
|||
|
|||
return this.rest.request<Foo, FooResponse>(request); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few chages compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular. |
|||
|
|||
|
|||
|
|||
### How to Disable Default Error Handler of RestService |
|||
|
|||
The `request` method, used with defaults, always handles errors. Let's see how you can change that behavior and handle errors yourself: |
|||
|
|||
```js |
|||
deleteFoo(id: number) { |
|||
const request: Rest.Request<null> = { |
|||
method: 'DELETE', |
|||
url: '/api/some/path/to/foo/' + id, |
|||
}; |
|||
|
|||
return this.rest.request<null, void>(request, { skipHandleError: true }); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription. |
|||
|
|||
```js |
|||
removeFooFromList(id: number) { |
|||
this.demoService.deleteFoo(id).subscribe( |
|||
foo => { |
|||
// Do something with foo. |
|||
}, |
|||
error => { |
|||
// Do something with error. |
|||
} |
|||
) |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### How to Get a Specific API Endpoint From Application Config |
|||
|
|||
Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration. |
|||
|
|||
|
|||
|
|||
```js |
|||
putFoo(body: Foo, id: string) { |
|||
const request: Rest.Request<Foo> = { |
|||
method: 'PUT', |
|||
url: '/' + id, |
|||
body |
|||
}; |
|||
|
|||
return this.rest.request<Foo, void>(request, {apiName: 'foo'}); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows: |
|||
|
|||
```js |
|||
// environment.ts |
|||
|
|||
export const environment = { |
|||
apis: { |
|||
default: { |
|||
url: 'https://localhost:44305', |
|||
}, |
|||
foo: { |
|||
url: 'https://localhost:44305/api/some/path/to/foo', |
|||
}, |
|||
}, |
|||
|
|||
/* rest of the environment variables here */ |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### How to Observe Response Object or HTTP Events Instead of Body |
|||
|
|||
`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object. |
|||
|
|||
```js |
|||
getSomeCustomHeaderValue() { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/some/path/that/sends/some-custom-header', |
|||
}; |
|||
|
|||
return this.rest.request<null, HttpResponse<any>>( |
|||
request, |
|||
{observe: Rest.Observe.Response}, |
|||
).pipe( |
|||
map(response => response.headers.get('Some-Custom-Header')) |
|||
); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). |
|||
|
|||
## What's Next? |
|||
|
|||
* [Localization](./Localization.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) |
|||
@ -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,140 @@ |
|||
# Localization |
|||
|
|||
Before you read about _the Localization Pipe_ and _the Localization Service_, you should know about localization keys. |
|||
|
|||
The Localization key format consists of 2 sections which are **Resource Name** and **Key**. |
|||
`ResourceName::Key` |
|||
|
|||
> If you do not specify the resource name, it will be `defaultResourceName` which is declared in `environment.ts` |
|||
|
|||
```js |
|||
const environment = { |
|||
//... |
|||
localization: { |
|||
defaultResourceName: 'MyProjectName', |
|||
}, |
|||
}; |
|||
``` |
|||
|
|||
So these two are the same: |
|||
|
|||
```html |
|||
<h1>{%{{{ '::Key' | abpLocalization }}}%}</h1> |
|||
|
|||
<h1>{%{{{ 'MyProjectName::Key' | abpLocalization }}}%}</h1> |
|||
``` |
|||
|
|||
## Using the Localization Pipe |
|||
|
|||
You can use the `abpLocalization` pipe to get localized text as in this example: |
|||
|
|||
```html |
|||
<h1>{%{{{ 'Resource::Key' | abpLocalization }}}%}</h1> |
|||
``` |
|||
|
|||
The pipe will replace the key with the localized text. |
|||
|
|||
You can also specify a default value as shown below: |
|||
|
|||
```html |
|||
<h1>{%{{{ { key: 'Resource::Key', defaultValue: 'Default Value' } | abpLocalization }}}%}</h1> |
|||
``` |
|||
|
|||
To use interpolation, you must give the values for interpolation as pipe parameters, for example: |
|||
|
|||
Localization data is stored in key-value pairs: |
|||
|
|||
```js |
|||
{ |
|||
//... |
|||
AbpAccount: { // AbpAccount is the resource name |
|||
Key: "Value", |
|||
PagerInfo: "Showing {0} to {1} of {2} entries" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
So we can use this key like this: |
|||
|
|||
```html |
|||
<h1>{%{{{ 'AbpAccount::PagerInfo' | abpLocalization:'20':'30':'50' }}}%}</h1> |
|||
|
|||
<!-- Output: Showing 20 to 30 of 50 entries --> |
|||
``` |
|||
|
|||
### Using the Localization Service |
|||
|
|||
First of all you should import the `LocalizationService` from **@abp/ng.core** |
|||
|
|||
```js |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
|
|||
class MyClass { |
|||
constructor(private localizationService: LocalizationService) {} |
|||
} |
|||
``` |
|||
|
|||
After that, you are able to use localization service. |
|||
|
|||
> You can add interpolation parameters as arguments to `instant()` and `get()` methods. |
|||
|
|||
```js |
|||
this.localizationService.instant('AbpIdentity::UserDeletionConfirmation', 'John'); |
|||
|
|||
// with fallback value |
|||
this.localizationService.instant( |
|||
{ key: 'AbpIdentity::UserDeletionConfirmation', defaultValue: 'Default Value' }, |
|||
'John', |
|||
); |
|||
|
|||
// Output |
|||
// User 'John' will be deleted. Do you confirm that? |
|||
``` |
|||
|
|||
To get a localized text as [_Observable_](https://rxjs.dev/guide/observable) use `get` method instead of `instant`: |
|||
|
|||
```js |
|||
this.localizationService.get('Resource::Key'); |
|||
|
|||
// with fallback value |
|||
this.localizationService.get({ key: 'Resource::Key', defaultValue: 'Default Value' }); |
|||
``` |
|||
|
|||
### Using the Config State |
|||
|
|||
In order to you `getLocalization` method you should import ConfigState. |
|||
|
|||
```js |
|||
import { ConfigState } from '@abp/ng.core'; |
|||
``` |
|||
|
|||
Then you can use it as followed: |
|||
|
|||
```js |
|||
this.store.selectSnapshot(ConfigState.getLocalization('ResourceName::Key')); |
|||
``` |
|||
|
|||
`getLocalization` method can be used with both `localization key` and [`LocalizationWithDefault`](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L34) interface. |
|||
|
|||
```js |
|||
this.store.selectSnapshot( |
|||
ConfigState.getLocalization( |
|||
{ |
|||
key: 'AbpIdentity::UserDeletionConfirmation', |
|||
defaultValue: 'Default Value', |
|||
}, |
|||
'John', |
|||
), |
|||
); |
|||
``` |
|||
|
|||
Localization resources are stored in the `localization` property of `ConfigState`. |
|||
|
|||
|
|||
## See Also |
|||
|
|||
* [Localization in ASP.NET Core](../../Localization.md) |
|||
|
|||
## What's Next? |
|||
|
|||
* [Permission Management](./Permission-Management.md) |
|||
@ -0,0 +1,79 @@ |
|||
# Permission Management |
|||
|
|||
A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../Authorization.md) document. |
|||
|
|||
You can get permission of authenticated user using `getGrantedPolicy` selector of `ConfigState`. |
|||
|
|||
You can get permission as boolean value from store: |
|||
|
|||
```js |
|||
import { Store } from '@ngxs/store'; |
|||
import { ConfigState } from '../states'; |
|||
|
|||
export class YourComponent { |
|||
constructor(private store: Store) {} |
|||
|
|||
ngOnInit(): void { |
|||
const canCreate = this.store.selectSnapshot(ConfigState.getGrantedPolicy('AbpIdentity.Roles.Create')); |
|||
} |
|||
|
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
Or you can get it via `ConfigStateService`: |
|||
|
|||
```js |
|||
import { ConfigStateService } from '../services/config-state.service'; |
|||
|
|||
export class YourComponent { |
|||
constructor(private configStateService: ConfigStateService) {} |
|||
|
|||
ngOnInit(): void { |
|||
const canCreate = this.configStateService.getGrantedPolicy('AbpIdentity.Roles.Create'); |
|||
} |
|||
|
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
## Permission Directive |
|||
|
|||
You can use the `PermissionDirective` to manage visibility of a DOM Element accordingly to user's permission. |
|||
|
|||
```html |
|||
<div *abpPermission="AbpIdentity.Roles"> |
|||
This content is only visible if the user has 'AbpIdentity.Roles' permission. |
|||
</div> |
|||
``` |
|||
|
|||
As shown above you can remove elements from DOM with `abpPermission` structural directive. |
|||
|
|||
The directive can also be used as an attribute directive but we recommend to you to use it as a structural directive. |
|||
|
|||
## Permission Guard |
|||
|
|||
You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation. |
|||
|
|||
Add `requiredPolicy` to the `routes` property in your routing module. |
|||
|
|||
```js |
|||
const routes: Routes = [ |
|||
{ |
|||
path: 'path', |
|||
component: YourComponent, |
|||
canActivate: [PermissionGuard], |
|||
data: { |
|||
routes: { |
|||
requiredPolicy: 'AbpIdentity.Roles.Create', |
|||
}, |
|||
}, |
|||
}, |
|||
]; |
|||
``` |
|||
|
|||
Granted Policies are stored in the `auth` property of `ConfigState`. |
|||
|
|||
## What's Next? |
|||
|
|||
* [Config State](./Config-State.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,67 @@ |
|||
## Service Proxies |
|||
|
|||
It is common to call a REST endpoint in the server from our Angular applications. In this case, we generally create **services** (those have methods for each service method on the server side) and **model objects** (matches to [DTOs](../../Data-Transfer-Objects) in the server side). |
|||
|
|||
In addition to manually creating such server-interacting services, we could use tools like [NSWAG](https://github.com/RicoSuter/NSwag) to generate service proxies for us. But NSWAG has the following problems we've experienced: |
|||
|
|||
* It generates a **big, single** .ts file which has some problems; |
|||
* It get **too large** when your application grows. |
|||
* It doesn't fit into the **[modular](../../Module-Development-Basics) approach** of the ABP framework. |
|||
* It creates a bit **ugly code**. We want to have a clean code (just like if we write manually). |
|||
* It can not generate the same **method signature** declared in the server side (because swagger.json doesn't exactly reflect the method signature of the backend service). We've created an endpoint that exposes server side method contacts to allow clients generate a better aligned client proxies. |
|||
|
|||
ABP CLI `generate-proxies` command automatically generates the typescript client proxies by creating folders which separated by module names in the `src/app` folder. |
|||
Run the following command in the **root folder** of the angular application: |
|||
|
|||
```bash |
|||
abp generate-proxy |
|||
``` |
|||
|
|||
It only creates proxies only for your own application's services. It doesn't create proxies for the services of the application modules you're using (by default). There are several options. See the [CLI documentation](../../CLI). |
|||
|
|||
The files generated with the `--module all` option like below: |
|||
|
|||
 |
|||
|
|||
### Services |
|||
|
|||
Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests#restservice). |
|||
|
|||
A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config) |
|||
|
|||
The `providedIn` property of the services is defined as `'root'`. Therefore no need to add a service as a provider to a module. You can use a service by injecting it into a constructor as shown below: |
|||
|
|||
```js |
|||
import { AbpApplicationConfigurationService } from '../app/shared/services'; |
|||
|
|||
//... |
|||
export class HomeComponent{ |
|||
constructor(private appConfigService: AbpApplicationConfigurationService) {} |
|||
|
|||
ngOnInit() { |
|||
this.appConfigService.get().subscribe() |
|||
} |
|||
} |
|||
``` |
|||
|
|||
The Angular compiler removes the services that have not been injected anywhere from the final output. See the [tree-shakable providers documentation](https://angular.io/guide/dependency-injection-providers#tree-shakable-providers). |
|||
|
|||
### Models |
|||
|
|||
The generated models match the DTOs in the back-end. Each model is generated as a class under the `src/app/*/shared/models` folder. |
|||
|
|||
There are a few [base classes](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts) in the `@abp/ng.core` package. Some models extend these classes. |
|||
|
|||
A class instance can be created as shown below: |
|||
|
|||
```js |
|||
import { IdentityRoleCreateDto } from '../identity/shared/models'; |
|||
//... |
|||
const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true}) |
|||
``` |
|||
|
|||
Initial values can optionally be passed to each class constructor. |
|||
|
|||
## What's Next? |
|||
|
|||
* [HTTP Requests](./Http-Requests) |
|||
@ -0,0 +1,113 @@ |
|||
# Easy TrackByFunction Implementation |
|||
|
|||
`TrackByService` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose. |
|||
|
|||
|
|||
|
|||
## Getting Started |
|||
|
|||
You do not have to provide the `TrackByService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components. For better type support, you may pass in the type of the iterated item to it. |
|||
|
|||
```js |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
/* class metadata here */ |
|||
}) |
|||
class DemoComponent { |
|||
list: Item[]; |
|||
|
|||
constructor(public readonly track: TrackByService<Item>) {} |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
> Noticed `track` is `public` and `readonly`? That is because we will see some examples where methods of `TrackByService` instance are directly called in the component's template. That may be considered as an anti-pattern, but it has its own advantage, especially when component inheritance is leveraged. You can always use public component properties instead. |
|||
|
|||
|
|||
|
|||
**The members are also exported as separate functions.** If you do not want to inject `TrackByService`, you can always import and use those functions directly in your classes. |
|||
|
|||
|
|||
|
|||
## Usage |
|||
|
|||
There are two approaches available. |
|||
|
|||
1. You may inject `TrackByService` to your component and use its members. |
|||
2. You may use exported higher-order functions directly on component properties. |
|||
|
|||
|
|||
|
|||
### How to Track Items by a Key |
|||
|
|||
You can use `by` to get a `TrackByFunction` that tracks the iterated object based on one of its keys. For type support, you may pass in the type of the iterated item to it. |
|||
|
|||
```html |
|||
<!-- template of DemoComponent --> |
|||
|
|||
<div *ngFor="let item of list; trackBy: track.by('id')">{%{{{ item.name }}}%}</div> |
|||
``` |
|||
|
|||
|
|||
|
|||
`by` is exported as a stand-alone function and is named `trackBy`. |
|||
|
|||
```js |
|||
import { trackBy } from "@abp/ng.core"; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<div |
|||
*ngFor="let item of list; trackBy: trackById" |
|||
> |
|||
{%{{{ item.name }}}%} |
|||
</div> |
|||
`, |
|||
}) |
|||
class DemoComponent { |
|||
list: Item[]; |
|||
|
|||
trackById = trackBy<Item>('id'); |
|||
} |
|||
``` |
|||
|
|||
|
|||
|
|||
### How to Track by a Deeply Nested Key |
|||
|
|||
You can use `byDeep` to get a `TrackByFunction` that tracks the iterated object based on a deeply nested key. For type support, you may pass in the type of the iterated item to it. |
|||
|
|||
```html |
|||
<!-- template of DemoComponent --> |
|||
|
|||
<div |
|||
*ngFor="let item of list; trackBy: track.byDeep('tenant', 'account', 'id')" |
|||
> |
|||
{%{{{ item.tenant.name }}}%} |
|||
</div> |
|||
``` |
|||
|
|||
|
|||
|
|||
`byDeep` is exported as a stand-alone function and is named `trackByDeep`. |
|||
|
|||
```js |
|||
import { trackByDeep } from "@abp/ng.core"; |
|||
|
|||
@Component({ |
|||
template: ` |
|||
<div |
|||
*ngFor="let item of list; trackBy: trackByTenantAccountId" |
|||
> |
|||
{%{{{ item.name }}}%} |
|||
</div> |
|||
`, |
|||
}) |
|||
class DemoComponent { |
|||
list: Item[]; |
|||
|
|||
trackByTenantAccountId = trackByDeep<Item>('tenant', 'account', 'id'); |
|||
} |
|||
``` |
|||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 235 KiB |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue