mirror of https://github.com/abpframework/abp.git
152 changed files with 3239 additions and 1153 deletions
@ -1,3 +0,0 @@ |
|||||
## AutoMapper Integration |
|
||||
|
|
||||
TODO |
|
||||
@ -1,3 +1,267 @@ |
|||||
# Object Extensions |
# Object Extensions |
||||
|
|
||||
TODO |
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. |
||||
|
|
||||
|
Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
> See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option. |
||||
|
|
||||
|
`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. |
||||
|
|
||||
|
## Object to Object Mapping |
||||
|
|
||||
|
Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients. |
||||
|
|
||||
|
This section offers some **good practices** to control your extra properties on object mapping. |
||||
|
|
||||
|
### MapExtraPropertiesTo |
||||
|
|
||||
|
`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo(identityUserDto); |
||||
|
```` |
||||
|
|
||||
|
`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction. |
||||
|
|
||||
|
#### MappingPropertyDefinitionChecks |
||||
|
|
||||
|
`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
MappingPropertyDefinitionChecks.None |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too. |
||||
|
|
||||
|
If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.CheckPairDefinitionOnMapping = false; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
#### Ignored Properties |
||||
|
|
||||
|
You may want to ignore some properties on a specific mapping operation: |
||||
|
|
||||
|
````csharp |
||||
|
identityUser.MapExtraPropertiesTo( |
||||
|
identityUserDto, |
||||
|
ignoredProperties: new[] {"MySensitiveProp"} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
Ignored properties are not copied to the target object. |
||||
|
|
||||
|
#### AutoMapper Integration |
||||
|
|
||||
|
If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. |
||||
|
|
||||
|
You can use the `MapExtraProperties()` method inside your mapping profile. |
||||
|
|
||||
|
````csharp |
||||
|
public class MyProfile : Profile |
||||
|
{ |
||||
|
public MyProfile() |
||||
|
{ |
||||
|
CreateMap<IdentityUser, IdentityUserDto>() |
||||
|
.MapExtraProperties(); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
It has the same parameters with the `MapExtraPropertiesTo` method. |
||||
|
|
||||
|
## Entity Framework Core Database Mapping |
||||
|
|
||||
|
If you're using the EF Core, you can map an extra property to a table field in the database. Example: |
||||
|
|
||||
|
````csharp |
||||
|
ObjectExtensionManager.Instance |
||||
|
.AddOrUpdateProperty<IdentityUser, string>( |
||||
|
"SocialSecurityNumber", |
||||
|
options => |
||||
|
{ |
||||
|
options.MapEfCore(b => b.HasMaxLength(32)); |
||||
|
} |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. |
||||
@ -0,0 +1,101 @@ |
|||||
|
# ContainerStrategy |
||||
|
|
||||
|
`ContainerStrategy` is an abstract class exposed by @abp/ng.core package. There are two container strategies extending it: `ClearContainerStrategy` and `InsertIntoContainerStrategy`. Implementing the same methods and properties, both of these strategies help you define how your containers will be prepared and where your content will be projected. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## API |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
### constructor |
||||
|
|
||||
|
```js |
||||
|
constructor( |
||||
|
public containerRef: ViewContainerRef, |
||||
|
private index?: number, // works only in InsertIntoContainerStrategy |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
- `containerRef` is the `ViewContainerRef` that will be used when projecting the content. |
||||
|
|
||||
|
|
||||
|
### getIndex |
||||
|
|
||||
|
```js |
||||
|
getIndex(): number |
||||
|
``` |
||||
|
|
||||
|
This method return the given index clamped by `0` and `length` of the `containerRef`. For strategies without an index, it returns `0`. |
||||
|
|
||||
|
|
||||
|
### prepare |
||||
|
|
||||
|
```js |
||||
|
prepare(): void |
||||
|
``` |
||||
|
|
||||
|
This method is called before content projection. Based on used container strategy, it either clears the container or does nothing (noop). |
||||
|
|
||||
|
|
||||
|
|
||||
|
## ClearContainerStrategy |
||||
|
|
||||
|
`ClearContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **clear a container before projecting content in it**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## InsertIntoContainerStrategy |
||||
|
|
||||
|
`InsertIntoContainerStrategy` is a class that extends `ContainerStrategy`. It lets you **project your content at a specific node index in the container**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Predefined Container Strategies |
||||
|
|
||||
|
Predefined container strategies are accessible via `CONTAINER_STRATEGY` constant. |
||||
|
|
||||
|
|
||||
|
### Clear |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Clear(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Clears given container before content projection. |
||||
|
|
||||
|
|
||||
|
### Append |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Append(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be appended to the container. |
||||
|
|
||||
|
|
||||
|
### Prepend |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Prepend(containerRef: ViewContainerRef) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be prepended to the container. |
||||
|
|
||||
|
|
||||
|
### Insert |
||||
|
|
||||
|
```js |
||||
|
CONTAINER_STRATEGY.Insert( |
||||
|
containerRef: ViewContainerRef, |
||||
|
index: number, |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
Projected content will be inserted into to the container at given index (clamped by `0` and `length` of the `containerRef`). |
||||
|
|
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
- [ProjectionStrategy](./Projection-Strategy.md) |
||||
@ -0,0 +1,78 @@ |
|||||
|
# Content Projection |
||||
|
|
||||
|
You can use the `ContentProjectionService` in @abp/ng.core package in order to project content in an easy and explicit way. |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
You do not have to provide the `ContentProjectionService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. |
||||
|
|
||||
|
```js |
||||
|
import { ContentProjectionService } from '@abp/ng.core'; |
||||
|
|
||||
|
@Component({ |
||||
|
/* class metadata here */ |
||||
|
}) |
||||
|
class DemoComponent { |
||||
|
constructor(private contentProjectionService: ContentProjectionService) {} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
You can use the `projectContent` method of `ContentProjectionService` to render components and templates dynamically in your project. |
||||
|
|
||||
|
### How to Project Components to Root Level |
||||
|
|
||||
|
If you pass a `RootComponentProjectionStrategy` as the first parameter of `projectContent` method, the `ContentProjectionService` will resolve the projected component and place it at the root level. If provided, it will also pass the component a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.AppendComponentToBody( |
||||
|
SomeOverlayComponent, |
||||
|
{ someOverlayProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In the example above, `SomeOverlayComponent` component will placed at the **end** of `<body>` and a `ComponentRef` will be returned. Additionally, the given context will be applied, so `someOverlayProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` instance, as it is a reference to the projected component and you will need that reference to destroy the projected view and the component instance. |
||||
|
|
||||
|
### How to Project Components and Templates into a Container |
||||
|
|
||||
|
If you pass a `ComponentProjectionStrategy` or `TemplateProjectionStrategy` as the first parameter of `projectContent` method, and a `ViewContainerRef` as the second parameter of that strategy, the `ContentProjectionService` will project the component or template to the given container. If provided, it will also pass the component or the template a context. |
||||
|
|
||||
|
```js |
||||
|
const strategy = PROJECTION_STRATEGY.ProjectComponentToContainer( |
||||
|
SomeComponent, |
||||
|
viewContainerRefOfTarget, |
||||
|
{ someProp: "SOME_VALUE" } |
||||
|
); |
||||
|
|
||||
|
const componentRef = this.contentProjectionService.projectContent(strategy); |
||||
|
``` |
||||
|
|
||||
|
In this example, the `viewContainerRefOfTarget`, which is a `ViewContainerRef` instance, will be cleared and `SomeComponent` component will be placed inside it. In addition, the given context will be applied and `someProp` of the component will be set to `SOME_VALUE`. |
||||
|
|
||||
|
> You should keep the returned `ComponentRef` or `EmbeddedViewRef`, as they are a reference to the projected content and you will need them to destroy it when necessary. |
||||
|
|
||||
|
Please refer to [ProjectionStrategy](./Projection-Strategy.md) to see all available projection strategies and how you can build your own projection strategy. |
||||
|
|
||||
|
## API |
||||
|
|
||||
|
### projectContent |
||||
|
|
||||
|
```js |
||||
|
projectContent<T extends Type<any> | TemplateRef<any>>( |
||||
|
projectionStrategy: ProjectionStrategy<T>, |
||||
|
injector = this.injector, |
||||
|
): ComponentRef<C> | EmbeddedViewRef<C> |
||||
|
``` |
||||
|
|
||||
|
- `projectionStrategy` parameter is the primary focus here and is explained above. |
||||
|
- `injector` parameter is the `Injector` instance you can pass to the projected content. It is not used in `TemplateProjectionStrategy`. |
||||
|
|
||||
|
|
||||
|
## What's Next? |
||||
|
|
||||
|
- [TrackByService](./Track-By-Service.md) |
||||
@ -0,0 +1,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,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) |
||||
@ -1,3 +1,198 @@ |
|||||
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 |
# 如何对MVC / Razor页面应用程序使用Azure Active Directory身份验证 |
||||
|
|
||||
TODO... |
本文介绍了如何将AzureAD集成到ABP应用程序中,用 **Azure Active Directory** 凭据使用 OAuth 2.0 登录. |
||||
|
|
||||
|
添加Azure Active Directory到ABP框架非常简单,只需要正确的完成几个配置. |
||||
|
|
||||
|
为了覆盖更多范围,我们演示两种不同的集成AzureAD的**方法**. |
||||
|
|
||||
|
1. **AddAzureAD**: 该方法使用微软[AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),在网络上搜索如何将AzureAD集成到应用程序时,这个包是最流行的. |
||||
|
|
||||
|
2. **AddOpenIdConnect**: 该方法使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/). 它不仅可用于AzureAD,还可用于所有OpenId连接. |
||||
|
|
||||
|
> 这些方法之间的功能**没有区别**,AddAzureAD是具有预定义Cookie设置的OpenIdConnection([源](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADAuthenticationBuilderExtensions.cs#L122))的抽象方法. |
||||
|
> |
||||
|
> 但是默认配置的登录方案在与ABP应用程序集成方面存在关键差异,下面将对此进行说明. |
||||
|
|
||||
|
## 1. AddAzureAD |
||||
|
|
||||
|
这个方法使用 [Microsoft AzureAD UI nuget 包](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.AzureAD.UI/),它是最常用的集成AzureAD方法. |
||||
|
|
||||
|
如果选择这种方法,需要将 `Microsoft.AspNetCore.Authentication.AzureAD.UI` 软件包安装到 **.Web** 项目中. 由于AddAzureAD扩展使用[配置绑定](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#default-configuration),你需要更改 **.Web** 项目中的appsettings.json文件. |
||||
|
|
||||
|
#### **更改 `appsettings.json`** |
||||
|
|
||||
|
你添加向 `appsettings.json` 添加新的配置节,在配置 `OpenIdConnectOptions` 时绑定配置: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> 这里重要的配置是CallbackPath. 值必须与你的 Azure AD-> app registrations-> Authentication -> RedirectUri 之一相同. |
||||
|
|
||||
|
然后你需要配置 `OpenIdConnectOptions` 完成集成. |
||||
|
|
||||
|
#### 配置 OpenIdConnectOptions |
||||
|
|
||||
|
在你的 **.Web** 项目找到 **ApplicationWebModule** 使用以下代码修改 `ConfigureAuthentication` 方法: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "Acme.BookStore"; |
||||
|
}) |
||||
|
.AddAzureAD(options => configuration.Bind("AzureAd", options)); |
||||
|
|
||||
|
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => |
||||
|
{ |
||||
|
options.Authority = options.Authority + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
|
||||
|
options.TokenValidationParameters.ValidateIssuer = false; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
options.SaveTokens = true; |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> **不要忘记:** |
||||
|
> |
||||
|
> * 在 `AddAuthentication()` 之后添加 `.AddAzureAD(options => configuration.Bind("AzureAd", options))` . 它绑定了你的 AzureAD 配置并且容易忘记. |
||||
|
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. 它会禁用默认的 Microsoft claim type 映射. |
||||
|
> * 添加 `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. 映射 [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) 很重要,因为默认SignIn Manager和行为使用这个claim type用于外部登录信息. |
||||
|
> * 添加 `options.SignInScheme = IdentityConstants.ExternalScheme` 因为 [默认登录方法为 `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). |
||||
|
> * 如果你使用的是 **v2.0** 端点,应添加 `options.Scope.Add("email")` 因为 v2.0 端点不会将 `email` 做为默认值返回. [账户模块](../Modules/Account.md) 使用 `email` claim 来 [注册外部账户](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). |
||||
|
|
||||
|
你已经完成了集成. |
||||
|
|
||||
|
## 2. 替代方法: AddOpenIdConnect |
||||
|
|
||||
|
如果你不想在应用程序安装一个额外的NuGet包,你可以使用默认的[OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect/),它适用于所有的OpenId连接,包括AzureAD外部认证. |
||||
|
|
||||
|
你不必使用 `appsettings.json` 配置, 但将AzureAD信息放在 `appsettings.json` 是一个很好的做法. |
||||
|
|
||||
|
为了从 `appsettings.json` 获取AzureAD信息在 `OpenIdConnectOptions` 配置使用,只需要在你的 **.Web** 项目中的 `appsettings.json` 添加一个新的配置节: |
||||
|
|
||||
|
````json |
||||
|
"AzureAd": { |
||||
|
"Instance": "https://login.microsoftonline.com/", |
||||
|
"TenantId": "<your-tenant-id>", |
||||
|
"ClientId": "<your-client-id>", |
||||
|
"Domain": "domain.onmicrosoft.com", |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
然后在你的 **.Web** 项目的 **ApplicationWebModule** 用以下代码修改 `ConfigureAuthentication` 方法: |
||||
|
|
||||
|
````csharp |
||||
|
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) |
||||
|
{ |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
|
||||
|
context.Services.AddAuthentication() |
||||
|
.AddIdentityServerAuthentication(options => |
||||
|
{ |
||||
|
options.Authority = configuration["AuthServer:Authority"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.ApiName = "BookStore"; |
||||
|
}) |
||||
|
.AddOpenIdConnect("AzureOpenId", "Azure Active Directory OpenId", options => |
||||
|
{ |
||||
|
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"] + "/v2.0/"; |
||||
|
options.ClientId = configuration["AzureAd:ClientId"]; |
||||
|
options.ResponseType = OpenIdConnectResponseType.CodeIdToken; |
||||
|
options.CallbackPath = configuration["AzureAd:CallbackPath"]; |
||||
|
options.RequireHttpsMetadata = false; |
||||
|
options.SaveTokens = true; |
||||
|
options.GetClaimsFromUserInfoEndpoint = true; |
||||
|
|
||||
|
options.Scope.Add("email"); |
||||
|
}); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
集成结束. 请记住你可以连接任何其他外部认证供应商. |
||||
|
|
||||
|
## 本文的源代码 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
||||
|
|
||||
|
# FAQ |
||||
|
|
||||
|
* Help! `GetExternalLoginInfoAsync` 返回 `null`! |
||||
|
|
||||
|
* 有两方面的原因; |
||||
|
|
||||
|
1. 你在尝试验证错误的方案. 检查是否设置 **SignInScheme** 为 `IdentityConstants.ExternalScheme`: |
||||
|
|
||||
|
````csharp |
||||
|
options.SignInScheme = IdentityConstants.ExternalScheme; |
||||
|
```` |
||||
|
|
||||
|
2. 你的 `ClaimTypes.NameIdentifier` 为 `null`. 检查是否添加 claim 映射: |
||||
|
|
||||
|
````csharp |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); |
||||
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); |
||||
|
```` |
||||
|
|
||||
|
* Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! |
||||
|
|
||||
|
* 如果你在appsettings设置 **CallbackPath** 为: |
||||
|
|
||||
|
````csharp |
||||
|
"AzureAd": { |
||||
|
... |
||||
|
"CallbackPath": "/signin-azuread-oidc" |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的<u>域</u>, 而不仅是 `/signin-azuread-oidc`. |
||||
|
|
||||
|
* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! |
||||
|
|
||||
|
* 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 |
||||
|
|
||||
|
````csharp |
||||
|
options.Scope.Add("email"); |
||||
|
```` |
||||
|
|
||||
|
到你的 openid 配置. |
||||
|
|
||||
|
* 如何**调试/监视**在映射之前获得的声明? |
||||
|
|
||||
|
* 你可以在 openid 配置下加一个简单的事件在映射之前进行调试,例如: |
||||
|
|
||||
|
````csharp |
||||
|
options.Events.OnTokenValidated = (async context => |
||||
|
{ |
||||
|
var claimsFromOidcProvider = context.Principal.Claims.ToList(); |
||||
|
await Task.CompletedTask; |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
||||
|
* [如何为ABP应用程序定制SignIn Manager](Customize-SignIn-Manager.md). |
||||
@ -1,3 +1,101 @@ |
|||||
# 如何为ABP应用程序定制SignIn Manager |
# 如何为ABP应用程序定制SignIn Manager |
||||
|
|
||||
TODO... |
在使用[应用程序启动模板](../Startup-Templates/Application.md)创建新项目后,你可能想要扩展或更改SignIn Manager的默认行为,以满足你需要的身份验证和注册流程. ABP[账户模块](../Modules/Account.md)使用[身份管理模块](../Modules/Identity.md)做为SignIn Manager,而[身份管理模块](../Modules/Identity.md)使用默认的[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)([参阅此处]((https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17))). |
||||
|
|
||||
|
编写自定义SignIn Manager,你需要扩展[Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs)类并注入到DI容器. |
||||
|
|
||||
|
本文介绍了如何为你自己的应用程序自定义SignIn Manager. |
||||
|
|
||||
|
## 创建 CustomSignInManager |
||||
|
|
||||
|
创建一个类并继承自Microsoft Identity 包的 [SignInMager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs). |
||||
|
|
||||
|
````csharp |
||||
|
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser> |
||||
|
{ |
||||
|
public CustomSignInManager( |
||||
|
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager, |
||||
|
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, |
||||
|
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, |
||||
|
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, |
||||
|
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger, |
||||
|
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, |
||||
|
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation) |
||||
|
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> 重点是使用**Volo.Abp.Identity.IdentityUser**做为泛型参数,而不是应用程序的AppUser. |
||||
|
|
||||
|
然后你可以覆盖SignIn Manager的任何方法并且为你的身份验证和注册流程添加需要的方法和属性. |
||||
|
|
||||
|
## 重写 GetExternalLoginInfoAsync 方法 |
||||
|
|
||||
|
在这个用例中我们重写第三方身份验证时使用的 `GetExternalLoginInfoAsync` 方法实现. |
||||
|
|
||||
|
一个好的开始是从复制[源码](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L638-L674)而不是从零开始. 在这个用例中我们对源码进行较少的修改,为了帮助理解概念它显式显示了方法和属性的命名空间. |
||||
|
|
||||
|
````csharp |
||||
|
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) |
||||
|
{ |
||||
|
var auth = await Context.AuthenticateAsync(Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme); |
||||
|
var items = auth?.Properties?.Items; |
||||
|
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (expectedXsrf != null) |
||||
|
{ |
||||
|
if (!items.ContainsKey("XsrfKey")) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
var userId = items[XsrfKey] as string; |
||||
|
if (userId != expectedXsrf) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); |
||||
|
var provider = items[LoginProviderKey] as string; |
||||
|
if (providerKey == null || provider == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName |
||||
|
?? provider; |
||||
|
return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) |
||||
|
{ |
||||
|
AuthenticationTokens = auth.Properties.GetTokens() |
||||
|
}; |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
要使你自定义的SignIn Manager类生效,你需要将其注册[依赖注入系统](../Dependency-Injection.md)中. |
||||
|
|
||||
|
## 注册到依赖注入 |
||||
|
|
||||
|
应该使用 [IdentityBuilder](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityBuilder.cs) 的 [IdentityBuilderExtensions](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/IdentityBuilderExtensions.cs) 类的 **AddSignInManager** 扩展方法注册 `CustomSignInManager`. |
||||
|
|
||||
|
在你的 `.Web` 项目找到 `YourProjectNameWebModule` 的 `PreConfigureServices` 方法添加以下代码替换老的 `SignInManager`: |
||||
|
|
||||
|
````csharp |
||||
|
PreConfigure<IdentityBuilder>(identityBuilder => |
||||
|
{ |
||||
|
identityBuilder.AddSignInManager<CustomSignInManager>(); |
||||
|
}); |
||||
|
```` |
||||
|
|
||||
|
## 本文的源代码 |
||||
|
|
||||
|
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization)找到已完成的示例源码. |
||||
|
|
||||
|
## 另请参阅 |
||||
|
|
||||
|
* [如何为MVC / Razor页面应用程序自定义登录页面](Customize-Login-Page-MVC.md). |
||||
|
* [身份管理模块](../Modules/Identity.md). |
||||
|
|||||
@ -0,0 +1,9 @@ |
|||||
|
export const enum eAccountComponents { |
||||
|
Login = 'Account.LoginComponent', |
||||
|
Register = 'Account.RegisterComponent', |
||||
|
ManageProfile = 'Account.ManageProfileComponent', |
||||
|
TenantBox = 'Account.TenantBoxComponent', |
||||
|
AuthWrapper = 'Account.AuthWrapperComponent', |
||||
|
ChangePassword = 'Account.ChangePasswordComponent', |
||||
|
PersonalSettings = 'Account.PersonalSettingsComponent', |
||||
|
} |
||||
@ -1,5 +1,6 @@ |
|||||
export * from './lib/account.module'; |
export * from './lib/account.module'; |
||||
export * from './lib/components'; |
export * from './lib/components'; |
||||
|
export * from './lib/enums/components'; |
||||
export * from './lib/tokens'; |
export * from './lib/tokens'; |
||||
export * from './lib/models'; |
export * from './lib/models'; |
||||
export * from './lib/services'; |
export * from './lib/services'; |
||||
|
|||||
@ -0,0 +1,4 @@ |
|||||
|
import { TemplateRef, Type } from '@angular/core'; |
||||
|
|
||||
|
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never; |
||||
|
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never; |
||||
@ -0,0 +1,14 @@ |
|||||
|
import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; |
||||
|
import { ProjectionStrategy } from '../strategies/projection.strategy'; |
||||
|
|
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class ContentProjectionService { |
||||
|
constructor(private injector: Injector) {} |
||||
|
|
||||
|
projectContent<T extends Type<any> | TemplateRef<any>>( |
||||
|
projectionStrategy: ProjectionStrategy<T>, |
||||
|
injector = this.injector, |
||||
|
) { |
||||
|
return projectionStrategy.injectContent(injector); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
import { ViewContainerRef } from '@angular/core'; |
||||
|
|
||||
|
export abstract class ContainerStrategy { |
||||
|
constructor(public containerRef: ViewContainerRef) {} |
||||
|
|
||||
|
abstract getIndex(): number; |
||||
|
|
||||
|
prepare(): void {} |
||||
|
} |
||||
|
|
||||
|
export class ClearContainerStrategy extends ContainerStrategy { |
||||
|
getIndex(): number { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
prepare() { |
||||
|
this.containerRef.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class InsertIntoContainerStrategy extends ContainerStrategy { |
||||
|
constructor(containerRef: ViewContainerRef, private index: number) { |
||||
|
super(containerRef); |
||||
|
} |
||||
|
|
||||
|
getIndex() { |
||||
|
return Math.min(Math.max(0, this.index), this.containerRef.length); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const CONTAINER_STRATEGY = { |
||||
|
Clear(containerRef: ViewContainerRef) { |
||||
|
return new ClearContainerStrategy(containerRef); |
||||
|
}, |
||||
|
Append(containerRef: ViewContainerRef) { |
||||
|
return new InsertIntoContainerStrategy(containerRef, containerRef.length); |
||||
|
}, |
||||
|
Prepend(containerRef: ViewContainerRef) { |
||||
|
return new InsertIntoContainerStrategy(containerRef, 0); |
||||
|
}, |
||||
|
Insert(containerRef: ViewContainerRef, index: number) { |
||||
|
return new InsertIntoContainerStrategy(containerRef, index); |
||||
|
}, |
||||
|
}; |
||||
@ -0,0 +1,47 @@ |
|||||
|
import { ComponentRef, TemplateRef, Type } from '@angular/core'; |
||||
|
import { InferredContextOf, InferredInstanceOf } from '../models'; |
||||
|
|
||||
|
export abstract class ContextStrategy<T = any> { |
||||
|
constructor(public context: Partial<ContextType<T>>) {} |
||||
|
|
||||
|
/* tslint:disable-next-line:no-unused-variable */ |
||||
|
setContext(componentRef?: ComponentRef<InferredInstanceOf<T>>): Partial<ContextType<T>> { |
||||
|
return this.context; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class NoContextStrategy< |
||||
|
T extends Type<any> | TemplateRef<any> = any |
||||
|
> extends ContextStrategy<T> { |
||||
|
constructor() { |
||||
|
super(undefined); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class ComponentContextStrategy<T extends Type<any> = any> extends ContextStrategy<T> { |
||||
|
setContext(componentRef: ComponentRef<InferredInstanceOf<T>>): Partial<InferredInstanceOf<T>> { |
||||
|
Object.keys(this.context).forEach(key => (componentRef.instance[key] = this.context[key])); |
||||
|
componentRef.changeDetectorRef.detectChanges(); |
||||
|
return this.context; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class TemplateContextStrategy<T extends TemplateRef<any> = any> extends ContextStrategy<T> { |
||||
|
setContext(): Partial<InferredContextOf<T>> { |
||||
|
return this.context; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const CONTEXT_STRATEGY = { |
||||
|
None<T extends Type<any> | TemplateRef<any> = any>() { |
||||
|
return new NoContextStrategy<T>(); |
||||
|
}, |
||||
|
Component<T extends Type<any> = any>(context: Partial<InferredInstanceOf<T>>) { |
||||
|
return new ComponentContextStrategy<T>(context); |
||||
|
}, |
||||
|
Template<T extends TemplateRef<any> = any>(context: Partial<InferredContextOf<T>>) { |
||||
|
return new TemplateContextStrategy<T>(context); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
type ContextType<T> = T extends Type<infer U> | TemplateRef<infer U> ? U : never; |
||||
@ -1,5 +1,8 @@ |
|||||
|
export * from './container.strategy'; |
||||
export * from './content-security.strategy'; |
export * from './content-security.strategy'; |
||||
export * from './content.strategy'; |
export * from './content.strategy'; |
||||
|
export * from './context.strategy'; |
||||
export * from './cross-origin.strategy'; |
export * from './cross-origin.strategy'; |
||||
export * from './dom.strategy'; |
export * from './dom.strategy'; |
||||
export * from './loading.strategy'; |
export * from './loading.strategy'; |
||||
|
export * from './projection.strategy'; |
||||
|
|||||
@ -0,0 +1,176 @@ |
|||||
|
import { |
||||
|
ApplicationRef, |
||||
|
ComponentFactoryResolver, |
||||
|
ComponentRef, |
||||
|
EmbeddedViewRef, |
||||
|
Injector, |
||||
|
TemplateRef, |
||||
|
Type, |
||||
|
ViewContainerRef, |
||||
|
} from '@angular/core'; |
||||
|
import { InferredContextOf, InferredInstanceOf } from '../models/utility'; |
||||
|
import { ContainerStrategy, CONTAINER_STRATEGY } from './container.strategy'; |
||||
|
import { ContextStrategy, CONTEXT_STRATEGY } from './context.strategy'; |
||||
|
import { DomStrategy, DOM_STRATEGY } from './dom.strategy'; |
||||
|
|
||||
|
export abstract class ProjectionStrategy<T = any> { |
||||
|
constructor(public content: T) {} |
||||
|
|
||||
|
abstract injectContent(injector: Injector): ComponentRefOrEmbeddedViewRef<T>; |
||||
|
} |
||||
|
|
||||
|
export class ComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> { |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), |
||||
|
) { |
||||
|
super(component); |
||||
|
} |
||||
|
|
||||
|
injectContent(injector: Injector) { |
||||
|
this.containerStrategy.prepare(); |
||||
|
|
||||
|
const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; |
||||
|
const factory = resolver.resolveComponentFactory<InferredInstanceOf<T>>(this.content); |
||||
|
|
||||
|
const componentRef = this.containerStrategy.containerRef.createComponent( |
||||
|
factory, |
||||
|
this.containerStrategy.getIndex(), |
||||
|
injector, |
||||
|
); |
||||
|
this.contextStrategy.setContext(componentRef); |
||||
|
|
||||
|
return componentRef as ComponentRefOrEmbeddedViewRef<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class RootComponentProjectionStrategy<T extends Type<any>> extends ProjectionStrategy<T> { |
||||
|
constructor( |
||||
|
component: T, |
||||
|
private contextStrategy: ContextStrategy = CONTEXT_STRATEGY.None(), |
||||
|
private domStrategy: DomStrategy = DOM_STRATEGY.AppendToBody(), |
||||
|
) { |
||||
|
super(component); |
||||
|
} |
||||
|
|
||||
|
injectContent(injector: Injector) { |
||||
|
const appRef = injector.get(ApplicationRef); |
||||
|
const resolver = injector.get(ComponentFactoryResolver) as ComponentFactoryResolver; |
||||
|
const componentRef = resolver |
||||
|
.resolveComponentFactory<InferredInstanceOf<T>>(this.content) |
||||
|
.create(injector); |
||||
|
|
||||
|
this.contextStrategy.setContext(componentRef); |
||||
|
|
||||
|
appRef.attachView(componentRef.hostView); |
||||
|
const element: HTMLElement = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]; |
||||
|
this.domStrategy.insertElement(element); |
||||
|
|
||||
|
return componentRef as ComponentRefOrEmbeddedViewRef<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class TemplateProjectionStrategy<T extends TemplateRef<any>> extends ProjectionStrategy<T> { |
||||
|
constructor( |
||||
|
templateRef: T, |
||||
|
private containerStrategy: ContainerStrategy, |
||||
|
private contextStrategy = CONTEXT_STRATEGY.None(), |
||||
|
) { |
||||
|
super(templateRef); |
||||
|
} |
||||
|
|
||||
|
injectContent() { |
||||
|
this.containerStrategy.prepare(); |
||||
|
|
||||
|
const embeddedViewRef = this.containerStrategy.containerRef.createEmbeddedView( |
||||
|
this.content, |
||||
|
this.contextStrategy.context, |
||||
|
this.containerStrategy.getIndex(), |
||||
|
); |
||||
|
embeddedViewRef.detectChanges(); |
||||
|
|
||||
|
return embeddedViewRef as ComponentRefOrEmbeddedViewRef<T>; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const PROJECTION_STRATEGY = { |
||||
|
AppendComponentToBody<T extends Type<unknown>>(component: T, context?: InferredInstanceOf<T>) { |
||||
|
return new RootComponentProjectionStrategy<T>( |
||||
|
component, |
||||
|
context && CONTEXT_STRATEGY.Component(context), |
||||
|
); |
||||
|
}, |
||||
|
AppendComponentToContainer<T extends Type<unknown>>( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredInstanceOf<T>, |
||||
|
) { |
||||
|
return new ComponentProjectionStrategy<T>( |
||||
|
component, |
||||
|
CONTAINER_STRATEGY.Append(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Component(context), |
||||
|
); |
||||
|
}, |
||||
|
AppendTemplateToContainer<T extends TemplateRef<unknown>>( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredContextOf<T>, |
||||
|
) { |
||||
|
return new TemplateProjectionStrategy<T>( |
||||
|
templateRef, |
||||
|
CONTAINER_STRATEGY.Append(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Template(context), |
||||
|
); |
||||
|
}, |
||||
|
PrependComponentToContainer<T extends Type<unknown>>( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredInstanceOf<T>, |
||||
|
) { |
||||
|
return new ComponentProjectionStrategy<T>( |
||||
|
component, |
||||
|
CONTAINER_STRATEGY.Prepend(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Component(context), |
||||
|
); |
||||
|
}, |
||||
|
PrependTemplateToContainer<T extends TemplateRef<unknown>>( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredContextOf<T>, |
||||
|
) { |
||||
|
return new TemplateProjectionStrategy<T>( |
||||
|
templateRef, |
||||
|
CONTAINER_STRATEGY.Prepend(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Template(context), |
||||
|
); |
||||
|
}, |
||||
|
ProjectComponentToContainer<T extends Type<unknown>>( |
||||
|
component: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredInstanceOf<T>, |
||||
|
) { |
||||
|
return new ComponentProjectionStrategy<T>( |
||||
|
component, |
||||
|
CONTAINER_STRATEGY.Clear(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Component(context), |
||||
|
); |
||||
|
}, |
||||
|
ProjectTemplateToContainer<T extends TemplateRef<unknown>>( |
||||
|
templateRef: T, |
||||
|
containerRef: ViewContainerRef, |
||||
|
context?: InferredContextOf<T>, |
||||
|
) { |
||||
|
return new TemplateProjectionStrategy<T>( |
||||
|
templateRef, |
||||
|
CONTAINER_STRATEGY.Clear(containerRef), |
||||
|
context && CONTEXT_STRATEGY.Template(context), |
||||
|
); |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
type ComponentRefOrEmbeddedViewRef<T> = T extends Type<infer U> |
||||
|
? ComponentRef<U> |
||||
|
: T extends TemplateRef<infer C> |
||||
|
? EmbeddedViewRef<C> |
||||
|
: never; |
||||
@ -0,0 +1,80 @@ |
|||||
|
import { ViewContainerRef } from '@angular/core'; |
||||
|
import { |
||||
|
ClearContainerStrategy, |
||||
|
CONTAINER_STRATEGY, |
||||
|
InsertIntoContainerStrategy, |
||||
|
} from '../strategies'; |
||||
|
|
||||
|
describe('ClearContainerStrategy', () => { |
||||
|
const containerRef = ({ |
||||
|
clear: jest.fn(), |
||||
|
length: 7, |
||||
|
} as any) as ViewContainerRef; |
||||
|
|
||||
|
describe('#getIndex', () => { |
||||
|
it('should return 0', () => { |
||||
|
const strategy = new ClearContainerStrategy(containerRef); |
||||
|
expect(strategy.getIndex()).toBe(0); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('#prepare', () => { |
||||
|
it('should call clear method of containerRef once', () => { |
||||
|
const strategy = new ClearContainerStrategy(containerRef); |
||||
|
strategy.prepare(); |
||||
|
expect(strategy.getIndex()).toBe(0); |
||||
|
expect(containerRef.clear).toHaveBeenCalledTimes(1); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('InsertIntoContainerStrategy', () => { |
||||
|
const containerRef = ({ |
||||
|
clear: jest.fn(), |
||||
|
length: 7, |
||||
|
} as any) as ViewContainerRef; |
||||
|
|
||||
|
describe('#getIndex', () => { |
||||
|
test.each` |
||||
|
index | expected |
||||
|
${0} | ${0} |
||||
|
${4} | ${4} |
||||
|
${9} | ${7} |
||||
|
${-1} | ${0} |
||||
|
${Infinity} | ${7} |
||||
|
`(
|
||||
|
'should return $expected when index is given $index', |
||||
|
({ index, expected }: { index: number; expected: number }) => { |
||||
|
const strategy = new InsertIntoContainerStrategy(containerRef, index); |
||||
|
expect(strategy.getIndex()).toBe(expected); |
||||
|
}, |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
describe('#prepare', () => { |
||||
|
it('should not call clear method of containerRef', () => { |
||||
|
const strategy = new InsertIntoContainerStrategy(containerRef, 0); |
||||
|
strategy.prepare(); |
||||
|
expect(containerRef.clear).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('CONTAINER_STRATEGY', () => { |
||||
|
const containerRef = ({ |
||||
|
clear: jest.fn(), |
||||
|
length: 7, |
||||
|
} as any) as ViewContainerRef; |
||||
|
|
||||
|
test.each` |
||||
|
name | Strategy | index |
||||
|
${'Clear'} | ${ClearContainerStrategy} | ${undefined} |
||||
|
${'Append'} | ${InsertIntoContainerStrategy} | ${containerRef.length} |
||||
|
${'Prepend'} | ${InsertIntoContainerStrategy} | ${0} |
||||
|
${'Insert'} | ${InsertIntoContainerStrategy} | ${4} |
||||
|
`('should successfully map $name to $Strategy.name', ({ name, Strategy, index }) => {
|
||||
|
expect(CONTAINER_STRATEGY[name](containerRef, index)).toEqual( |
||||
|
new Strategy(containerRef, index), |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,38 @@ |
|||||
|
import { Component, ComponentRef, NgModule } from '@angular/core'; |
||||
|
import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; |
||||
|
import { ContentProjectionService } from '../services'; |
||||
|
import { PROJECTION_STRATEGY } from '../strategies'; |
||||
|
|
||||
|
describe('ContentProjectionService', () => { |
||||
|
@Component({ template: '<div class="foo">bar</div>' }) |
||||
|
class TestComponent {} |
||||
|
|
||||
|
// createServiceFactory does not accept entryComponents directly
|
||||
|
@NgModule({ |
||||
|
declarations: [TestComponent], |
||||
|
entryComponents: [TestComponent], |
||||
|
}) |
||||
|
class TestModule {} |
||||
|
|
||||
|
let componentRef: ComponentRef<TestComponent>; |
||||
|
let spectator: SpectatorService<ContentProjectionService>; |
||||
|
const createService = createServiceFactory({ |
||||
|
service: ContentProjectionService, |
||||
|
imports: [TestModule], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => (spectator = createService())); |
||||
|
|
||||
|
afterEach(() => componentRef.destroy()); |
||||
|
|
||||
|
describe('#projectContent', () => { |
||||
|
it('should call injectContent of given projectionStrategy and return what it returns', () => { |
||||
|
const strategy = PROJECTION_STRATEGY.AppendComponentToBody(TestComponent); |
||||
|
componentRef = spectator.service.projectContent(strategy); |
||||
|
const foo = document.querySelector('body > ng-component > div.foo'); |
||||
|
|
||||
|
expect(componentRef).toBeInstanceOf(ComponentRef); |
||||
|
expect(foo.textContent).toBe('bar'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,79 @@ |
|||||
|
import { ComponentRef } from '@angular/core'; |
||||
|
import { |
||||
|
ComponentContextStrategy, |
||||
|
CONTEXT_STRATEGY, |
||||
|
NoContextStrategy, |
||||
|
TemplateContextStrategy, |
||||
|
} from '../strategies'; |
||||
|
import { uuid } from '../utils'; |
||||
|
|
||||
|
describe('ComponentContextStrategy', () => { |
||||
|
describe('#setContext', () => { |
||||
|
let componentRef: ComponentRef<any>; |
||||
|
|
||||
|
beforeEach( |
||||
|
() => |
||||
|
(componentRef = { |
||||
|
instance: { |
||||
|
x: '', |
||||
|
y: '', |
||||
|
z: '', |
||||
|
}, |
||||
|
changeDetectorRef: { |
||||
|
detectChanges: jest.fn(), |
||||
|
}, |
||||
|
} as any), |
||||
|
); |
||||
|
|
||||
|
test.each` |
||||
|
props | values |
||||
|
${['x']} | ${[uuid()]} |
||||
|
${['x', 'y']} | ${[uuid(), uuid()]} |
||||
|
${['x', 'y', 'z']} | ${[uuid(), uuid(), uuid()]} |
||||
|
`(
|
||||
|
'should set $props as $values and call detectChanges once', |
||||
|
({ props, values }: { props: string[]; values: string[] }) => { |
||||
|
const context = {}; |
||||
|
props.forEach((prop, i) => { |
||||
|
context[prop] = values[i]; |
||||
|
}); |
||||
|
|
||||
|
const strategy = new ComponentContextStrategy(context); |
||||
|
strategy.setContext(componentRef); |
||||
|
|
||||
|
expect(props.every(prop => componentRef.instance[prop] === context[prop])).toBe(true); |
||||
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1); |
||||
|
}, |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('NoContextStrategy', () => { |
||||
|
describe('#setContext', () => { |
||||
|
it('should return undefined', () => { |
||||
|
const strategy = new NoContextStrategy(); |
||||
|
expect(strategy.setContext(null)).toBeUndefined(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('TemplateContextStrategy', () => { |
||||
|
describe('#setContext', () => { |
||||
|
it('should return context', () => { |
||||
|
const context = { x: uuid() }; |
||||
|
const strategy = new TemplateContextStrategy(context); |
||||
|
expect(strategy.setContext()).toEqual(context); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('CONTEXT_STRATEGY', () => { |
||||
|
test.each` |
||||
|
name | Strategy |
||||
|
${'Component'} | ${ComponentContextStrategy} |
||||
|
${'None'} | ${NoContextStrategy} |
||||
|
${'Template'} | ${TemplateContextStrategy} |
||||
|
`('should successfully map $name to $Strategy.name', ({ name, Strategy }) => {
|
||||
|
expect(CONTEXT_STRATEGY[name](undefined)).toEqual(new Strategy(undefined)); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,276 @@ |
|||||
|
import { |
||||
|
Component, |
||||
|
ComponentRef, |
||||
|
EmbeddedViewRef, |
||||
|
TemplateRef, |
||||
|
ViewChild, |
||||
|
ViewContainerRef, |
||||
|
} from '@angular/core'; |
||||
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; |
||||
|
import { |
||||
|
ComponentProjectionStrategy, |
||||
|
ContainerStrategy, |
||||
|
CONTAINER_STRATEGY, |
||||
|
CONTEXT_STRATEGY, |
||||
|
DOM_STRATEGY, |
||||
|
PROJECTION_STRATEGY, |
||||
|
RootComponentProjectionStrategy, |
||||
|
TemplateProjectionStrategy, |
||||
|
} from '../strategies'; |
||||
|
|
||||
|
describe('ComponentProjectionStrategy', () => { |
||||
|
@Component({ |
||||
|
template: '<div class="foo">{{ bar || baz }}</div>', |
||||
|
}) |
||||
|
class TestComponent { |
||||
|
bar: string; |
||||
|
baz = 'baz'; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
template: '<ng-container #container></ng-container>', |
||||
|
}) |
||||
|
class HostComponent { |
||||
|
@ViewChild('container', { static: true, read: ViewContainerRef }) |
||||
|
containerRef: ViewContainerRef; |
||||
|
} |
||||
|
|
||||
|
let containerStrategy: ContainerStrategy; |
||||
|
let spectator: Spectator<HostComponent>; |
||||
|
let componentRef: ComponentRef<TestComponent>; |
||||
|
|
||||
|
const createComponent = createComponentFactory({ |
||||
|
component: HostComponent, |
||||
|
entryComponents: [TestComponent], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createComponent({}); |
||||
|
containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
componentRef.destroy(); |
||||
|
spectator.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
describe('#injectContent', () => { |
||||
|
it('should should insert content into container and return a ComponentRef', () => { |
||||
|
const strategy = new ComponentProjectionStrategy(TestComponent, containerStrategy); |
||||
|
componentRef = strategy.injectContent(spectator); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = spectator.query('div.foo'); |
||||
|
expect(div.textContent).toBe('baz'); |
||||
|
expect(componentRef).toBeInstanceOf(ComponentRef); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to map context to projected component', () => { |
||||
|
const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); |
||||
|
const strategy = new ComponentProjectionStrategy( |
||||
|
TestComponent, |
||||
|
containerStrategy, |
||||
|
contextStrategy, |
||||
|
); |
||||
|
componentRef = strategy.injectContent(spectator); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = spectator.query('div.foo'); |
||||
|
expect(div.textContent).toBe('bar'); |
||||
|
expect(componentRef.instance.bar).toBe('bar'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('RootComponentProjectionStrategy', () => { |
||||
|
@Component({ |
||||
|
template: '<div class="foo">{{ bar || baz }}</div>', |
||||
|
}) |
||||
|
class TestComponent { |
||||
|
bar: string; |
||||
|
baz = 'baz'; |
||||
|
} |
||||
|
|
||||
|
@Component({ template: '' }) |
||||
|
class HostComponent {} |
||||
|
|
||||
|
let spectator: Spectator<HostComponent>; |
||||
|
let componentRef: ComponentRef<TestComponent>; |
||||
|
|
||||
|
const createComponent = createComponentFactory({ |
||||
|
component: HostComponent, |
||||
|
entryComponents: [TestComponent], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createComponent({}); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
componentRef.destroy(); |
||||
|
spectator.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
describe('#injectContent', () => { |
||||
|
it('should should insert content into body and return a ComponentRef', () => { |
||||
|
const strategy = new RootComponentProjectionStrategy(TestComponent); |
||||
|
componentRef = strategy.injectContent(spectator); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = document.querySelector('body > ng-component > div.foo'); |
||||
|
expect(div.textContent).toBe('baz'); |
||||
|
expect(componentRef).toBeInstanceOf(ComponentRef); |
||||
|
componentRef.destroy(); |
||||
|
spectator.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to map context to projected component', () => { |
||||
|
const contextStrategy = CONTEXT_STRATEGY.Component({ bar: 'bar' }); |
||||
|
const strategy = new RootComponentProjectionStrategy(TestComponent, contextStrategy); |
||||
|
componentRef = strategy.injectContent(spectator); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = document.querySelector('body > ng-component > div.foo'); |
||||
|
expect(div.textContent).toBe('bar'); |
||||
|
expect(componentRef.instance.bar).toBe('bar'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('TemplateProjectionStrategy', () => { |
||||
|
@Component({ |
||||
|
template: ` |
||||
|
<ng-template #template let-bar> |
||||
|
<div class="foo">{{ bar || baz }}</div> |
||||
|
</ng-template> |
||||
|
<ng-container #container></ng-container> |
||||
|
`,
|
||||
|
}) |
||||
|
class HostComponent { |
||||
|
@ViewChild('container', { static: true, read: ViewContainerRef }) |
||||
|
containerRef: ViewContainerRef; |
||||
|
|
||||
|
@ViewChild('template', { static: true }) |
||||
|
templateRef: TemplateRef<{ $implicit?: string }>; |
||||
|
|
||||
|
baz = 'baz'; |
||||
|
} |
||||
|
|
||||
|
let containerStrategy: ContainerStrategy; |
||||
|
let spectator: Spectator<HostComponent>; |
||||
|
let embeddedViewRef: EmbeddedViewRef<{ $implicit?: string }>; |
||||
|
|
||||
|
const createComponent = createComponentFactory({ |
||||
|
component: HostComponent, |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createComponent({}); |
||||
|
containerStrategy = CONTAINER_STRATEGY.Clear(spectator.component.containerRef); |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
embeddedViewRef.destroy(); |
||||
|
spectator.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
describe('#injectContent', () => { |
||||
|
it('should should insert content into container and return an EmbeddedViewRef', () => { |
||||
|
const templateRef = spectator.component.templateRef; |
||||
|
const strategy = new TemplateProjectionStrategy(templateRef, containerStrategy); |
||||
|
embeddedViewRef = strategy.injectContent(); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = spectator.query('div.foo'); |
||||
|
expect(div.textContent).toBe('baz'); |
||||
|
expect(embeddedViewRef).toHaveProperty('detectChanges'); |
||||
|
expect(embeddedViewRef).toHaveProperty('markForCheck'); |
||||
|
expect(embeddedViewRef).toHaveProperty('detach'); |
||||
|
expect(embeddedViewRef).toHaveProperty('reattach'); |
||||
|
expect(embeddedViewRef).toHaveProperty('destroy'); |
||||
|
expect(embeddedViewRef).toHaveProperty('rootNodes'); |
||||
|
expect(embeddedViewRef).toHaveProperty('context'); |
||||
|
}); |
||||
|
|
||||
|
it('should be able to map context to projected template', () => { |
||||
|
const templateRef = spectator.component.templateRef; |
||||
|
const contextStrategy = CONTEXT_STRATEGY.Template<typeof templateRef>({ $implicit: 'bar' }); |
||||
|
const strategy = new TemplateProjectionStrategy( |
||||
|
templateRef, |
||||
|
containerStrategy, |
||||
|
contextStrategy, |
||||
|
); |
||||
|
embeddedViewRef = strategy.injectContent(); |
||||
|
spectator.detectChanges(); |
||||
|
|
||||
|
const div = spectator.query('div.foo'); |
||||
|
expect(div.textContent).toBe('bar'); |
||||
|
expect(embeddedViewRef.context).toEqual(contextStrategy.context); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('PROJECTION_STRATEGY', () => { |
||||
|
const content = undefined; |
||||
|
const containerRef = ({ length: 0 } as any) as ViewContainerRef; |
||||
|
let context: any; |
||||
|
|
||||
|
test.each` |
||||
|
name | Strategy | containerStrategy |
||||
|
${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} |
||||
|
${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} |
||||
|
${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} |
||||
|
${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} |
||||
|
${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} |
||||
|
${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} |
||||
|
`(
|
||||
|
'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', |
||||
|
({ name, Strategy, containerStrategy }) => { |
||||
|
expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( |
||||
|
new Strategy(content, containerStrategy(containerRef), CONTEXT_STRATEGY.None()), |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
test.each` |
||||
|
name | Strategy | domStrategy |
||||
|
${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${DOM_STRATEGY.AppendToBody} |
||||
|
`(
|
||||
|
'should successfully map $name to $Strategy.name with $domStrategy.name dom strategy', |
||||
|
({ name, Strategy, domStrategy }) => { |
||||
|
expect(PROJECTION_STRATEGY[name](content, context)).toEqual( |
||||
|
new Strategy(content, CONTEXT_STRATEGY.None(), domStrategy()), |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
test.each` |
||||
|
name | Strategy | containerStrategy | contextStrategy |
||||
|
${'AppendComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Component} |
||||
|
${'AppendTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Append} | ${CONTEXT_STRATEGY.Template} |
||||
|
${'PrependComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Component} |
||||
|
${'PrependTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Prepend} | ${CONTEXT_STRATEGY.Template} |
||||
|
${'ProjectComponentToContainer'} | ${ComponentProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Component} |
||||
|
${'ProjectTemplateToContainer'} | ${TemplateProjectionStrategy} | ${CONTAINER_STRATEGY.Clear} | ${CONTEXT_STRATEGY.Template} |
||||
|
`(
|
||||
|
'should successfully map $name to $Strategy.name with $containerStrategy.name container strategy and $contextStrategy.name context strategy', |
||||
|
({ name, Strategy, containerStrategy, contextStrategy }) => { |
||||
|
context = { x: true }; |
||||
|
expect(PROJECTION_STRATEGY[name](content, containerRef, context)).toEqual( |
||||
|
new Strategy(content, containerStrategy(containerRef), contextStrategy(context)), |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
test.each` |
||||
|
name | Strategy | contextStrategy | domStrategy |
||||
|
${'AppendComponentToBody'} | ${RootComponentProjectionStrategy} | ${CONTEXT_STRATEGY.Component} | ${DOM_STRATEGY.AppendToBody} |
||||
|
`(
|
||||
|
'should successfully map $name to $Strategy.name with $contextStrategy.name context strategy and $domStrategy.name dom strategy', |
||||
|
({ name, Strategy, domStrategy, contextStrategy }) => { |
||||
|
context = { x: true }; |
||||
|
expect(PROJECTION_STRATEGY[name](content, context)).toEqual( |
||||
|
new Strategy(content, contextStrategy(context), domStrategy()), |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
}); |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const enum eFeatureManagementComponents { |
||||
|
FeatureManagement = 'FeatureManagement.FeatureManagementComponent', |
||||
|
} |
||||
@ -1,2 +1,3 @@ |
|||||
export * from './lib/feature-management.module'; |
export * from './lib/feature-management.module'; |
||||
export * from './lib/components'; |
export * from './lib/components'; |
||||
|
export * from './lib/enums/components'; |
||||
|
|||||
@ -0,0 +1,4 @@ |
|||||
|
export const enum eIdentityComponents { |
||||
|
Roles = 'Identity.RolesComponent', |
||||
|
Users = 'Identity.UsersComponent', |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const enum ePermissionManagementComponents { |
||||
|
PermissionManagement = 'PermissionManagement.PermissionManagementComponent', |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
export const enum eSettingManagementComponents { |
||||
|
SettingManagement = 'SettingManagement.SettingManagementComponent', |
||||
|
} |
||||
@ -1,2 +1,3 @@ |
|||||
export * from './lib/setting-management.module'; |
export * from './lib/setting-management.module'; |
||||
export * from './lib/components/setting-management.component'; |
export * from './lib/components/setting-management.component'; |
||||
|
export * from './lib/enums/components'; |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
export const enum eTenantManagementComponents { |
||||
|
Tenants = 'TenantManagement.TenantsComponent', |
||||
|
} |
||||
@ -1,6 +1,7 @@ |
|||||
export * from './lib/tenant-management.module'; |
export * from './lib/tenant-management.module'; |
||||
export * from './lib/actions'; |
export * from './lib/actions'; |
||||
export * from './lib/components'; |
export * from './lib/components'; |
||||
|
export * from './lib/enums/components'; |
||||
export * from './lib/models'; |
export * from './lib/models'; |
||||
export * from './lib/services'; |
export * from './lib/services'; |
||||
export * from './lib/states'; |
export * from './lib/states'; |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
export const enum eThemeBasicComponents { |
||||
|
ApplicationLayout = 'Theme.ApplicationLayoutComponent', |
||||
|
AccountLayout = 'Theme.AccountLayoutComponent', |
||||
|
EmptyLayout = 'Theme.EmptyLayoutComponent', |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue