Merge remote-tracking branch 'abpframework/dev' into docs

pull/3402/head
liangshiwei 6 years ago
commit de1014d44f

@ -86,6 +86,11 @@
"AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?",
"DeleteAll": "Delete all",
"DoYouWantToCreateNewUser": "Do you want to create new user?",
"MasterModules": "Master Modules"
"MasterModules": "Master Modules",
"OrganizationName": "Organization name",
"OrganizationNamePlaceholder": "Organization name...",
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member"
}
}

@ -37,13 +37,14 @@ As mentioned above, all extra properties of an entity are stored as a single JSO
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 `EntityExtensionManager` static class:
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
EntityExtensionManager.AddProperty<IdentityUser, string>(
"SocialSecurityNumber",
b => { b.HasMaxLength(32); }
);
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).

@ -375,7 +375,7 @@ The way to store this dictionary in the database depends on the database provide
* For [Entity Framework Core](Entity-Framework-Core.md), here are two type of configurations;
* By default, it is stored in a single `ExtraProperties` field as a `JSON` string (that means all extra properties stored in a single database table field). Serializing to `JSON` and deserializing from the `JSON` are automatically done by the ABP Framework using the [value conversions](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions) system of the EF Core.
* If you want, you can use the `EntityExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `EntityExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `EntityExtensionManager`.
* If you want, you can use the `ObjectExtensionManager` to define a separate table field for a desired extra property. Properties those are not configured through the `ObjectExtensionManager` will continue to use a single `JSON` field as described above. This feature is especially useful when you are using a pre-built [application module](Modules/Index.md) and want to [extend its entities](Customizing-Application-Modules-Extending-Entities.md). See the [EF Core integration document](Entity-Framework-Core.md) to learn how to use the `ObjectExtensionManager`.
* For [MongoDB](MongoDB.md), it is stored as a **regular field**, since MongoDB naturally supports this kind of [extra elements](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) system.
### Discussion for the Extra Properties

@ -409,18 +409,19 @@ public static class MyProjectNameEntityExtensions
{
OneTimeRunner.Run(() =>
{
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
ObjectExtensionManager.Instance
.MapEfCoreProperty<IdentityRole, string>(
"Title",
builder => { builder.HasMaxLength(64); }
);
});
}
}
````
> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)`.
> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)` or use a constant string.
`EntityExtensionManager` is used to add properties to existing entities. Since `EntityExtensionManager` is static, we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework.
`ObjectExtensionManager` is used to add properties to existing entities. Since `ObjectExtensionManager.Instance` is a static instance (singleton), we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework.
See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system.
@ -543,7 +544,7 @@ In this way, you can easily attach any type of value to an entity of a depended
Entity extension system solves the main problem of the extra properties: It can store an extra property in a **standard table field** in the database.
All you need to do is to use the `EntityExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
###### Creating a New Table

@ -298,56 +298,57 @@ public class BookService
> Important: You must reference to the `Volo.Abp.EntityFrameworkCore` package from the project you want to access to the DbContext. This breaks encapsulation, but this is what you want in that case.
## Extra Properties & Entity Extension Manager
## Extra Properties & Object Extension Manager
Extra Properties system allows you to set/get dynamic properties to entities those implement the `IHasExtraProperties` interface. It is especially useful when you want to add custom properties to the entities defined in an [application module](Modules/Index.md), when you use the module as package reference.
By default, all the extra properties of an entity are stored as a single `JSON` object in the database. Entity extension system allows you to to store desired extra properties in separate fields in the related database table.
By default, all the extra properties of an entity are stored as a single `JSON` object in the database.
For more information about the extra properties & the entity extension system, see the following documents:
Entity extension system allows you to to store desired extra properties in separate fields in the related database table. For more information about the extra properties & the entity extension system, see the following documents:
* [Customizing the Application Modules: Extending Entities](Customizing-Application-Modules-Extending-Entities.md)
* [Entities](Entities.md)
This section only explains the `EntityExtensionManager` and its usage.
This section only explains the EF Core related usage of the `ObjectExtensionManager`.
### AddProperty Method
### ObjectExtensionManager.Instance
`AddProperty` method of the `EntityExtensionManager` allows you to define additional properties for an entity type.
`ObjectExtensionManager` implements the singleton pattern, so you need to use the static `ObjectExtensionManager.Instance` to perform all the operations.
### MapEfCoreProperty
`MapEfCoreProperty` is a shortcut extension method to define an extension property for an entity and map to the database.
**Example**: Add `Title` property (database field) to the `IdentityRole` entity:
````csharp
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
ObjectExtensionManager.Instance
.MapEfCoreProperty<IdentityRole, string>(
"Title",
builder => { builder.HasMaxLength(64); }
);
````
If the related module has implemented this feature (by using the `ConfigureExtensions` explained below), then the new property is added to the model. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field.
If the related module has implemented this feature (by using the `ConfigureEfCoreEntity` explained below), then the new property is added to the model. Then you need to run the standard `Add-Migration` and `Update-Database` commands to update your database to add the new field.
>`AddProperty` method must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEntityExtensions` class that is safe to use this method inside.
>`MapEfCoreProperty` method must be called before using the related `DbContext`. It is a static method. The best way is to use it in your application as earlier as possible. The application startup template has a `YourProjectNameEntityExtensions` class that is safe to use this method inside.
### ConfigureExtensions
### ConfigureEfCoreEntity
If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureExtensions` extension method in your entity mapping:
If you are building a reusable module and want to allow application developers to add properties to your entities, you can use the `ConfigureEfCoreEntity` extension method in your entity mapping. However, there is a shortcut extension method `ConfigureObjectExtensions` that can be used while configuring the entity mapping:
````csharp
builder.Entity<YourEntity>(b =>
{
b.ConfigureExtensions();
b.ConfigureObjectExtensions();
//...
});
````
If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` in this example), ABP Framework internally calls the `ConfigureExtensions` method. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention.
> If you call `ConfigureByConvention()` extension method (like `b.ConfigureByConvention()` for this example), ABP Framework internally calls the `ConfigureObjectExtensions` method. It is a **best practice** to use the `ConfigureByConvention()` method since it also configures database mapping for base properties by convention.
See the "*ConfigureByConvention Method*" section above for more information.
### GetPropertyNames
`EntityExtensionManager.GetPropertyNames` static method can be used the names of the extension properties defined for this entity. It is normally not needed by an application code, but used by the ABP Framework internally.
## Advanced Topics
### Set Default Repository Classes

@ -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,3 @@
# Object Extensions
TODO

@ -2,11 +2,12 @@
## Introduction
This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below:
This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices.
* See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider).
* See the [Angular Application Development Tutorial](../Tutorials/Part-1.md?UI=NG) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider).
This document explains **the solution structure** and projects in details. If you want to start quickly, follow the guides below:
* [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes.
* [The application development tutorial](../Tutorials/Part-1) explains step by step application development.
## How to Start With?
@ -123,6 +124,8 @@ Notice that the migration `DbContext` is only used for database migrations and *
* Depends on the `.EntityFrameworkCore` project since it re-uses the configuration defined for the `DbContext` of the application.
> This project is available only if you are using EF Core as the database provider.
>
> See the [Entity Framework Core Migrations Guide](../Entity-Framework-Core-Migrations.md) to understand this project in details.
#### .DbMigrator Project
@ -269,5 +272,5 @@ The files under the `angular/src/environments` folder has the essential configur
## What's Next?
- See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template.
- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md) to learn how to develop applications using this template.
- [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes.
- [The application development tutorial](../Tutorials/Part-1) explains step by step application development.

@ -1,10 +1,10 @@
# Component Replacement
## 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
### 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`.
@ -29,7 +29,54 @@ export class AppComponent {
![Example Usage](./images/component-replacement.gif)
## Available Replaceable Components
### 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 { 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: 'Theme.ApplicationLayoutComponent',
}),
);
//...
}
}
```
### Available Replaceable Components
| Component key | Description |
| -------------------------------------------------- | --------------------------------------------- |

@ -288,3 +288,7 @@ Note that **you do not have to call this method at application initiation**, bec
#### 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)

@ -26,7 +26,7 @@ The constructor does not get any parameters.
### How to Add New Nodes
There are a few methods to create new nodes in a linked list and all of them are separately available as well as revealed from an `add` method.
There are several methods to create new nodes in a linked list and all of them are separately available as well as revealed by `add` and `addMany` methods.
@ -50,6 +50,22 @@ list.addHead('c');
#### addManyHead(values: T\[\]): ListNode\<T\>\[\]
Adds multiple nodes with given values as the first nodes in list:
```js
list.addManyHead(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
list.addManyHead(['x', 'y', 'z']);
// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c"
```
#### addTail(value: T): ListNode\<T\>
Adds a node with given value as the last node in list:
@ -70,6 +86,22 @@ list.addTail('c');
#### addManyTail(values: T\[\]): ListNode\<T\>\[\]
Adds multiple nodes with given values as the last nodes in list:
```js
list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
list.addManyTail(['x', 'y', 'z']);
// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z"
```
#### addAfter(value: T, previousValue: T, compareFn = compare): ListNode\<T\>
Adds a node with given value after the first node that has the previous value:
@ -109,6 +141,40 @@ list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addManyAfter(values: T\[\], previousValue: T, compareFn = compare): ListNode\<T\>\[\]
Adds multiple nodes with given values after the first node that has the previous value:
```js
list.addManyTail(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
list.addManyAfter(['x', 'y'], 'b');
// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]);
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addManyAfter([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3}
```
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### addBefore(value: T, nextValue: T, compareFn = compare): ListNode\<T\>
Adds a node with given value before the first node that has the next value:
@ -148,6 +214,40 @@ list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
#### addManyBefore(values: T\[\], nextValue: T, compareFn = compare): ListNode\<T\>\[\]
Adds multiple nodes with given values before the first node that has the next value:
```js
list.addManyTail(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
list.addManyBefore(['x', 'y'], 'b');
// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addManyTail([{ x: 1 },{ x: 2 },{ x: 3 }]);
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addManyBefore([{ x: 4 }, { x: 5 }], { x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3}
```
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### addByIndex(value: T, position: number): ListNode\<T\>
Adds a node with given value at the specified position in the list:
@ -166,6 +266,52 @@ list.addByIndex('x', 2);
It works with negative index too:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
list.addByIndex('x', -1);
// "a" <-> "b" <-> "x" <-> "c"
```
#### addManyByIndex(values: T\[\], position: number): ListNode\<T\>\[\]
Adds multiple nodes with given values at the specified position in the list:
```js
list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
list.addManyByIndex(['x', 'y'], 2);
// "a" <-> "b" <-> "x" <-> "y" <-> "c"
```
It works with negative index too:
```js
list.addManyTail(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
list.addManyByIndex(['x', 'y'], -1);
// "a" <-> "b" <-> "x" <-> "y" <-> "c"
```
#### add(value: T).head(): ListNode\<T\>
Adds a node with given value as the first node in list:
@ -314,10 +460,172 @@ list.add('x').byIndex(2);
It works with negative index too:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').byIndex(-1);
// "a" <-> "b" <-> "x" <-> "c"
```
> This is an alternative API for `addByIndex`.
#### addMany(values: T\[\]).head(): ListNode\<T\>\[\]
Adds multiple nodes with given values as the first nodes in list:
```js
list.addMany(['a', 'b', 'c']).head();
// "a" <-> "b" <-> "c"
list.addMany(['x', 'y', 'z']).head();
// "x" <-> "y" <-> "z" <-> "a" <-> "b" <-> "c"
```
> This is an alternative API for `addManyHead`.
#### addMany(values: T\[\]).tail(): ListNode\<T\>\[\]
Adds multiple nodes with given values as the last nodes in list:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.addMany(['x', 'y', 'z']).tail();
// "a" <-> "b" <-> "c" <-> "x" <-> "y" <-> "z"
```
> This is an alternative API for `addManyTail`.
#### addMany(values: T\[\]).after(previousValue: T, compareFn = compare): ListNode\<T\>\[\]
Adds multiple nodes with given values after the first node that has the previous value:
```js
list.addMany(['a', 'b', 'b', 'c']).tail();
// "a" <-> "b" <-> "b" <-> "c"
list.addMany(['x', 'y']).after('b');
// "a" <-> "b" <-> "x" <-> "y" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addMany([{ x: 4 }, { x: 5 }]).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":4} <-> {"x":5} <-> {"x":3}
```
> This is an alternative API for `addManyAfter`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### addMany(values: T\[\]).before(nextValue: T, compareFn = compare): ListNode\<T\>\[\]
Adds multiple nodes with given values before the first node that has the next value:
```js
list.addMany(['a', 'b', 'b', 'c']).tail();
// "a" <-> "b" <-> "b" <-> "c"
list.addMany(['x', 'y']).before('b');
// "a" <-> "x" <-> "y" <-> "b" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addMany([{ x: 4 }, { x: 5 }]).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":4} <-> {"x":5} <-> {"x":2} <-> {"x":3}
```
> This is an alternative API for `addManyBefore`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### addMany(values: T\[\]).byIndex(position: number): ListNode\<T\>\[\]
Adds multiple nodes with given values at the specified position in the list:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.addMany(['x', 'y']).byIndex(2);
// "a" <-> "b" <-> "x" <-> "y" <-> "c"
```
It works with negative index too:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.addMany(['x', 'y']).byIndex(-1);
// "a" <-> "b" <-> "x" <-> "y" <-> "c"
```
> This is an alternative API for `addManyByIndex`.
### How to Remove Nodes
There are a few methods to remove nodes from a linked list and all of them are separately available as well as revealed from a `drop` method.
@ -329,9 +637,7 @@ There are a few methods to remove nodes from a linked list and all of them are s
Removes the first node from the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -342,14 +648,28 @@ list.dropHead();
#### dropManyHead(count: number): ListNode\<T\>\[\]
Removes the first nodes from the list based on given count:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.dropManyHead(2);
// "c"
```
#### dropTail(): ListNode\<T\> | undefined
Removes the last node from the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -360,14 +680,28 @@ list.dropTail();
#### dropManyTail(count: number): ListNode\<T\>\[\]
Removes the last nodes from the list based on given count:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.dropManyTail(2);
// "a"
```
#### dropByIndex(position: number): ListNode\<T\> | undefined
Removes the node with the specified position from the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -378,16 +712,56 @@ list.dropByIndex(1);
It works with negative index too:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.dropByIndex(-2);
// "a" <-> "c"
```
#### dropManyByIndex(count: number, position: number): ListNode\<T\>\[\]
Removes the nodes starting from the specified position from the list based on given count:
```js
list.addMany(['a', 'b', 'c', 'd']).tail();
// "a" <-> "b" <-> "c" <-> "d
list.dropManyByIndex(2, 1);
// "a" <-> "d"
```
It works with negative index too:
```js
list.addMany(['a', 'b', 'c', 'd']).tail();
// "a" <-> "b" <-> "c" <-> "d
list.dropManyByIndex(2, -2);
// "a" <-> "d"
```
#### dropByValue(value: T, compareFn = compare): ListNode\<T\> | undefined
Removes the first node with given value from the list:
```js
list.addTail('a');
list.addTail('x');
list.addTail('b');
list.addTail('x');
list.addTail('c');
list.addMany(['a', 'x', 'b', 'x', 'c']).tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
@ -401,11 +775,7 @@ list.dropByValue('x');
You may pass a custom compare function to detect the searched value:
```js
list.addTail({ x: 1 });
list.addTail({ x: 0 });
list.addTail({ x: 2 });
list.addTail({ x: 0 });
list.addTail({ x: 3 });
list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@ -425,11 +795,7 @@ list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
Removes all nodes with given value from the list:
```js
list.addTail('a');
list.addTail('x');
list.addTail('b');
list.addTail('x');
list.addTail('c');
list.addMany(['a', 'x', 'b', 'x', 'c']).tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
@ -443,11 +809,7 @@ list.dropByValueAll('x');
You may pass a custom compare function to detect the searched value:
```js
list.addTail({ x: 1 });
list.addTail({ x: 0 });
list.addTail({ x: 2 });
list.addTail({ x: 0 });
list.addTail({ x: 3 });
list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@ -467,9 +829,7 @@ list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
Removes the first node in list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -489,9 +849,7 @@ list.drop().head();
Removes the last node in list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -511,9 +869,7 @@ list.drop().tail();
Removes the node with the specified position from the list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
@ -524,6 +880,20 @@ list.drop().byIndex(1);
It works with negative index too:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.drop().byIndex(-2);
// "a" <-> "c"
```
> This is an alternative API for `dropByIndex`.
@ -533,11 +903,7 @@ list.drop().byIndex(1);
Removes the first node with given value from the list:
```js
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
list.addMany(['a', 'x', 'b', 'x', 'c']).tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
@ -551,11 +917,7 @@ list.drop().byValue('x');
You may pass a custom compare function to detect the searched value:
```js
list.add({ x: 1 }).tail();
list.add({ x: 0 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 0 }).tail();
list.add({ x: 3 }).tail();
list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@ -577,11 +939,7 @@ list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
Removes all nodes with given value from the list:
```js
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
list.addMany(['a', 'x', 'b', 'x', 'c']).tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
@ -595,11 +953,7 @@ list.drop().byValueAll('x');
You may pass a custom compare function to detect the searched value:
```js
list.add({ x: 1 }).tail();
list.add({ x: 0 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 0 }).tail();
list.add({ x: 3 }).tail();
list.addMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]).tail();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@ -616,6 +970,80 @@ list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x);
#### dropMany(count: number).head(): ListNode\<T\>\[\]
Removes the first nodes from the list based on given count:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.dropMany(2).head();
// "c"
```
> This is an alternative API for `dropManyHead`.
#### dropMany(count: number).tail(): ListNode\<T\>\[\]
Removes the last nodes from the list based on given count:
```js
list.addMany(['a', 'b', 'c']).tail();
// "a" <-> "b" <-> "c"
list.dropMany(2).tail();
// "a"
```
> This is an alternative API for `dropManyTail`.
#### dropMany(count: number).byIndex(position: number): ListNode\<T\>\[\]
Removes the nodes starting from the specified position from the list based on given count:
```js
list.addMany(['a', 'b', 'c', 'd']).tail();
// "a" <-> "b" <-> "c" <-> "d
list.dropMany(2).byIndex(1);
// "a" <-> "d"
```
It works with negative index too:
```js
list.addMany(['a', 'b', 'c', 'd']).tail();
// "a" <-> "b" <-> "c" <-> "d
list.dropMany(2).byIndex(-2);
// "a" <-> "d"
```
> This is an alternative API for `dropManyByIndex`.
### How to Find Nodes
There are a few methods to find specific nodes in a linked list.
@ -627,10 +1055,7 @@ There are a few methods to find specific nodes in a linked list.
Finds the first node from the list that matches the given predicate:
```js
list.addTail('a');
list.addTail('b');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@ -650,10 +1075,7 @@ found.next.value === "b"
Finds the position of the first node from the list that matches the given predicate:
```js
list.addTail('a');
list.addTail('b');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@ -677,9 +1099,7 @@ i3 === -1
Finds and returns the node with specific position in the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@ -699,10 +1119,7 @@ found.next.value === "c"
Finds the position of the first node from the list that has the given value:
```js
list.addTail('a');
list.addTail('b');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'b', 'c']);
// "a" <-> "b" <-> "b" <-> "c"
@ -724,11 +1141,7 @@ i3 === -1
You may pass a custom compare function to detect the searched value:
```js
list.addTail({ x: 1 });
list.addTail({ x: 0 });
list.addTail({ x: 2 });
list.addTail({ x: 0 });
list.addTail({ x: 3 });
list.addTailMany([{ x: 1 }, { x: 0 }, { x: 2 }, { x: 0 }, { x: 3 }]);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
@ -764,9 +1177,7 @@ There are a few ways to iterate over or display a linked list.
Runs a callback function on all nodes in a linked list from head to tail:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@ -784,9 +1195,7 @@ list.forEach((node, index) => console.log(node.value + index));
A linked list is iterable. In other words, you may use methods like `for...of` on it.
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@ -801,14 +1210,12 @@ for(const node of list) {
#### toArray(): T[]
#### toArray(): T\[\]
Converts a linked list to an array:
Converts a linked list to an array of values:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
list.addTailMany(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
@ -821,15 +1228,32 @@ arr === ['a', 'b', 'c']
#### toNodeArray(): T\[\]
Converts a linked list to an array of nodes:
```js
list.addTailMany(['a', 'b', 'c']);
// "a" <-> "b" <-> "c"
const arr = list.toNodeArray();
/*
arr[0].value === 'a'
arr[1].value === 'a'
arr[2].value === 'a'
*/
```
#### toString(): string
Converts a linked list to a string representation of nodes and their relations:
```js
list.addTail('a');
list.addTail(2);
list.addTail('c');
list.addTail({ k: 4, v: 'd' });
list.addTailMany(['a', 2, 'c', { k: 4, v: 'd' }]);
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
@ -842,3 +1266,19 @@ str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}'
You may pass a custom mapper function to map values before stringifying them:
```js
list.addMany([{ x: 1 }, { x: 2 }, { x: 3 }, { x: 4 }, { x: 5 }]).tail();
// {"x":1} <-> {"x":2} <-> {"x":3} <-> {"x":4} <-> {"x":5}
const str = list.toString(value => value.x);
/*
str === '1 <-> 2 <-> 3 <-> 4 <-> 5'
*/
```

@ -76,4 +76,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`.
## What's Next?
* [Component Replacement](./Component-Replacement.md)
* [Config State](./Config-State.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:
![generated-files-via-generate-proxy](./images/generated-files-via-generate-proxy.png)
### 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

@ -5,6 +5,7 @@
"items": [
{
"text": "From Startup Templates",
"path": "Getting-Started-With-Startup-Templates.md",
"items": [
{
"text": "Application with MVC (Razor Pages) UI",
@ -312,6 +313,10 @@
{
"text": "Angular",
"items": [
{
"text": "Service Proxies",
"path": "UI/Angular/Service-Proxies.md"
},
{
"text": "HTTP Requests",
"path": "UI/Angular/HTTP-Requests.md"

@ -273,7 +273,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo", "test\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.csproj", "{0C498CF2-D052-4BF7-AD35-509A90F69707}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Http.Client.IdentityModel.Web.Tests", "test\Volo.Abp.Http.Client.IdentityModel.Web.Tests\Volo.Abp.Http.Client.IdentityModel.Web.Tests.csproj", "{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Http.Client.IdentityModel.Web.Tests", "test\Volo.Abp.Http.Client.IdentityModel.Web.Tests\Volo.Abp.Http.Client.IdentityModel.Web.Tests.csproj", "{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.ObjectExtending", "src\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj", "{D1815C77-16D6-4F99-8814-69065CD89FB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.ObjectExtending.Tests", "test\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj", "{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -817,6 +821,14 @@ Global
{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1963439-2BE5-4DB5-8438-2A9A792A1ADA}.Release|Any CPU.Build.0 = Release|Any CPU
{D1815C77-16D6-4F99-8814-69065CD89FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1815C77-16D6-4F99-8814-69065CD89FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1815C77-16D6-4F99-8814-69065CD89FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1815C77-16D6-4F99-8814-69065CD89FB3}.Release|Any CPU.Build.0 = Release|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -956,6 +968,8 @@ Global
{29E42ADB-85F8-44AE-A9B0-078F84C1B866} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{0C498CF2-D052-4BF7-AD35-509A90F69707} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{E1963439-2BE5-4DB5-8438-2A9A792A1ADA} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{D1815C77-16D6-4F99-8814-69065CD89FB3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{17F8CA89-D9A2-4863-A5BD-B8E4D2901FD5} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

@ -0,0 +1,36 @@
using System.Collections.Generic;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace AutoMapper
{
public static class AbpAutoMapperExtensibleDtoExtensions
{
public static IMappingExpression<TSource, TDestination> MapExtraProperties<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
where TDestination : IHasExtraProperties
where TSource : IHasExtraProperties
{
return mappingExpression
.ForMember(
x => x.ExtraProperties,
y => y.MapFrom(
(source, destination, extraProps) =>
{
var result = extraProps.IsNullOrEmpty()
? new Dictionary<string, object>()
: new Dictionary<string, object>(extraProps);
HasExtraPropertiesObjectExtendingExtensions
.MapExtraPropertiesTo<TSource, TDestination>(
source.ExtraProperties,
result
);
return result;
})
);
}
}
}

@ -15,6 +15,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj" />
<ProjectReference Include="..\Volo.Abp.ObjectMapping\Volo.Abp.ObjectMapping.csproj" />
</ItemGroup>

@ -3,11 +3,14 @@ using AutoMapper;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.AutoMapper
{
[DependsOn(typeof(AbpObjectMappingModule))]
[DependsOn(
typeof(AbpObjectMappingModule),
typeof(AbpObjectExtendingModule))]
public class AbpAutoMapperModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

@ -121,6 +121,21 @@ namespace Volo.Abp
return value;
}
[ContractAnnotation("type:null => halt")]
public static Type AssignableTo<TBaseType>(
Type type,
[InvokerParameterName] [NotNull] string parameterName)
{
NotNull(type, parameterName);
if (!type.IsAssignableTo<TBaseType>())
{
throw new ArgumentException($"{parameterName} (type of {type.AssemblyQualifiedName}) should be assignable to the {typeof(TBaseType).GetFullNameWithAssemblyName()}!");
}
return type;
}
public static string Length(
[CanBeNull] string value,
[InvokerParameterName] [NotNull] string parameterName,

@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
<ProjectReference Include="..\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj" />
<ProjectReference Include="..\Volo.Abp.Uow\Volo.Abp.Uow.csproj" />
</ItemGroup>

@ -2,13 +2,15 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Uow;
namespace Volo.Abp.Data
{
[DependsOn(
typeof(AbpObjectExtendingModule),
typeof(AbpUnitOfWorkModule)
)]
)]
public class AbpDataModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)

@ -5,6 +5,7 @@ namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It has the <see cref="Creator"/> and <see cref="LastModifier"/> objects as a DTOs represent the related user.
/// </summary>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
@ -19,6 +20,7 @@ namespace Volo.Abp.Application.Dtos
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It has the <see cref="Creator"/> and <see cref="LastModifier"/> objects as a DTOs represent the related user.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>

@ -5,6 +5,7 @@ namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject{TCreator}"/> interface.
/// It also has the <see cref="Creator"/> object as a DTO represents the user.
/// </summary>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
@ -14,7 +15,8 @@ namespace Volo.Abp.Application.Dtos
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObjectObject{TCreator}"/> interface.
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject{TCreator}"/> interface.
/// It also has the <see cref="Creator"/> object as a DTO represents the user.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>

@ -0,0 +1,35 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
[Serializable]
public abstract class ExtensibleAuditedEntityDto<TPrimaryKey> : ExtensibleCreationAuditedEntityDto<TPrimaryKey>, IAuditedObject
{
/// <inheritdoc />
public DateTime? LastModificationTime { get; set; }
/// <inheritdoc />
public Guid? LastModifierId { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
[Serializable]
public abstract class ExtensibleAuditedEntityDto : ExtensibleCreationAuditedEntityDto, IAuditedObject
{
/// <inheritdoc />
public DateTime? LastModificationTime { get; set; }
/// <inheritdoc />
public Guid? LastModifierId { get; set; }
}
}

@ -0,0 +1,40 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It has the <see cref="Creator"/> and <see cref="LastModifier"/> objects as a DTOs represent the related user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
public abstract class ExtensibleAuditedEntityWithUserDto<TPrimaryKey, TUserDto> : ExtensibleAuditedEntityDto<TPrimaryKey>, IAuditedObject<TUserDto>
{
/// <inheritdoc />
public TUserDto Creator { get; set; }
/// <inheritdoc />
public TUserDto LastModifier { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IAuditedObject"/> interface.
/// It has the <see cref="Creator"/> and <see cref="LastModifier"/> objects as a DTOs represent the related user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
public abstract class ExtensibleAuditedEntityWithUserDto<TUserDto> : ExtensibleAuditedEntityDto,
IAuditedObject<TUserDto>
{
/// <inheritdoc />
public TUserDto Creator { get; set; }
/// <inheritdoc />
public TUserDto LastModifier { get; set; }
}
}

@ -0,0 +1,35 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
[Serializable]
public abstract class ExtensibleCreationAuditedEntityDto<TPrimaryKey> : ExtensibleEntityDto<TPrimaryKey>, ICreationAuditedObject
{
/// <inheritdoc />
public DateTime CreationTime { get; set; }
/// <inheritdoc />
public Guid? CreatorId { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
[Serializable]
public abstract class ExtensibleCreationAuditedEntityDto : ExtensibleEntityDto, ICreationAuditedObject
{
/// <inheritdoc />
public DateTime CreationTime { get; set; }
/// <inheritdoc />
public Guid? CreatorId { get; set; }
}
}

@ -0,0 +1,32 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject{TCreator}"/> interface.
/// It has the <see cref="Creator"/> object as a DTO represents the user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
public abstract class ExtensibleCreationAuditedEntityWithUserDto<TPrimaryKey, TUserDto> : ExtensibleCreationAuditedEntityDto<TPrimaryKey>, ICreationAuditedObject<TUserDto>
{
public TUserDto Creator { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="ICreationAuditedObject{TCreator}"/> interface.
/// It has the <see cref="Creator"/> object as a DTO represents the user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TUserDto">Type of the User DTO</typeparam>
[Serializable]
public abstract class ExtensibleCreationAuditedEntityWithUserDto<TUserDto> : ExtensibleCreationAuditedEntityDto,
ICreationAuditedObject<TUserDto>
{
public TUserDto Creator { get; set; }
}
}

@ -0,0 +1,28 @@
using System;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Application.Dtos
{
[Serializable]
public abstract class ExtensibleEntityDto<TKey> : ExtensibleObject, IEntityDto<TKey>
{
/// <summary>
/// Id of the entity.
/// </summary>
public TKey Id { get; set; }
public override string ToString()
{
return $"[DTO: {GetType().Name}] Id = {Id}";
}
}
[Serializable]
public abstract class ExtensibleEntityDto : ExtensibleObject, IEntityDto
{
public override string ToString()
{
return $"[DTO: {GetType().Name}]";
}
}
}

@ -0,0 +1,41 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
[Serializable]
public abstract class ExtensibleFullAuditedEntityDto<TPrimaryKey> : ExtensibleAuditedEntityDto<TPrimaryKey>, IFullAuditedObject
{
/// <inheritdoc />
public bool IsDeleted { get; set; }
/// <inheritdoc />
public Guid? DeleterId { get; set; }
/// <inheritdoc />
public DateTime? DeletionTime { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject"/> interface.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
[Serializable]
public abstract class ExtensibleFullAuditedEntityDto : ExtensibleAuditedEntityDto, IFullAuditedObject
{
/// <inheritdoc />
public bool IsDeleted { get; set; }
/// <inheritdoc />
public Guid? DeleterId { get; set; }
/// <inheritdoc />
public DateTime? DeletionTime { get; set; }
}
}

@ -0,0 +1,46 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject{TUser}"/> interface.
/// It has the <see cref="Creator"/>, <see cref="LastModifier"/> and <see cref="Deleter"/> objects as a DTOs represent the related user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User</typeparam>
[Serializable]
public abstract class ExtensibleFullAuditedEntityWithUserDto<TPrimaryKey, TUserDto> : ExtensibleFullAuditedEntityDto<TPrimaryKey>, IFullAuditedObject<TUserDto>
{
/// <inheritdoc />
public TUserDto Creator { get; set; }
/// <inheritdoc />
public TUserDto LastModifier { get; set; }
/// <inheritdoc />
public TUserDto Deleter { get; set; }
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject{TUser}"/> interface.
/// It has the <see cref="Creator"/>, <see cref="LastModifier"/> and <see cref="Deleter"/> objects as a DTOs represent the related user.
/// It also implements the <see cref="IHasExtraProperties"/> interface.
/// </summary>
/// <typeparam name="TUserDto">Type of the User</typeparam>
[Serializable]
public abstract class ExtensibleFullAuditedEntityWithUserDto<TUserDto> : ExtensibleFullAuditedEntityDto,
IFullAuditedObject<TUserDto>
{
/// <inheritdoc />
public TUserDto Creator { get; set; }
/// <inheritdoc />
public TUserDto LastModifier { get; set; }
/// <inheritdoc />
public TUserDto Deleter { get; set; }
}
}

@ -4,7 +4,8 @@ using Volo.Abp.Auditing;
namespace Volo.Abp.Application.Dtos
{
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObjectObject{TUser}"/> interface.
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject{TUser}"/> interface.
/// It has the <see cref="Creator"/>, <see cref="LastModifier"/> and <see cref="Deleter"/> objects as a DTOs represent the related user.
/// </summary>
/// <typeparam name="TUserDto">Type of the User</typeparam>
[Serializable]
@ -21,7 +22,8 @@ namespace Volo.Abp.Application.Dtos
}
/// <summary>
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObjectObject{TUser}"/> interface.
/// This class can be inherited by DTO classes to implement <see cref="IFullAuditedObject{TUser}"/> interface.
/// It has the <see cref="Creator"/>, <see cref="LastModifier"/> and <see cref="Deleter"/> objects as a DTOs represent the related user.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of primary key</typeparam>
/// <typeparam name="TUserDto">Type of the User</typeparam>

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
</ItemGroup>
</Project>

@ -18,11 +18,11 @@ using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Reflection;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
@ -183,10 +183,18 @@ namespace Volo.Abp.EntityFrameworkCore
return;
}
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType);
if (objectExtension == null)
{
return;
}
foreach (var propertyName in propertyNames)
foreach (var property in objectExtension.GetProperties())
{
if (!property.IsMappedToFieldForEfCore())
{
continue;
}
/* Checking "currentValue != null" has a good advantage:
* Assume that you we already using a named extra property,
* then decided to create a field (entity extension) for it.
@ -194,10 +202,10 @@ namespace Volo.Abp.EntityFrameworkCore
* updates the field on the next save!
*/
var currentValue = e.Entry.CurrentValues[propertyName];
var currentValue = e.Entry.CurrentValues[property.Name];
if (currentValue != null)
{
entity.SetProperty(propertyName, currentValue);
entity.SetProperty(property.Name, currentValue);
}
}
}
@ -251,12 +259,21 @@ namespace Volo.Abp.EntityFrameworkCore
{
return;
}
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
foreach (var propertyName in propertyNames)
var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType);
if (objectExtension == null)
{
return;
}
foreach (var property in objectExtension.GetProperties())
{
entry.Property(propertyName).CurrentValue = entity.GetProperty(propertyName);
if (!entity.HasProperty(property.Name))
{
continue;
}
entry.Property(property.Name).CurrentValue = entity.GetProperty(property.Name);
}
}

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace Volo.Abp.EntityFrameworkCore.Extensions
{
public class EntityExtensionInfo
{
public Dictionary<string, PropertyExtensionInfo> Properties { get; set; }
public EntityExtensionInfo()
{
Properties = new Dictionary<string, PropertyExtensionInfo>();
}
}
}

@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp.Data;
namespace Volo.Abp.EntityFrameworkCore.Extensions
{
public static class EntityExtensionManager
{
private static readonly Dictionary<Type, EntityExtensionInfo> ExtensionInfos;
static EntityExtensionManager()
{
ExtensionInfos = new Dictionary<Type, EntityExtensionInfo>();
}
/// <summary>
/// Adds an extension property for an entity.
/// If it is already added, replaces the <paramref name="propertyBuildAction"/>
/// by the given one!
/// </summary>
/// <typeparam name="TEntity">Type of the entity</typeparam>
/// <typeparam name="TProperty">Type of the new property</typeparam>
/// <param name="propertyName">Name of the property</param>
/// <param name="propertyBuildAction">An action to configure the database mapping for the new property</param>
public static void AddProperty<TEntity, TProperty>(
[NotNull]string propertyName,
[NotNull]Action<PropertyBuilder> propertyBuildAction)
{
AddProperty(
typeof(TEntity),
typeof(TProperty),
propertyName,
propertyBuildAction
);
}
/// <summary>
/// Adds an extension property for an entity.
/// If it is already added, replaces the <paramref name="propertyBuildAction"/>
/// by the given one!
/// </summary>
/// <param name="entityType">Type of the entity</param>
/// <param name="propertyType">Type of the new property</param>
/// <param name="propertyName">Name of the property</param>
/// <param name="propertyBuildAction">An action to configure the database mapping for the new property</param>
public static void AddProperty(
Type entityType,
Type propertyType,
[NotNull]string propertyName,
[NotNull]Action<PropertyBuilder> propertyBuildAction)
{
Check.NotNull(entityType, nameof(entityType));
Check.NotNull(propertyType, nameof(propertyType));
Check.NotNullOrWhiteSpace(propertyName, nameof(propertyName));
Check.NotNull(propertyBuildAction, nameof(propertyBuildAction));
var extensionInfo = ExtensionInfos
.GetOrAdd(entityType, () => new EntityExtensionInfo());
var propertyExtensionInfo = extensionInfo.Properties
.GetOrAdd(propertyName, () => new PropertyExtensionInfo(propertyType));
propertyExtensionInfo.Action = propertyBuildAction;
}
/// <summary>
/// Configures the entity mapping for the defined extensions.
/// </summary>
/// <typeparam name="TEntity">The entity tye</typeparam>
/// <param name="entityTypeBuilder">Entity type builder</param>
public static void ConfigureExtensions<TEntity>(
[NotNull] this EntityTypeBuilder<TEntity> entityTypeBuilder)
where TEntity : class, IHasExtraProperties
{
ConfigureExtensions(typeof(TEntity), entityTypeBuilder);
}
/// <summary>
/// Configures the entity mapping for the defined extensions.
/// </summary>
/// <param name="entityType">Type of the entity</param>
/// <param name="entityTypeBuilder">Entity type builder</param>
public static void ConfigureExtensions(
[NotNull] Type entityType,
[NotNull] EntityTypeBuilder entityTypeBuilder)
{
Check.NotNull(entityType, nameof(entityType));
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
var entityExtensionInfo = ExtensionInfos.GetOrDefault(entityType);
if (entityExtensionInfo == null)
{
return;
}
foreach (var propertyExtensionInfo in entityExtensionInfo.Properties)
{
var propertyName = propertyExtensionInfo.Key;
var propertyType = propertyExtensionInfo.Value.PropertyType;
/* Prevent multiple calls to the entityTypeBuilder.Property(...) method */
if (entityTypeBuilder.Metadata.FindProperty(propertyName) != null)
{
continue;
}
var property = entityTypeBuilder.Property(
propertyType,
propertyName
);
propertyExtensionInfo.Value.Action(property);
}
}
public static string[] GetPropertyNames(Type entityType)
{
var entityExtensionInfo = ExtensionInfos.GetOrDefault(entityType);
if (entityExtensionInfo == null)
{
return Array.Empty<string>();
}
return entityExtensionInfo
.Properties
.Select(p => p.Key)
.ToArray();
}
}
}

@ -1,17 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Volo.Abp.EntityFrameworkCore.Extensions
{
public class PropertyExtensionInfo
{
public Action<PropertyBuilder> Action { get; set; }
public Type PropertyType { get; }
public PropertyExtensionInfo(Type propertyType)
{
PropertyType = propertyType;
}
}
}

@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.EntityFrameworkCore.ValueComparers;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.EntityFrameworkCore.Modeling
{
@ -18,6 +18,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
{
b.TryConfigureConcurrencyStamp();
b.TryConfigureExtraProperties();
b.TryConfigureObjectExtensions();
b.TryConfigureMayHaveCreator();
b.TryConfigureMustHaveCreator();
b.TryConfigureSoftDelete();
@ -54,15 +55,30 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
public static void TryConfigureExtraProperties(this EntityTypeBuilder b)
{
//TODO: Max length?
if (b.Metadata.ClrType.IsAssignableTo<IHasExtraProperties>())
if (!b.Metadata.ClrType.IsAssignableTo<IHasExtraProperties>())
{
b.Property<Dictionary<string, object>>(nameof(IHasExtraProperties.ExtraProperties))
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType))
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, object>());
return;
}
EntityExtensionManager.ConfigureExtensions(b.Metadata.ClrType, b);
b.Property<Dictionary<string, object>>(nameof(IHasExtraProperties.ExtraProperties))
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType))
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, object>());
b.TryConfigureObjectExtensions();
}
public static void ConfigureObjectExtensions<T>(this EntityTypeBuilder<T> b)
where T : class, IHasExtraProperties
{
b.As<EntityTypeBuilder>().TryConfigureObjectExtensions();
}
public static void TryConfigureObjectExtensions(this EntityTypeBuilder b)
{
if (b.Metadata.ClrType.IsAssignableTo<IHasExtraProperties>())
{
ObjectExtensionManager.Instance.ConfigureEfCoreEntity(b);
}
}
@ -286,7 +302,6 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
b.As<EntityTypeBuilder>().TryConfigureConcurrencyStamp();
}
//TODO: Add other interfaces (IAuditedObject<TUser>...)
}
}

@ -2,7 +2,7 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
@ -22,11 +22,16 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters
if (entityType != null)
{
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
foreach (var propertyName in propertyNames)
var objectExtension = ObjectExtensionManager.Instance.GetOrNull(entityType);
if (objectExtension != null)
{
copyDictionary.Remove(propertyName);
foreach (var property in objectExtension.GetProperties())
{
if (property.IsMappedToFieldForEfCore())
{
copyDictionary.Remove(property.Name);
}
}
}
}

@ -0,0 +1,41 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Volo.Abp.ObjectExtending
{
public static class EfCoreObjectExtensionInfoExtensions
{
public static ObjectExtensionInfo MapEfCoreProperty<TProperty>(
[NotNull] this ObjectExtensionInfo objectExtensionInfo,
[NotNull] string propertyName,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction)
{
return objectExtensionInfo.MapEfCoreProperty(
typeof(TProperty),
propertyName,
propertyBuildAction
);
}
public static ObjectExtensionInfo MapEfCoreProperty(
[NotNull] this ObjectExtensionInfo objectExtensionInfo,
[NotNull] Type propertyType,
[NotNull] string propertyName,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction)
{
Check.NotNull(objectExtensionInfo, nameof(objectExtensionInfo));
return objectExtensionInfo.AddOrUpdateProperty(
propertyType,
propertyName,
options =>
{
options.MapEfCore(
propertyBuildAction
);
}
);
}
}
}

@ -0,0 +1,80 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
namespace Volo.Abp.ObjectExtending
{
public static class EfCoreObjectExtensionManagerExtensions
{
public static ObjectExtensionManager MapEfCoreProperty<TEntity, TProperty>(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] string propertyName,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction = null)
where TEntity : IHasExtraProperties, IEntity
{
return objectExtensionManager.MapEfCoreProperty(
typeof(TEntity),
typeof(TProperty),
propertyName,
propertyBuildAction
);
}
public static ObjectExtensionManager MapEfCoreProperty(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] Type entityType,
[NotNull] Type propertyType,
[NotNull] string propertyName,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction = null)
{
Check.NotNull(objectExtensionManager, nameof(objectExtensionManager));
return objectExtensionManager.AddOrUpdateProperty(
entityType,
propertyType,
propertyName,
options =>
{
options.MapEfCore(
propertyBuildAction
);
}
);
}
public static void ConfigureEfCoreEntity(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] EntityTypeBuilder typeBuilder)
{
Check.NotNull(objectExtensionManager, nameof(objectExtensionManager));
Check.NotNull(typeBuilder, nameof(typeBuilder));
var objectExtension = objectExtensionManager.GetOrNull(typeBuilder.Metadata.ClrType);
if (objectExtension == null)
{
return;
}
foreach (var property in objectExtension.GetProperties())
{
var efCoreMapping = property.GetEfCoreMappingOrNull();
if (efCoreMapping == null)
{
continue;
}
/* Prevent multiple calls to the entityTypeBuilder.Property(...) method */
if (typeBuilder.Metadata.FindProperty(property.Name) != null)
{
continue;
}
var propertyBuilder = typeBuilder.Property(property.Type, property.Name);
efCoreMapping.PropertyBuildAction?.Invoke(propertyBuilder);
}
}
}
}

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Volo.Abp.ObjectExtending
{
public static class EfCoreObjectExtensionPropertyInfoExtensions
{
public const string EfCorePropertyConfigurationName = "EfCoreMapping";
[NotNull]
public static ObjectExtensionPropertyInfo MapEfCore(
[NotNull] this ObjectExtensionPropertyInfo propertyExtension,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction = null)
{
Check.NotNull(propertyExtension, nameof(propertyExtension));
propertyExtension.Configuration[EfCorePropertyConfigurationName] =
new ObjectExtensionPropertyInfoEfCoreMappingOptions(
propertyExtension,
propertyBuildAction
);
return propertyExtension;
}
[CanBeNull]
public static ObjectExtensionPropertyInfoEfCoreMappingOptions GetEfCoreMappingOrNull(
[NotNull] this ObjectExtensionPropertyInfo propertyExtension)
{
Check.NotNull(propertyExtension, nameof(propertyExtension));
return propertyExtension
.Configuration
.GetOrDefault(EfCorePropertyConfigurationName)
as ObjectExtensionPropertyInfoEfCoreMappingOptions;
}
public static bool IsMappedToFieldForEfCore(
[NotNull] this ObjectExtensionPropertyInfo propertyExtension)
{
Check.NotNull(propertyExtension, nameof(propertyExtension));
return propertyExtension
.Configuration
.ContainsKey(EfCorePropertyConfigurationName);
}
}
}

@ -0,0 +1,27 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionPropertyInfoEfCoreMappingOptions
{
[NotNull]
public ObjectExtensionPropertyInfo ExtensionProperty { get; }
[NotNull]
public ObjectExtensionInfo ObjectExtension => ExtensionProperty.ObjectExtension;
[CanBeNull]
public Action<PropertyBuilder> PropertyBuildAction { get; set; }
public ObjectExtensionPropertyInfoEfCoreMappingOptions(
[NotNull] ObjectExtensionPropertyInfo extensionProperty,
[CanBeNull] Action<PropertyBuilder> propertyBuildAction = null)
{
ExtensionProperty = Check.NotNull(extensionProperty, nameof(extensionProperty));
PropertyBuildAction = propertyBuildAction;
}
}
}

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Volo.Abp.ObjectExtending.Tests")]

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.ObjectExtending</AssemblyName>
<PackageId>Volo.Abp.ObjectExtending</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,9 @@
using Volo.Abp.Modularity;
namespace Volo.Abp.ObjectExtending
{
public class AbpObjectExtendingModule : AbpModule
{
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
[Serializable]
public class ExtensibleObject : IHasExtraProperties
{
public Dictionary<string, object> ExtraProperties { get; protected set; }
public ExtensibleObject()
{
ExtraProperties = new Dictionary<string, object>();
}
}
}

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
public static class HasExtraPropertiesObjectExtendingExtensions
{
/// <summary>
/// Copies extra properties from the <paramref name="source"/> object
/// to the <paramref name="destination"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <typeparam name="TSource">Source class type</typeparam>
/// <typeparam name="TDestination">Destination class type</typeparam>
/// <param name="source">The source object</param>
/// <param name="destination">The destination object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] this TSource source,
[NotNull] TDestination destination,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
Check.NotNull(source, nameof(source));
Check.NotNull(destination, nameof(destination));
MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
source.ExtraProperties,
destination.ExtraProperties,
definitionChecks
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <typeparam name="TSource">Source class type (for definition check)</typeparam>
/// <typeparam name="TDestination">Destination class type (for definition check)</typeparam>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
public static void MapExtraPropertiesTo<TSource, TDestination>(
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
where TSource : IHasExtraProperties
where TDestination : IHasExtraProperties
{
MapExtraPropertiesTo(
typeof(TSource),
typeof(TDestination),
sourceDictionary,
destinationDictionary,
definitionChecks
);
}
/// <summary>
/// Copies extra properties from the <paramref name="sourceDictionary"/> object
/// to the <paramref name="destinationDictionary"/> object.
///
/// Checks property definitions (over the <see cref="ObjectExtensionManager"/>)
/// based on the <paramref name="definitionChecks"/> preference.
/// </summary>
/// <param name="sourceType">Source type (for definition check)</param>
/// <param name="destinationType">Destination class type (for definition check)</param>
/// <param name="sourceDictionary">The source dictionary object</param>
/// <param name="destinationDictionary">The destination dictionary object</param>
/// <param name="definitionChecks">
/// Controls which properties to map.
/// </param>
public static void MapExtraPropertiesTo(
[NotNull] Type sourceType,
[NotNull] Type destinationType,
[NotNull] Dictionary<string, object> sourceDictionary,
[NotNull] Dictionary<string, object> destinationDictionary,
MappingPropertyDefinitionChecks definitionChecks = MappingPropertyDefinitionChecks.Both)
{
Check.AssignableTo<IHasExtraProperties>(sourceType, nameof(sourceType));
Check.AssignableTo<IHasExtraProperties>(destinationType, nameof(destinationType));
Check.NotNull(sourceDictionary, nameof(sourceDictionary));
Check.NotNull(destinationDictionary, nameof(destinationDictionary));
var sourceObjectExtension = ObjectExtensionManager.Instance.GetOrNull(sourceType);
if (definitionChecks.HasFlag(MappingPropertyDefinitionChecks.Source) &&
sourceObjectExtension == null)
{
return;
}
var destinationObjectExtension = ObjectExtensionManager.Instance.GetOrNull(destinationType);
if (definitionChecks.HasFlag(MappingPropertyDefinitionChecks.Destination) &&
destinationObjectExtension == null)
{
return;
}
if (definitionChecks == MappingPropertyDefinitionChecks.None)
{
foreach (var keyValue in sourceDictionary)
{
destinationDictionary[keyValue.Key] = keyValue.Value;
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Source)
{
Debug.Assert(sourceObjectExtension != null, nameof(sourceObjectExtension) + " != null");
foreach (var property in sourceObjectExtension.GetProperties())
{
if (!sourceDictionary.ContainsKey(property.Name))
{
continue;
}
destinationDictionary[property.Name] = sourceDictionary[property.Name];
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Destination)
{
Debug.Assert(destinationObjectExtension != null, nameof(destinationObjectExtension) + " != null");
foreach (var keyValue in sourceDictionary)
{
if (!destinationObjectExtension.HasProperty(keyValue.Key))
{
continue;
}
destinationDictionary[keyValue.Key] = keyValue.Value;
}
}
else if (definitionChecks == MappingPropertyDefinitionChecks.Both)
{
Debug.Assert(sourceObjectExtension != null, nameof(sourceObjectExtension) + " != null");
Debug.Assert(destinationObjectExtension != null, nameof(destinationObjectExtension) + " != null");
foreach (var property in sourceObjectExtension.GetProperties())
{
if (!sourceDictionary.ContainsKey(property.Name))
{
continue;
}
if (!destinationObjectExtension.HasProperty(property.Name))
{
continue;
}
destinationDictionary[property.Name] = sourceDictionary[property.Name];
}
}
else
{
throw new NotImplementedException(definitionChecks + " was not implemented!");
}
}
}
}

@ -0,0 +1,28 @@
using System;
namespace Volo.Abp.ObjectExtending
{
[Flags]
public enum MappingPropertyDefinitionChecks : byte
{
/// <summary>
/// No check. Copy all extra properties from the source to the destination.
/// </summary>
None = 0,
/// <summary>
/// Copy the extra properties defined for the source class.
/// </summary>
Source = 1,
/// <summary>
/// Copy the extra properties defined for the destination class.
/// </summary>
Destination = 2,
/// <summary>
/// Copy extra properties defined for both of the source and destination classes.
/// </summary>
Both = Source | Destination
}
}

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionInfo
{
[NotNull]
public Type Type { get; }
[NotNull]
protected Dictionary<string, ObjectExtensionPropertyInfo> Properties { get; }
[NotNull]
public Dictionary<object, object> Configuration { get; }
public ObjectExtensionInfo([NotNull] Type type)
{
Type = Check.AssignableTo<IHasExtraProperties>(type, nameof(type));
Properties = new Dictionary<string, ObjectExtensionPropertyInfo>();
Configuration = new Dictionary<object, object>();
}
public virtual bool HasProperty(string propertyName)
{
return Properties.ContainsKey(propertyName);
}
[NotNull]
public virtual ObjectExtensionInfo AddOrUpdateProperty<TProperty>(
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
{
return AddOrUpdateProperty(
typeof(TProperty),
propertyName,
configureAction
);
}
[NotNull]
public virtual ObjectExtensionInfo AddOrUpdateProperty(
[NotNull] Type propertyType,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
{
Check.NotNull(propertyType, nameof(propertyType));
Check.NotNull(propertyName, nameof(propertyName));
var propertyInfo = Properties.GetOrAdd(
propertyName,
() => new ObjectExtensionPropertyInfo(this, propertyType, propertyName)
);
configureAction?.Invoke(propertyInfo);
return this;
}
[NotNull]
public virtual ImmutableList<ObjectExtensionPropertyInfo> GetProperties()
{
return Properties.Values.ToImmutableList();
}
[CanBeNull]
public virtual ObjectExtensionPropertyInfo GetPropertyOrNull(
[NotNull] string propertyName)
{
Check.NotNullOrEmpty(propertyName, nameof(propertyName));
return Properties.GetOrDefault(propertyName);
}
}
}

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionManager
{
public static ObjectExtensionManager Instance { get; set; } = new ObjectExtensionManager();
protected Dictionary<Type, ObjectExtensionInfo> ObjectsExtensions { get; }
protected internal ObjectExtensionManager()
{
ObjectsExtensions = new Dictionary<Type, ObjectExtensionInfo>();
}
[NotNull]
public virtual ObjectExtensionManager AddOrUpdate<TObject>(
[CanBeNull] Action<ObjectExtensionInfo> configureAction = null)
where TObject : IHasExtraProperties
{
return AddOrUpdate(typeof(TObject), configureAction);
}
[NotNull]
public virtual ObjectExtensionManager AddOrUpdate(
[NotNull] Type type,
[CanBeNull] Action<ObjectExtensionInfo> configureAction = null)
{
Check.AssignableTo<IHasExtraProperties>(type, nameof(type));
var extensionInfo = ObjectsExtensions.GetOrAdd(
type,
() => new ObjectExtensionInfo(type)
);
configureAction?.Invoke(extensionInfo);
return this;
}
[CanBeNull]
public virtual ObjectExtensionInfo GetOrNull<TObject>()
where TObject : IHasExtraProperties
{
return GetOrNull(typeof(TObject));
}
[CanBeNull]
public virtual ObjectExtensionInfo GetOrNull([NotNull] Type type)
{
Check.AssignableTo<IHasExtraProperties>(type, nameof(type));
return ObjectsExtensions.GetOrDefault(type);
}
[NotNull]
public virtual ImmutableList<ObjectExtensionInfo> GetExtendedObjects()
{
return ObjectsExtensions.Values.ToImmutableList();
}
}
}

@ -0,0 +1,44 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Data;
namespace Volo.Abp.ObjectExtending
{
public static class ObjectExtensionManagerExtensions
{
public static ObjectExtensionManager AddOrUpdateProperty<TObject, TProperty>(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
where TObject : IHasExtraProperties
{
return objectExtensionManager.AddOrUpdateProperty(
typeof(TObject),
typeof(TProperty),
propertyName,
configureAction
);
}
public static ObjectExtensionManager AddOrUpdateProperty(
[NotNull] this ObjectExtensionManager objectExtensionManager,
[NotNull] Type objectType,
[NotNull] Type propertyType,
[NotNull] string propertyName,
[CanBeNull] Action<ObjectExtensionPropertyInfo> configureAction = null)
{
Check.NotNull(objectExtensionManager, nameof(objectExtensionManager));
return objectExtensionManager.AddOrUpdate(
objectType,
options =>
{
options.AddOrUpdateProperty(
propertyType,
propertyName,
configureAction
);
});
}
}
}

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionPropertyInfo
{
[NotNull]
public ObjectExtensionInfo ObjectExtension { get; }
[NotNull]
public string Name { get; }
[NotNull]
public Type Type { get; }
[NotNull]
public Dictionary<object, object> Configuration { get; }
public ObjectExtensionPropertyInfo(
[NotNull] ObjectExtensionInfo objectExtension,
[NotNull] Type type,
[NotNull] string name)
{
ObjectExtension = Check.NotNull(objectExtension, nameof(objectExtension));
Type = Check.NotNull(type, nameof(type));
Name = Check.NotNull(name, nameof(name));
Configuration = new Dictionary<object, object>();
}
}
}

@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.AutoMapper;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending.TestObjects;
using Volo.Abp.Testing;
using Xunit;
namespace AutoMapper
{
public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest<AutoMapperTestModule>
{
private readonly Volo.Abp.ObjectMapping.IObjectMapper _objectMapper;
public AbpAutoMapperExtensibleDtoExtensions_Tests()
{
_objectMapper = ServiceProvider.GetRequiredService<Volo.Abp.ObjectMapping.IObjectMapper>();
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default()
{
var person = new ExtensibleTestPerson()
.SetProperty("Name", "John")
.SetProperty("Age", 42)
.SetProperty("ChildCount", 2)
.SetProperty("Sex", "male");
var personDto = new ExtensibleTestPersonDto()
.SetProperty("ExistingDtoProperty", "existing-value");
_objectMapper.Map(person, personDto);
personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
}
}

@ -11,7 +11,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AutoMapper\Volo.Abp.AutoMapper.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\Volo.Abp.ObjectExtending.Tests\Volo.Abp.ObjectExtending.Tests.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
</ItemGroup>

@ -1,9 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.AutoMapper
{
[DependsOn(typeof(AbpAutoMapperModule))]
[DependsOn(
typeof(AbpAutoMapperModule),
typeof(AbpObjectExtendingTestModule)
)]
public class AutoMapperTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

@ -1,4 +1,5 @@
using AutoMapper;
using Volo.Abp.ObjectExtending.TestObjects;
namespace Volo.Abp.AutoMapper.SampleClasses
{
@ -7,6 +8,9 @@ namespace Volo.Abp.AutoMapper.SampleClasses
public MyMapProfile()
{
CreateMap<MyEntity, MyEntityDto>().ReverseMap();
CreateMap<ExtensibleTestPerson, ExtensibleTestPersonDto>()
.MapExtraProperties();
}
}
}

@ -1,4 +1,4 @@
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.ObjectExtending;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Threading;
@ -12,10 +12,11 @@ namespace Volo.Abp.EntityFrameworkCore.Domain
{
OneTimeRunner.Run(() =>
{
EntityExtensionManager.AddProperty<City, string>(
"PhoneCode",
p => p.HasMaxLength(8)
);
ObjectExtensionManager.Instance
.MapEfCoreProperty<City, string>(
"PhoneCode",
p => p.HasMaxLength(8)
);
});
}
}

@ -1,5 +1,4 @@
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext;
using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext;
using Volo.Abp.TestApp.Domain;
@ -37,8 +36,6 @@ namespace Volo.Abp.EntityFrameworkCore
modelBuilder.Entity<City>(b =>
{
//b.ConfigureExtensions();
b.OwnsMany(c => c.Districts, d =>
{
d.WithOwner().HasForeignKey(x => x.CityId);

@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext;
using Volo.Abp.TestApp.Domain;
@ -44,8 +43,6 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore
modelBuilder.Entity<City>(b =>
{
//b.ConfigureExtensions();
b.OwnsMany(c => c.Districts, d =>
{
d.WithOwner().HasForeignKey(x => x.CityId);

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.test.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.ObjectExtending\Volo.Abp.ObjectExtending.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
</ItemGroup>
</Project>

@ -0,0 +1,9 @@
using Volo.Abp.Testing;
namespace Volo.Abp.ObjectExtending
{
public abstract class AbpObjectExtendingTestBase : AbpIntegratedTest<AbpObjectExtendingTestModule>
{
}
}

@ -0,0 +1,27 @@
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending.TestObjects;
using Volo.Abp.Threading;
namespace Volo.Abp.ObjectExtending
{
[DependsOn(
typeof(AbpObjectExtendingModule),
typeof(AbpTestBaseModule)
)]
public class AbpObjectExtendingTestModule : AbpModule
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public override void PreConfigureServices(ServiceConfigurationContext context)
{
OneTimeRunner.Run(() =>
{
ObjectExtensionManager.Instance
.AddOrUpdateProperty<ExtensibleTestPerson, string>("Name")
.AddOrUpdateProperty<ExtensibleTestPerson, int>("Age")
.AddOrUpdateProperty<ExtensibleTestPersonDto, string>("Name")
.AddOrUpdateProperty<ExtensibleTestPersonDto, int>("ChildCount");
});
}
}
}

@ -0,0 +1,73 @@
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending.TestObjects;
using Xunit;
namespace Volo.Abp.ObjectExtending
{
public class HasExtraPropertiesObjectExtendingExtensions_Tests : AbpObjectExtendingTestBase
{
private readonly ExtensibleTestPerson _person;
private readonly ExtensibleTestPersonDto _personDto;
public HasExtraPropertiesObjectExtendingExtensions_Tests()
{
_person = new ExtensibleTestPerson()
.SetProperty("Name", "John")
.SetProperty("Age", 42)
.SetProperty("ChildCount", 2)
.SetProperty("Sex", "male");
_personDto = new ExtensibleTestPersonDto()
.SetProperty("ExistingDtoProperty", "existing-value");
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default()
{
_person.MapExtraPropertiesTo(_personDto);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
_personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Source_Defined_Properties_If_Requested()
{
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.Source);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<int>("Age").ShouldBe(42); //Defined in source
_personDto.HasProperty("ChildCount").ShouldBeFalse(); //Not defined in the source
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
[Fact]
public void MapExtraPropertiesTo_Should_Only_Map_Destination_Defined_Properties_If_Requested()
{
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.Destination);
_personDto.GetProperty<string>("Name").ShouldBe("John"); //Defined in both classes
_personDto.GetProperty<int>("ChildCount").ShouldBe(2); //Defined in destination
_personDto.HasProperty("Age").ShouldBeFalse(); //Not defined in destination
_personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
[Fact]
public void MapExtraPropertiesTo_Should_Copy_all_With_No_Property_Definition_Check()
{
_person.MapExtraPropertiesTo(_personDto, MappingPropertyDefinitionChecks.None);
_personDto.GetProperty<string>("Name").ShouldBe("John");
_personDto.GetProperty<int>("Age").ShouldBe(42);
_personDto.GetProperty<int>("ChildCount").ShouldBe(2);
_personDto.GetProperty<string>("Sex").ShouldBe("male");
_personDto.GetProperty<string>("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values
}
}
}

@ -0,0 +1,63 @@
using System.Linq;
using Shouldly;
using Xunit;
namespace Volo.Abp.ObjectExtending
{
public class ObjectExtensionManager_Tests
{
private readonly ObjectExtensionManager _objectExtensionManager;
public ObjectExtensionManager_Tests()
{
_objectExtensionManager = new ObjectExtensionManager();
}
[Fact]
public void Should_Not_Add_Same_Property_Multiple_Times()
{
_objectExtensionManager
.AddOrUpdateProperty<MyExtensibleObject, string>("TestProp")
.AddOrUpdateProperty<MyExtensibleObject, string>("TestProp");
var objectExtension = _objectExtensionManager.GetOrNull<MyExtensibleObject>();
objectExtension.ShouldNotBeNull();
var properties = objectExtension.GetProperties();
properties.Count.ShouldBe(1);
properties.FirstOrDefault(p => p.Name == "TestProp").ShouldNotBeNull();
}
[Fact]
public void Should_Update_Property_Configuration()
{
_objectExtensionManager
.AddOrUpdateProperty<MyExtensibleObject, string>(
"TestProp",
options =>
{
options.Configuration["TestConfig1"] = "TestConfig1-Value";
}
).AddOrUpdateProperty<MyExtensibleObject, string>(
"TestProp",
options =>
{
options.Configuration["TestConfig2"] = "TestConfig2-Value";
}
);
var objectExtension = _objectExtensionManager.GetOrNull<MyExtensibleObject>();
objectExtension.ShouldNotBeNull();
var property = objectExtension.GetPropertyOrNull("TestProp");
property.ShouldNotBeNull();
property.Configuration["TestConfig1"].ShouldBe("TestConfig1-Value");
property.Configuration["TestConfig2"].ShouldBe("TestConfig2-Value");
}
private class MyExtensibleObject : ExtensibleObject
{
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.ObjectExtending.TestObjects
{
public class ExtensibleTestPerson : ExtensibleObject
{
}
}

@ -0,0 +1,7 @@
namespace Volo.Abp.ObjectExtending.TestObjects
{
public class ExtensibleTestPersonDto : ExtensibleObject
{
}
}

@ -202,7 +202,7 @@ namespace Volo.Blogging.Posts
private async Task RemoveOldTags(ICollection<string> newTags, Post post)
{
foreach (var oldTag in post.Tags)
foreach (var oldTag in post.Tags.ToList())
{
var tag = await _tagRepository.GetAsync(oldTag.TagId);

@ -1,12 +0,0 @@
using Volo.Abp.AspNetCore.Mvc;
namespace VoloDocs.Web.Controllers
{
public class HomeController : AbpController
{
public void Index()
{
}
}
}

@ -3,3 +3,21 @@
@{
}
@if (!Model.Projects.Any())
{
<abp-alert alert-type="Warning">
<strong>No projects found!</strong><br />
See <a href=" https://docs.abp.io/en/abp/latest/Modules/Docs">documentation</a> to see how you can create a new one.
</abp-alert>
}
else
{
<h1>Projects</h1>
<abp-list-group class="mt-5">
@foreach (var project in Model.Projects)
{
<abp-list-group-item href="@Model.GetUrlForProject(project)">@project.Name</abp-list-group-item>
}
</abp-list-group>
}

@ -1,29 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
using Volo.Docs;
using Volo.Docs.Projects;
namespace VoloDocs.Web.Pages
{
public class IndexModel : PageModel
{
public IReadOnlyList<ProjectDto> Projects { get; set; }
private readonly DocsUiOptions _urlUiOptions;
public IndexModel(IOptions<DocsUiOptions> urlOptions)
private readonly IProjectAppService _projectAppService;
public IndexModel(IOptions<DocsUiOptions> urlOptions, IProjectAppService projectAppService)
{
_projectAppService = projectAppService;
_urlUiOptions = urlOptions.Value;
}
public IActionResult OnGet()
public async Task<IActionResult> OnGetAsync()
{
//TODO: Create HomeController & Index instead of Page. Otherwise, we have an empty Index.cshtml file.
if (!_urlUiOptions.RoutePrefix.IsNullOrWhiteSpace())
var projects = await _projectAppService.GetListAsync();
if (projects.Items.Count == 1)
{
return Redirect("." + _urlUiOptions.RoutePrefix);
return await RedirectToProjectAsync(projects.Items.First());
}
else if (projects.Items.Count > 1)
{
Projects = projects.Items;
}
return Page();
}
private async Task<IActionResult> RedirectToProjectAsync(ProjectDto project, string language = "en", string version = null)
{
var path = GetUrlForProject(project, language, version);
return await Task.FromResult(Redirect(path));
}
//Eg: "/en/abp/latest"
public string GetUrlForProject(ProjectDto project, string language = "en", string version = null)
{
return "." +
_urlUiOptions.RoutePrefix.EnsureStartsWith('/').EnsureEndsWith('/') +
language.EnsureEndsWith('/') +
project.ShortName.EnsureEndsWith('/') +
(version ?? DocsAppConsts.Latest);
}
}
}
}

@ -3,5 +3,10 @@
"LogoUrl": "/assets/images/Logo.png",
"ElasticSearch": {
"Url": "http://localhost:9200"
},
"Volo.Docs": {
"DocumentCacheTimeoutInterval": "24:00:00",
"DocumentResource.AbsoluteExpirationRelativeToNow": "24:00:00",
"DocumentResource.SlidingExpiration": "02:00:00"
}
}

File diff suppressed because it is too large Load Diff

@ -6,4 +6,4 @@
"@abp/aspnetcore.mvc.ui.theme.basic": "^1.0.2",
"@abp/docs": "^1.0.2"
}
}
}

@ -4,11 +4,13 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Newtonsoft.Json;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using Volo.Docs.Documents;
using Volo.Docs.Documents.FullSearch.Elastic;
using Volo.Docs.Projects;
using Volo.Extensions;
namespace Volo.Docs.Admin.Documents
{
@ -38,15 +40,19 @@ namespace Volo.Docs.Admin.Documents
{
var project = await _projectRepository.GetAsync(input.ProjectId);
var navigationFile = await GetDocumentAsync(
var navigationDocument = await GetDocumentAsync(
project,
project.NavigationDocumentName,
input.LanguageCode,
input.Version
);
var nav = JsonConvert.DeserializeObject<NavigationNode>(navigationFile.Content);
var leafs = nav.Items.GetAllNodes(x => x.Items)
if (!JsonConvertExtensions.TryDeserializeObject<NavigationNode>(navigationDocument.Content, out var navigation))
{
throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}.");
}
var leafs = navigation.Items.GetAllNodes(x => x.Items)
.Where(x => x.IsLeaf && !x.Path.IsNullOrWhiteSpace())
.ToList();

@ -3,13 +3,17 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Nest;
using Newtonsoft.Json;
using Volo.Abp;
using Volo.Abp.Caching;
using Volo.Docs.Documents.FullSearch.Elastic;
using Volo.Docs.Projects;
using Volo.Extensions;
namespace Volo.Docs.Documents
{
@ -24,6 +28,10 @@ namespace Volo.Docs.Documents
protected IHostEnvironment HostEnvironment { get; }
private readonly IDocumentFullSearch _documentFullSearch;
private readonly DocsElasticSearchOptions _docsElasticSearchOptions;
private readonly IConfiguration _configuration;
private readonly TimeSpan _cacheTimeout;
private readonly TimeSpan _documentResourceAbsoluteExpiration;
private readonly TimeSpan _documentResourceSlidingExpiration;
public DocumentAppService(
IProjectRepository projectRepository,
@ -32,9 +40,10 @@ namespace Volo.Docs.Documents
IDistributedCache<LanguageConfig> languageCache,
IDistributedCache<DocumentResourceDto> resourceCache,
IDistributedCache<DocumentUpdateInfo> documentUpdateCache,
IHostEnvironment hostEnvironment,
IDocumentFullSearch documentFullSearch,
IOptions<DocsElasticSearchOptions> docsElasticSearchOptions)
IHostEnvironment hostEnvironment,
IDocumentFullSearch documentFullSearch,
IOptions<DocsElasticSearchOptions> docsElasticSearchOptions,
IConfiguration configuration)
{
_projectRepository = projectRepository;
_documentRepository = documentRepository;
@ -44,7 +53,11 @@ namespace Volo.Docs.Documents
DocumentUpdateCache = documentUpdateCache;
HostEnvironment = hostEnvironment;
_documentFullSearch = documentFullSearch;
_configuration = configuration;
_docsElasticSearchOptions = docsElasticSearchOptions.Value;
_cacheTimeout = GetCacheTimeout();
_documentResourceAbsoluteExpiration = GetDocumentResourceAbsoluteExpirationTimeout();
_documentResourceSlidingExpiration = GetDocumentResourceSlidingExpirationTimeout();
}
public virtual async Task<DocumentWithDetailsDto> GetAsync(GetDocumentInput input)
@ -82,7 +95,10 @@ namespace Volo.Docs.Documents
input.Version
);
var navigationNode = JsonConvert.DeserializeObject<NavigationNode>(navigationDocument.Content);
if (!JsonConvertExtensions.TryDeserializeObject<NavigationNode>(navigationDocument.Content, out var navigationNode))
{
throw new UserFriendlyException($"Cannot validate navigation file '{project.NavigationDocumentName}' for the project {project.Name}.");
}
var leafs = navigationNode.Items.GetAllNodes(x => x.Items)
.Where(x => !x.Path.IsNullOrWhiteSpace())
@ -127,9 +143,8 @@ namespace Volo.Docs.Documents
GetResourceAsync,
() => new DistributedCacheEntryOptions
{
//TODO: Configurable?
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6),
SlidingExpiration = TimeSpan.FromMinutes(30)
AbsoluteExpirationRelativeToNow = _documentResourceAbsoluteExpiration,
SlidingExpiration = _documentResourceSlidingExpiration
}
);
}
@ -173,11 +188,16 @@ namespace Volo.Docs.Documents
input.Version
);
return JsonConvert.DeserializeObject<DocumentParametersDto>(document.Content);
if (!JsonConvertExtensions.TryDeserializeObject<DocumentParametersDto>(document.Content, out var documentParameters))
{
throw new UserFriendlyException($"Cannot validate document parameters file '{project.ParametersDocumentName}' for the project {project.Name}.");
}
return documentParameters;
}
catch (DocumentNotFoundException)
{
Logger.LogWarning($"Parameter file ({project.ParametersDocumentName}) not found.");
Logger.LogWarning($"Parameter file ({project.ParametersDocumentName}) not found!");
return new DocumentParametersDto();
}
}
@ -190,48 +210,23 @@ namespace Volo.Docs.Documents
{
version = string.IsNullOrWhiteSpace(version) ? project.LatestVersionBranchName : version;
async Task<DocumentWithDetailsDto> GetDocumentAsync(Document oldDocument = null)
{
Logger.LogInformation($"Not found in the cache. Requesting {documentName} from the source...");
var source = _documentStoreFactory.Create(project.DocumentStoreType);
var sourceDocument = await source.GetDocumentAsync(project, documentName, languageCode, version, oldDocument?.LastSignificantUpdateTime);
await _documentRepository.DeleteAsync(project.Id, sourceDocument.Name, sourceDocument.LanguageCode, sourceDocument.Version);
await _documentRepository.InsertAsync(sourceDocument, true);
Logger.LogInformation($"Document retrieved: {documentName}");
var cacheKey = $"DocumentUpdateInfo{sourceDocument.ProjectId}#{sourceDocument.Name}#{sourceDocument.LanguageCode}#{sourceDocument.Version}";
await DocumentUpdateCache.SetAsync(cacheKey, new DocumentUpdateInfo
{
Name = sourceDocument.Name,
CreationTime = sourceDocument.CreationTime,
LastUpdatedTime = sourceDocument.LastUpdatedTime,
LastSignificantUpdateTime = sourceDocument.LastSignificantUpdateTime
});
return CreateDocumentWithDetailsDto(project, sourceDocument);
}
if (HostEnvironment.IsDevelopment())
{
return await GetDocumentAsync();
return await GetDocumentAsync(documentName, project, languageCode, version);
}
var document = await _documentRepository.FindAsync(project.Id, documentName, languageCode, version);
if (document == null)
{
return await GetDocumentAsync();
return await GetDocumentAsync(documentName, project, languageCode, version);
}
//Only the latest version (dev) of the document needs to update the cache.
if (!project.LatestVersionBranchName.IsNullOrWhiteSpace() &&
document.Version == project.LatestVersionBranchName &&
//TODO: Configurable cache time?
document.LastCachedTime + TimeSpan.FromHours(2) < DateTime.Now)
document.LastCachedTime + _cacheTimeout < DateTime.Now)
{
return await GetDocumentAsync(document);
return await GetDocumentAsync(documentName, project, languageCode, version, document);
}
var cacheKey = $"DocumentUpdateInfo{document.ProjectId}#{document.Name}#{document.LanguageCode}#{document.Version}";
@ -253,5 +248,62 @@ namespace Volo.Docs.Documents
documentDto.Contributors = ObjectMapper.Map<List<DocumentContributor>, List<DocumentContributorDto>>(document.Contributors);
return documentDto;
}
private async Task<DocumentWithDetailsDto> GetDocumentAsync(string documentName, Project project, string languageCode, string version, Document oldDocument = null)
{
Logger.LogInformation($"Not found in the cache. Requesting {documentName} from the source...");
var source = _documentStoreFactory.Create(project.DocumentStoreType);
var sourceDocument = await source.GetDocumentAsync(project, documentName, languageCode, version, oldDocument?.LastSignificantUpdateTime);
await _documentRepository.DeleteAsync(project.Id, sourceDocument.Name, sourceDocument.LanguageCode, sourceDocument.Version);
await _documentRepository.InsertAsync(sourceDocument, true);
Logger.LogInformation($"Document retrieved: {documentName}");
var cacheKey = $"DocumentUpdateInfo{sourceDocument.ProjectId}#{sourceDocument.Name}#{sourceDocument.LanguageCode}#{sourceDocument.Version}";
await DocumentUpdateCache.SetAsync(cacheKey, new DocumentUpdateInfo
{
Name = sourceDocument.Name,
CreationTime = sourceDocument.CreationTime,
LastUpdatedTime = sourceDocument.LastUpdatedTime,
LastSignificantUpdateTime = sourceDocument.LastSignificantUpdateTime
});
return CreateDocumentWithDetailsDto(project, sourceDocument);
}
private TimeSpan GetCacheTimeout()
{
var value = _configuration["Volo.Docs:DocumentCacheTimeoutInterval"];
if (value.IsNullOrEmpty())
{
return TimeSpan.FromHours(6);
}
return TimeSpan.Parse(value);
}
private TimeSpan GetDocumentResourceAbsoluteExpirationTimeout()
{
var value = _configuration["Volo.Docs:DocumentResource.AbsoluteExpirationRelativeToNow"];
if (value.IsNullOrEmpty())
{
return TimeSpan.FromHours(6);
}
return TimeSpan.Parse(value);
}
private TimeSpan GetDocumentResourceSlidingExpirationTimeout()
{
var value = _configuration["Volo.Docs:DocumentResource.SlidingExpiration"];
if (value.IsNullOrEmpty())
{
return TimeSpan.FromMinutes(30);
}
return TimeSpan.Parse(value);
}
}
}

@ -0,0 +1,7 @@
namespace Volo.Docs
{
public class DocsDomainConsts
{
public static string LanguageConfigFileName = "docs-langs.json";
}
}

@ -4,11 +4,13 @@ using System.IO;
using System.Security;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Volo.Abp;
using Volo.Abp.Domain.Services;
using Volo.Abp.IO;
using Volo.Docs.Documents;
using Volo.Docs.FileSystem.Projects;
using Volo.Docs.Projects;
using Volo.Extensions;
namespace Volo.Docs.FileSystem.Documents
{
@ -22,7 +24,7 @@ namespace Volo.Docs.FileSystem.Documents
var path = Path.Combine(projectFolder, languageCode, documentName);
CheckDirectorySecurity(projectFolder, path);
var content = await FileHelper.ReadAllTextAsync(path);
var localDirectory = "";
@ -55,10 +57,15 @@ namespace Volo.Docs.FileSystem.Documents
public async Task<LanguageConfig> GetLanguageListAsync(Project project, string version)
{
var path = Path.Combine(project.GetFileSystemPath(), "docs-langs.json");
var configAsJson = await FileHelper.ReadAllTextAsync(path);
var path = Path.Combine(project.GetFileSystemPath(), DocsDomainConsts.LanguageConfigFileName);
var configJsonContent = await FileHelper.ReadAllTextAsync(path);
if (!JsonConvertExtensions.TryDeserializeObject<LanguageConfig>(configJsonContent, out var languageConfig))
{
throw new UserFriendlyException($"Cannot validate language config file '{DocsDomainConsts.LanguageConfigFileName}' for the project {project.Name}.");
}
return JsonConvert.DeserializeObject<LanguageConfig>(configAsJson);
return languageConfig;
}
public async Task<DocumentResource> GetResource(Project project, string resourceName, string languageCode, string version)

@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Volo.Abp.Domain.Services;
using Volo.Docs.Documents;
using Volo.Docs.GitHub.Projects;
using Volo.Docs.Projects;
using Newtonsoft.Json.Linq;
using Octokit;
using Volo.Abp;
using Volo.Extensions;
using Project = Volo.Docs.Projects.Project;
namespace Volo.Docs.GitHub.Documents
@ -55,11 +55,11 @@ namespace Volo.Docs.GitHub.Documents
var lastSignificantUpdateTime = !isNavigationDocument && !isParameterDocument && version == project.LatestVersionBranchName ?
await GetLastSignificantUpdateTime(
fileCommits,
project,
project,
project.GetGitHubInnerUrl(languageCode, documentName),
lastKnownSignificantUpdateTime,
documentCreationTime
) ?? lastKnownSignificantUpdateTime
) ?? lastKnownSignificantUpdateTime
: null;
var document = new Document(GuidGenerator.Create(),
@ -179,11 +179,16 @@ namespace Volo.Docs.GitHub.Documents
var rootUrl = project.GetGitHubUrl(version);
var userAgent = project.GetGithubUserAgentOrNull();
var url = CalculateRawRootUrl(rootUrl) + "docs-langs.json";
var url = CalculateRawRootUrl(rootUrl) + DocsDomainConsts.LanguageConfigFileName;
var configAsJson = await DownloadWebContentAsStringAsync(url, token, userAgent);
return JsonConvert.DeserializeObject<LanguageConfig>(configAsJson);
if (!JsonConvertExtensions.TryDeserializeObject<LanguageConfig>(configAsJson, out var languageConfig))
{
throw new UserFriendlyException($"Cannot validate language config file '{DocsDomainConsts.LanguageConfigFileName}' for the project {project.Name} - v{version}.");
}
return languageConfig;
}
private async Task<IReadOnlyList<Release>> GetReleasesAsync(Project project)
@ -199,8 +204,14 @@ namespace Volo.Docs.GitHub.Documents
var url = project.GetGitHubUrl();
var ownerName = GetOwnerNameFromUrl(url);
var repositoryName = GetRepositoryNameFromUrl(url);
return await _githubRepositoryManager.GetFileCommitsAsync(ownerName, repositoryName,
version, filename, project.GetGitHubAccessTokenOrNull());
return await _githubRepositoryManager.GetFileCommitsAsync(
ownerName,
repositoryName,
version,
filename,
project.GetGitHubAccessTokenOrNull()
);
}
protected virtual string GetOwnerNameFromUrl(string url)
@ -212,7 +223,7 @@ namespace Volo.Docs.GitHub.Documents
}
catch (Exception)
{
throw new Exception($"Github url is not valid: {url}");
throw new Exception($"GitHub url is not valid: {url}");
}
}

@ -23,54 +23,25 @@ namespace Volo.Docs.GitHub.Documents
public async Task<string> GetFileRawStringContentAsync(string rawUrl, string token, string userAgent)
{
var httpClient = _clientFactory.CreateClient(HttpClientName);
if (!token.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token);
}
if (!userAgent.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
using var httpClient = CreateHttpClient(token, userAgent);
return await httpClient.GetStringAsync(new Uri(rawUrl));
}
public async Task<byte[]> GetFileRawByteArrayContentAsync(string rawUrl, string token, string userAgent)
{
var httpClient = _clientFactory.CreateClient(HttpClientName);
if (!token.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token);
}
if (!userAgent.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
using var httpClient = CreateHttpClient(token, userAgent);
return await httpClient.GetByteArrayAsync(new Uri(rawUrl));
}
public async Task<IReadOnlyList<Release>> GetReleasesAsync(string name, string repositoryName, string token)
{
var client = token.IsNullOrWhiteSpace()
? new GitHubClient(new ProductHeaderValue(name))
: new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token)));
return (await client
.Repository
.Release
.GetAll(name, repositoryName)).ToList();
var client = GetGitHubClient(name, token);
return await client.Repository.Release.GetAll(name, repositoryName);
}
public async Task<IReadOnlyList<GitHubCommit>> GetFileCommitsAsync(string name, string repositoryName, string version, string filename, string token)
{
var client = token.IsNullOrWhiteSpace()
? new GitHubClient(new ProductHeaderValue(name))
: new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token)));
var client = GetGitHubClient(name, token);
var repo = await client.Repository.Get(name, repositoryName);
var request = new CommitRequest { Path = filename, Sha = version };
return await client.Repository.Commit.GetAll(repo.Id, request);
@ -78,12 +49,32 @@ namespace Volo.Docs.GitHub.Documents
public async Task<GitHubCommit> GetSingleCommitsAsync(string name, string repositoryName, string sha, string token)
{
var client = token.IsNullOrWhiteSpace()
? new GitHubClient(new ProductHeaderValue(name))
: new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token)));
var client = GetGitHubClient(name, token);
var repo = await client.Repository.Get(name, repositoryName);
return await client.Repository.Commit.Get(repo.Id, sha);
}
private HttpClient CreateHttpClient(string token, string userAgent)
{
var httpClient = _clientFactory.CreateClient(HttpClientName);
if (!token.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", token);
}
if (!userAgent.IsNullOrWhiteSpace())
{
httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent);
}
return httpClient;
}
private static GitHubClient GetGitHubClient(string name, string token)
{
return token.IsNullOrWhiteSpace()
? new GitHubClient(new ProductHeaderValue(name))
: new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token)));
}
}
}

@ -0,0 +1,22 @@
using System;
using Newtonsoft.Json;
namespace Volo.Extensions
{
public static class JsonConvertExtensions
{
public static bool TryDeserializeObject<T>(string jsonContent, out T result)
{
try
{
result = JsonConvert.DeserializeObject<T>(jsonContent);
return true;
}
catch
{
result = default;
return false;
}
}
}
}

@ -7,16 +7,17 @@ using Newtonsoft.Json;
using Scriban;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Docs.Documents;
using Volo.Abp;
using Volo.Extensions;
namespace Volo.Docs.HtmlConverting
{
public class ScribanDocumentSectionRenderer : IDocumentSectionRenderer
{
private const string jsonOpener = "````json";
private const string jsonCloser = "````";
private const string docs_param = "//[doc-params]";
private const string docs_templates = "//[doc-template]";
private const string JsonOpener = "````json";
private const string JsonCloser = "````";
private const string DocsParam = "//[doc-params]";
private const string DocsTemplates = "//[doc-template]";
public ILogger<ScribanDocumentSectionRenderer> Logger { get; set; }
@ -40,6 +41,7 @@ namespace Volo.Docs.HtmlConverting
}
var result = await scribanTemplate.RenderAsync(parameters);
return RemoveOptionsJson(result);
}
@ -47,7 +49,7 @@ namespace Volo.Docs.HtmlConverting
{
try
{
if (!document.Contains(jsonOpener) || !document.Contains(docs_param))
if (!document.Contains(JsonOpener) || !document.Contains(DocsParam))
{
return new Dictionary<string, List<string>>();
}
@ -59,9 +61,14 @@ namespace Volo.Docs.HtmlConverting
return new Dictionary<string, List<string>>();
}
var pureJson = insideJsonSection.Replace(docs_param, "").Trim();
var pureJson = insideJsonSection.Replace(DocsParam, "").Trim();
if (!JsonConvertExtensions.TryDeserializeObject<Dictionary<string, List<string>>>(pureJson, out var availableParameters))
{
throw new UserFriendlyException("ERROR-20200327: Cannot validate JSON content for `AvailableParameters`!");
}
return JsonConvert.DeserializeObject<Dictionary<string, List<string>>>(pureJson);
return await Task.FromResult(availableParameters);
}
catch (Exception)
{
@ -70,12 +77,13 @@ namespace Volo.Docs.HtmlConverting
}
}
private string RemoveOptionsJson(string document)
private static string RemoveOptionsJson(string document)
{
var orgDocument = document;
try
{
if (!document.Contains(jsonOpener) || !document.Contains(docs_param))
if (!document.Contains(JsonOpener) || !document.Contains(DocsParam))
{
return orgDocument;
}
@ -88,8 +96,9 @@ namespace Volo.Docs.HtmlConverting
}
return document.Remove(
jsonBeginningIndex - jsonOpener.Length, (jsonEndingIndex + jsonCloser.Length) - (jsonBeginningIndex - jsonOpener.Length)
);
jsonBeginningIndex - JsonOpener.Length,
(jsonEndingIndex + JsonCloser.Length) - (jsonBeginningIndex - JsonOpener.Length)
);
}
catch (Exception)
{
@ -97,25 +106,25 @@ namespace Volo.Docs.HtmlConverting
}
}
private (int, int, string) GetJsonBeginEndIndexesAndPureJson(string document)
private static (int, int, string) GetJsonBeginEndIndexesAndPureJson(string document)
{
var searchedIndex = 0;
while (searchedIndex < document.Length)
{
var jsonBeginningIndex = document.Substring(searchedIndex).IndexOf(jsonOpener, StringComparison.Ordinal) + jsonOpener.Length + searchedIndex;
var jsonBeginningIndex = document.Substring(searchedIndex).IndexOf(JsonOpener, StringComparison.Ordinal) + JsonOpener.Length + searchedIndex;
if (jsonBeginningIndex < 0)
{
return (-1, -1, "");
}
var jsonEndingIndex = document.Substring(jsonBeginningIndex).IndexOf(jsonCloser, StringComparison.Ordinal) + jsonBeginningIndex;
var jsonEndingIndex = document.Substring(jsonBeginningIndex).IndexOf(JsonCloser, StringComparison.Ordinal) + jsonBeginningIndex;
var insideJsonSection = document[jsonBeginningIndex..jsonEndingIndex];
if (insideJsonSection.IndexOf(docs_param) < 0)
if (insideJsonSection.IndexOf(DocsParam, StringComparison.Ordinal) < 0)
{
searchedIndex = jsonEndingIndex + jsonCloser.Length;
searchedIndex = jsonEndingIndex + JsonCloser.Length;
continue;
}
@ -129,68 +138,84 @@ namespace Volo.Docs.HtmlConverting
{
var templates = new List<DocumentPartialTemplateWithValuesDto>();
while (documentContent.Contains(jsonOpener))
while (documentContent.Contains(JsonOpener))
{
var afterJsonOpener = documentContent.Substring(
documentContent.IndexOf(jsonOpener, StringComparison.Ordinal) + jsonOpener.Length);
documentContent.IndexOf(JsonOpener, StringComparison.Ordinal) + JsonOpener.Length
);
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0,
afterJsonOpener.IndexOf(jsonCloser, StringComparison.Ordinal));
afterJsonOpener.IndexOf(JsonCloser, StringComparison.Ordinal)
);
documentContent = afterJsonOpener.Substring(
afterJsonOpener.IndexOf(jsonCloser, StringComparison.Ordinal) + jsonCloser.Length);
afterJsonOpener.IndexOf(JsonCloser, StringComparison.Ordinal) + JsonCloser.Length
);
if (!betweenJsonOpenerAndCloser.Contains(docs_templates))
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates))
{
continue;
}
var json = betweenJsonOpenerAndCloser.Substring(betweenJsonOpenerAndCloser.IndexOf(docs_templates, StringComparison.Ordinal) + docs_templates.Length);
var json = betweenJsonOpenerAndCloser.Substring(betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + DocsTemplates.Length);
var template = JsonConvert.DeserializeObject<DocumentPartialTemplateWithValuesDto>(json);
if (!JsonConvertExtensions.TryDeserializeObject<DocumentPartialTemplateWithValuesDto>(json, out var template))
{
throw new UserFriendlyException($"ERROR-20200327: Cannot validate JSON content for `AvailableParameters`!");
}
templates.Add(template);
}
return templates;
return await Task.FromResult(templates);
}
private string SetPartialTemplates(string document, List<DocumentPartialTemplateContent> templates)
private static string SetPartialTemplates(string document, IReadOnlyCollection<DocumentPartialTemplateContent> templates)
{
var newDocument = new StringBuilder();
while (document.Contains(jsonOpener))
while (document.Contains(JsonOpener))
{
var beforeJson = document.Substring(0,
document.IndexOf(jsonOpener, StringComparison.Ordinal) + jsonOpener.Length);
document.IndexOf(JsonOpener, StringComparison.Ordinal) + JsonOpener.Length
);
var afterJsonOpener = document.Substring(
document.IndexOf(jsonOpener, StringComparison.Ordinal) + jsonOpener.Length);
document.IndexOf(JsonOpener, StringComparison.Ordinal) + JsonOpener.Length
);
var betweenJsonOpenerAndCloser = afterJsonOpener.Substring(0,
afterJsonOpener.IndexOf(jsonCloser, StringComparison.Ordinal));
afterJsonOpener.IndexOf(JsonCloser, StringComparison.Ordinal)
);
if (!betweenJsonOpenerAndCloser.Contains(docs_templates))
if (!betweenJsonOpenerAndCloser.Contains(DocsTemplates))
{
document = afterJsonOpener.Substring(
afterJsonOpener.IndexOf(jsonCloser, StringComparison.Ordinal) + jsonCloser.Length);
newDocument.Append(beforeJson + betweenJsonOpenerAndCloser + jsonCloser);
afterJsonOpener.IndexOf(JsonCloser, StringComparison.Ordinal) + JsonCloser.Length
);
newDocument.Append(beforeJson + betweenJsonOpenerAndCloser + JsonCloser);
continue;
}
var json = betweenJsonOpenerAndCloser.Substring(betweenJsonOpenerAndCloser.IndexOf(docs_templates, StringComparison.Ordinal) + docs_templates.Length);
var templatePath = JsonConvert.DeserializeObject<DocumentPartialTemplateWithValuesDto>(json)?.Path;
var json = betweenJsonOpenerAndCloser.Substring(
betweenJsonOpenerAndCloser.IndexOf(DocsTemplates, StringComparison.Ordinal) + DocsTemplates.Length
);
var template = templates.FirstOrDefault(t => t.Path == templatePath);
if (JsonConvertExtensions.TryDeserializeObject<DocumentPartialTemplateWithValuesDto>(json, out var documentPartialTemplateWithValuesDto))
{
var template = templates.FirstOrDefault(t => t.Path == documentPartialTemplateWithValuesDto.Path);
var beforeTemplate = document.Substring(0,
document.IndexOf(jsonOpener, StringComparison.Ordinal));
var beforeTemplate = document.Substring(0,
document.IndexOf(JsonOpener, StringComparison.Ordinal)
);
newDocument.Append(beforeTemplate + template?.Content + jsonCloser);
newDocument.Append(beforeTemplate + template?.Content + JsonCloser);
document = afterJsonOpener.Substring(
afterJsonOpener.IndexOf(jsonCloser, StringComparison.Ordinal) + jsonCloser.Length);
document = afterJsonOpener.Substring(
afterJsonOpener.IndexOf(JsonCloser, StringComparison.Ordinal) + JsonCloser.Length
);
}
}
newDocument.Append(document);

@ -68,7 +68,7 @@
}
<div class="for-mobile">
<div class="navbar-light">
<div class="navbar-dark">
<button type="button" class="open-dmenu navbar-toggler" aria-label="Close">
<span class="navbar-toggler-icon"></span>
</button>
@ -108,7 +108,7 @@
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">
<i class="fa fa-check-square-o" aria-hidden="true" data-toggle="tooltip" title="@L["Version"]"></i>
<i class="fas fa-code-branch" aria-hidden="true" data-toggle="tooltip" title="@L["Version"]"></i>
</label>
</div>
@ -124,15 +124,15 @@
@if (Model.LanguageSelectListItems.Count > 1)
{
<div class="col @(Model.VersionSelectItems.Any()?"pl-0":"")">
<div class="col-5 @(Model.VersionSelectItems.Any()?"pl-0":"")">
<div class="docs-version docs-language @(Model.VersionSelectItems.Any()?"pl-1":"")">
<div class="version-select">
<div class="input-group">
<div class="input-group-prepend">
@*<div class="input-group-prepend">
<label class="input-group-text">
<i class="fa fa-globe" aria-hidden="true" data-toggle="tooltip" title="@L["Language"]"></i>
</label>
</div>
</div>*@
<select asp-items="Model.LanguageSelectListItems"
class="form-control"
onchange="window.location.replace(this.value)">
@ -163,11 +163,11 @@
@if (Model.FullSearchEnabled)
{
<div class="docs-version mb-4">
<div class="docs-version mt-2 mb-4">
<div class="version-select">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text"><i class="fa fa-filter"></i></label>
<label class="input-group-text"><i class="fa fa-search"></i></label>
</div>
<input class="form-control"
@ -224,7 +224,9 @@
@if (!string.IsNullOrEmpty(Model.Document.EditLink))
{
<a href="@Model.Document.EditLink" target="_blank">
<i class="fa fa-edit"></i> @(L["Edit"]) (@L["LastEditTime"]: @Model.Document.LastUpdatedTime.ToShortDateString())
<i class="fa fa-edit"></i>
@(L["Edit"])
<span class="for-desktop">(@L["LastEditTime"]: @Model.Document.LastUpdatedTime.ToShortDateString())</span>
</a>
}
</div>
@ -232,20 +234,21 @@
<div class="float-right mr-3">
@if (Model.Document.Contributors != null && Model.Document.Contributors.Count > 0)
{
@(L["Contributors"].Value + " :")
<span class="for-desktop">
@(L["Contributors"].Value + " :") </span>
@foreach (var contributor in Model.Document.Contributors)
{
<a href="@contributor.UserProfileUrl" target="_blank">
<img src="@contributor.AvatarUrl"
class="rounded-circle"
class="rounded-circle"
alt="Avatar"
height="21"
width="21"
height="21"
width="21"
title="@contributor.Username" />
</a>
}
}
</div>
}
</div>
</div>

@ -15,53 +15,73 @@
<style>
highlight {
font-weight: bold;
color: red;
font-style: italic;
color: #212529;
background: #f9efa6;
padding: 0 3px;
border-radius: 3px;
}
body {
background-color: rgba(0, 0, 0, 0.03);
}
</style>
}
<div class="container">
<div class="container mb-5">
<form method="get" action="/search/@Model.LanguageCode/@Model.ProjectName/@Model.Version/" class="mt-4">
<input type="text" asp-for="@Model.KeyWord" class="form-control" />
<button type="submit" class="btn-block btn-primary btn-lg mt-3">Search</button>
<h4 class="text-center my-3">Search in Documents</h4>
<div class="card rounded">
<div class="p-3 bg-white">
<div class="form-row">
<div class="col"><input type="text" asp-for="@Model.KeyWord" class="form-control" /></div>
<div class="col-auto"><button type="submit" class="btn btn-primary px-md-5">Search</button></div>
</div>
</div>
</div>
</form>
<div class="my-3 p-3 bg-white rounded">
<h6 class="border-bottom pb-4 mb-0">Search results</h6>
@foreach (var docs in Model.SearchOutputs)
{
<div class="media text-muted pt-3">
<div class="media-body pb-3 mb-0 small border-bottom">
<div class="list-group">
<div class="card mt-4 rounded">
<div class="card-header">
<h5 class="text-center ">Search Results</h5>
</div>
<div class="p-5 card-body">
@foreach (var docs in Model.SearchOutputs)
{
<div class="media text-muted">
<div class="media-body pb-3 small">
<div class="list-group">
@functions
{
string RemoveFileExtensionFromPath(string path)
{
if (path == null)
@functions
{
return null;
}
string RemoveFileExtensionFromPath(string path)
{
if (path == null)
{
return null;
}
return path.EndsWith("." + @Model.Project.Format)
? path.Left(path.Length - Model.Project.Format.Length - 1)
: path;
return path.EndsWith("." + @Model.Project.Format)
? path.Left(path.Length - Model.Project.Format.Length - 1)
: path;
}
}
}
<a href="/@Model.LanguageCode/@Model.ProjectName/@Model.Version/@RemoveFileExtensionFromPath(docs.Name)">
<h3>@RemoveFileExtensionFromPath(docs.Name)</h3></a>
@foreach (var highlight in docs.Highlight)
{
<p class="list-group-item list-group-item-action">@Html.Raw(highlight)</p>
}
<h5 class="mb-3">
<a href="/@Model.LanguageCode/@Model.ProjectName/@Model.Version/@RemoveFileExtensionFromPath(docs.Name)">
@RemoveFileExtensionFromPath(docs.Name)
</a>
</h5>
<div class="mb-4">
@foreach (var highlight in docs.Highlight)
{
<p class=" ">@Html.Raw(highlight)</p>
}
</div>
</div>
</div>
</div>
</div>
}
}
</div>
</div>
</div>

@ -10,21 +10,21 @@
transition: color .25s linear;
color: gray; }
.docs-page .anchorjs-link:hover {
color: #007bff;
text-decoration: none; }
.docs-page .docs-sidebar {
background: #f5f7f9;
padding-right: 1rem;
position: relative;
top: 0px;
left: 0;
position: fixed; }
position: fixed;
background: #1d1d1d; }
.docs-page .docs-sidebar .input-group {
border-radius: 5px;
overflow: hidden; }
.docs-page .docs-sidebar .docs-sidebar-wrapper {
width: 270px;
float: right; }
.docs-page .docs-sidebar .docs-sidebar-wrapper input.form-control {
background: none;
background: #fff;
border: 0; }
.docs-page .docs-sidebar .docs-sidebar-wrapper input.form-control:focus, .docs-page .docs-sidebar .docs-sidebar-wrapper input.form-control:active, .docs-page .docs-sidebar .docs-sidebar-wrapper input.form-control:hover, .docs-page .docs-sidebar .docs-sidebar-wrapper input.form-control:visited {
box-shadow: none; }
@ -33,27 +33,28 @@
padding: 0 1rem;
margin: .25rem 0; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select {
border-radius: 3px;
border: 1px solid #e9ecef; }
border-radius: 3px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select .input-group-text {
padding: 0.375rem 0.6rem;
padding: 0 10px;
font-size: .9rem;
width: 32px;
height: 30px;
width: 26px;
height: 34px;
line-height: 1;
border-radius: 0px;
border: 1px solid #e9ecef; }
border: 0; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select .input-group-text i {
color: #666; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control {
padding: 0 10px;
padding: 0 10px 2px 10px;
border: 0;
min-height: 30px;
height: 30px;
font-size: .85em;
border-radius: 1px; }
min-height: 34px;
height: 34px;
font-size: .9em;
border-radius: 0px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control:focus, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control:active, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control:hover, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control:visited, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control:focus, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control:active, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control:hover, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control:visited {
box-shadow: none; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control {
padding-left: 6px; }
padding: 0 10px 2px 6px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-filter {
padding: 0 1rem;
margin: .5rem 0;
@ -64,7 +65,7 @@
height: 100vh; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list > ul {
display: block;
height: calc(100vh - 220px);
height: calc(100vh - 320px);
overflow-y: auto; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul {
font-size: .935em;
@ -88,6 +89,12 @@
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a.last-link {
top: 11px;
color: #aaa; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li .badge {
text-transform: uppercase;
font-size: 9px;
position: relative;
letter-spacing: .125px;
top: -2px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li span.tree-toggle {
color: #999;
padding: 7px 0;
@ -129,10 +136,10 @@
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree.last-link > span .fa {
transform: rotate(0deg); }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-brand {
font-size: 1.5rem;
font-size: 1.35rem;
color: #000;
font-weight: 700;
padding: 20px 0 10px;
padding: 15px 0 15px;
line-height: 1; }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-brand strong {
font-weight: 300;
@ -153,7 +160,8 @@
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-logo-desc strong {
display: block; }
.docs-page .docs-content {
overflow-x: scroll; }
overflow-x: scroll;
min-height: 100vh; }
.docs-page .docs-content .contributors {
position: absolute;
top: 15px;
@ -222,10 +230,11 @@
.docs-page .docs-content article.docs-body .blockquote {
margin-bottom: 1rem;
margin-left: 0;
border-left: 2px solid gray;
padding: 1em;
background-color: #eee;
padding-bottom: .2em; }
border-left: 3px solid #d2dbe4;
padding: 1em 1.5em;
background-color: #e9edf1;
padding-bottom: .2em;
font-size: 1em; }
.docs-page .docs-content article.docs-body img {
max-width: 100%;
border: 1px solid #f4f5f7;
@ -257,10 +266,8 @@
.docs-page .docs-page-index .docs-inner-anchors {
position: fixed;
top: 0px;
/* max-width: 270px; */
padding: 10px;
font-size: .90em;
/* height: 100vh; */ }
font-size: .90em; }
.docs-page .docs-page-index .docs-inner-anchors .navbar .nav-pills {
font-size: .92em;
margin-left: 15px;
@ -300,83 +307,95 @@
.docs-page .docs-page-index .scroll-top-btn.showup {
display: block; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control, .docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control {
background: #000000;
color: white; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control {
background: #000000;
color: white; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control::placeholder {
color: white;
opacity: .5; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version .version-select label {
background: #000000;
border-color: #000000;
color: #ddd; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-filter .form-control {
background: #333;
color: #999; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-filter select {
border: 0;
border-radius: 6px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-filter .filter-icon i.fa {
color: #aaa; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a {
color: #aaa;
border-bottom: 0; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a:hover {
color: #fff; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon {
font-size: .85em;
transition: .3s;
width: 18px;
height: 18px;
text-align: center;
padding: 0;
line-height: 1;
border-radius: 50%;
margin-right: 4px;
position: absolute;
left: 2px;
top: 11px;
color: #aaa;
cursor: default; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon .fa-long-arrow-right.no-link {
color: #555; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon .fa-chevron-right {
cursor: pointer; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon.last-link {
top: 11px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li span.tree-toggle {
color: #555;
padding: 7px 0;
display: block;
border-bottom: 0; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a {
color: #fff;
transition: .4s; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a span .fa {
color: #fff; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a span:not(.last-link) .fa {
transform: rotate(90deg);
color: #fff; }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-brand {
color: #fff;
text-transform: uppercase;
white-space: unset; }
.docs-page .docs-sidebar .docs-top .navbar-logo .go-back-site {
color: #fff;
text-align: center;
display: block;
width: 100%;
background: #444;
padding: 6px 0 8px;
border-radius: 5px; }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-logo-desc {
color: #ddd; }
@media (min-width: 1100px) {
.container {
max-width: 1080px; }
.docs-page .docs-sidebar.dark-sidebar {
background: #191919; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select {
border: 1px solid #333; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select select.form-control, .docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control {
background: #191919;
border-color: #191919;
color: #999; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control {
background: #191919;
border-color: #191919;
color: #999; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select input.form-control::placeholder {
color: #999;
opacity: .5; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-version .version-select label {
background: #333;
border-color: #333;
color: #ddd; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-filter .form-control {
background: #333;
color: #999; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-filter select {
border: 0;
border-radius: 6px; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-filter .filter-icon i.fa {
color: #aaa; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a {
color: #aaa;
border-bottom: 0; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a:hover {
color: #fff; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon {
font-size: .85em;
transition: .3s;
width: 18px;
height: 18px;
text-align: center;
padding: 0;
line-height: 1;
border-radius: 50%;
margin-right: 4px;
position: absolute;
left: 2px;
top: 11px;
color: #aaa;
cursor: default; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon .fa-long-arrow-right.no-link {
color: #555; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon .fa-chevron-right {
cursor: pointer; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li a .plus-icon.last-link {
top: 11px; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li span.tree-toggle {
color: #555;
padding: 7px 0;
display: block;
border-bottom: 0; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a {
color: #fff;
transition: .4s; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a span .fa {
color: #fff; }
.docs-page .docs-sidebar.dark-sidebar .docs-sidebar-wrapper .docs-tree-list ul li.selected-tree > a span:not(.last-link) .fa {
transform: rotate(90deg);
color: #fff; }
.docs-page .docs-sidebar.dark-sidebar .docs-top .navbar-logo .navbar-brand {
color: #fff;
text-transform: uppercase;
white-space: unset; }
.docs-page .docs-sidebar.dark-sidebar .docs-top .navbar-logo .go-back-site {
color: #fff; }
.docs-page .docs-sidebar.dark-sidebar .docs-top .navbar-logo .navbar-logo-desc {
color: #ddd; } }
max-width: 1080px; } }
@media (min-width: 1366px) {
.container {
@ -420,30 +439,21 @@
left: 0;
width: 100%;
z-index: 100;
background: #f5f7f9;
background: #1d1d1d;
display: none; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-tree-list .docs-filter {
padding: 0 0 1rem !important; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version label {
width: 36px;
text-align: center;
padding-left: 0;
padding-right: 0;
display: inline-block;
line-height: 26px; }
.docs-page .docs-sidebar .docs-sidebar-wrapper .docs-version input.form-control {
padding-left: 12px !important; }
.docs-page .docs-sidebar .docs-top .navbar-logo {
padding: 0rem;
padding-top: .3rem;
display: block;
text-align: center; }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-brand {
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: 700;
display: block;
margin-right: 0em;
padding: 7px 0 10px;
padding: 10px 0 15px;
text-transform: uppercase; }
.docs-page .docs-sidebar .docs-top .navbar-logo .navbar-brand .docs-logo {
width: 110px; }

File diff suppressed because one or more lines are too long

@ -16,19 +16,20 @@ body {
}
.anchorjs-link:hover {
color: #007bff;
text-decoration: none;
}
.docs-sidebar {
background: #f5f7f9;
padding-right: 1rem;
position: relative;
top: 0px;
left: 0;
position: fixed;
.docs-language {
background: #1d1d1d;
.input-group {
border-radius: 5px;
overflow: hidden;
}
.docs-sidebar-wrapper {
@ -36,8 +37,6 @@ body {
float: right;
input.form-control {
background: none;
background: #fff;
border: 0;
&:focus, &:active, &:hover, &:visited {
@ -52,25 +51,28 @@ body {
.version-select {
border-radius: 3px;
border: 1px solid #e9ecef;
.input-group-text {
padding: 0.375rem 0.6rem;
padding: 0 10px;
font-size: .9rem;
width: 32px;
height: 30px;
width: 26px;
height: 34px;
line-height: 1;
border-radius: 0px;
border: 1px solid #e9ecef;
border: 0;
i {
color: #666;
}
}
select.form-control, input.form-control {
padding: 0 10px;
padding: 0 10px 2px 10px;
border: 0;
min-height: 30px;
height: 30px;
font-size: .85em;
border-radius: 1px;
min-height: 34px;
height: 34px;
font-size: .9em;
border-radius: 0px;
&:focus, &:active, &:hover, &:visited {
box-shadow: none;
@ -78,10 +80,7 @@ body {
}
select.form-control {
padding-left: 6px;
}
label {
padding: 0 10px 2px 6px;
}
}
}
@ -104,7 +103,7 @@ body {
> ul {
display: block;
height: calc(100vh - 220px);
height: calc(100vh - 320px);
overflow-y: auto;
}
@ -139,6 +138,14 @@ body {
}
}
.badge {
text-transform: uppercase;
font-size: 9px;
position: relative;
letter-spacing: .125px;
top: -2px;
}
span.tree-toggle {
color: #999;
padding: 7px 0;
@ -224,16 +231,16 @@ body {
.docs-top {
.navbar-logo {
.navbar-brand {
font-size: 1.5rem;
font-size: 1.35rem;
color: #000;
font-weight: 700;
padding: 20px 0 10px;
padding: 15px 0 15px;
line-height: 1;
strong {
font-weight: 300;
text-transform: uppercase;
font-size: .7em;
font-size: .7em;
letter-spacing: 1px;
}
}
@ -264,6 +271,7 @@ body {
.docs-content {
overflow-x: scroll;
min-height: 100vh;
.contributors {
position: absolute;
@ -372,10 +380,11 @@ body {
.blockquote {
margin-bottom: 1rem;
margin-left: 0;
border-left: 2px solid gray;
padding: 1em;
background-color: #eee;
padding-bottom: .2em
border-left: 3px solid #d2dbe4;
padding: 1em 1.5em;
background-color: #e9edf1;
padding-bottom: .2em;
font-size: 1em;
}
img {
@ -433,10 +442,9 @@ body {
.docs-inner-anchors {
position: fixed;
top: 0px;
/* max-width: 270px; */
padding: 10px;
font-size: .90em;
/* height: 100vh; */
.navbar {
.nav-pills {
font-size: .92em;
@ -508,163 +516,189 @@ body {
}
@media (min-width: 1100px) {
.container {
max-width: 1080px;
}
.docs-page {
.docs-sidebar {
&.dark-sidebar {
background: #191919;
.docs-sidebar-wrapper {
.docs-version {
.version-select {
border: 1px solid #333;
.docs-page {
.docs-sidebar {
.docs-sidebar-wrapper {
select.form-control, input.form-control {
background: #191919;
border-color: #191919;
color: #999;
}
.docs-version {
.version-select {
input.form-control {
background: #191919;
border-color: #191919;
color: #999;
select.form-control, input.form-control {
background: #000000;
color: white;
}
&::placeholder {
color: #999;
opacity: .5;
}
}
input.form-control {
background: #000000;
color: white;
label {
background: #333;
border-color: #333;
color: #ddd;
}
&::placeholder {
color: white;
opacity: .5;
}
}
.docs-filter {
.form-control {
background: #333;
color: #999;
}
label {
background: #000000;
border-color: #000000;
color: #ddd;
}
}
}
select {
border: 0;
border-radius: 6px;
}
.docs-filter {
.form-control {
background: #333;
color: #999;
}
.filter-icon {
i.fa {
color: #aaa;
}
}
}
select {
border: 0;
border-radius: 6px;
}
.docs-tree-list {
ul {
li {
.filter-icon {
i.fa {
color: #aaa;
}
}
}
.docs-tree-list {
ul {
li {
a {
color: #aaa;
border-bottom: 0;
&:hover {
color: #fff;
}
a {
color: #aaa;
border-bottom: 0;
.plus-icon {
font-size: .85em;
transition: .3s;
width: 18px;
height: 18px;
text-align: center;
padding: 0;
line-height: 1;
border-radius: 50%;
margin-right: 4px;
position: absolute;
left: 2px;
top: 11px;
color: #aaa;
cursor: default;
.fa-long-arrow-right {
&.no-link {
color: #555;
}
}
&:hover {
color: #fff;
}
.fa-chevron-right {
cursor: pointer;
}
.plus-icon {
font-size: .85em;
transition: .3s;
width: 18px;
height: 18px;
text-align: center;
padding: 0;
line-height: 1;
border-radius: 50%;
margin-right: 4px;
position: absolute;
left: 2px;
top: 11px;
color: #aaa;
cursor: default;
&.last-link {
top: 11px;
}
.fa-long-arrow-right {
&.no-link {
color: #555;
}
}
.fa-chevron-right {
cursor: pointer;
}
span.tree-toggle {
color: #555;
padding: 7px 0;
display: block;
border-bottom: 0;
&.last-link {
top: 11px;
}
}
}
&.selected-tree {
> a {
color: #fff;
transition: .4s;
span.tree-toggle {
color: #555;
padding: 7px 0;
display: block;
border-bottom: 0;
}
span {
.fa {
color: #fff;
}
&:not(.last-link) .fa {
transform: rotate(90deg);
color: #fff;
}
}
&.selected-tree {
> a {
color: #fff;
transition: .4s;
span {
.fa {
color: #fff;
}
&:not(.last-link) .fa {
transform: rotate(90deg);
color: #fff;
}
}
}
}
}
}
}
}
.docs-top {
.navbar-logo {
.navbar-brand {
color: #fff;
text-transform: uppercase;
white-space: unset;
}
.docs-top {
.navbar-logo {
.navbar-brand {
color: #fff;
text-transform: uppercase;
white-space: unset;
}
.go-back-site {
color: #fff;
}
.go-back-site {
color: #fff;
text-align: center;
display: block;
width: 100%;
background: #444;
padding: 6px 0 8px;
border-radius: 5px;
}
.navbar-logo-desc {
color: #ddd;
}
}
.navbar-logo-desc {
color: #ddd;
}
}
}
}
}
@media (min-width: 1100px) {
.container {
max-width: 1080px;
}
}
@media (min-width: 1366px) {
.container {
max-width: 1340px;
@ -720,7 +754,7 @@ body {
left: 0;
width: 100%;
z-index: 100;
background: #f5f7f9;
background: #1d1d1d;
display: none;
.docs-filter {
@ -732,17 +766,6 @@ body {
}
.docs-version {
label {
width: 36px;
text-align: center;
padding-left: 0;
padding-right: 0;
display: inline-block;
line-height: 26px;
}
input.form-control {
padding-left: 12px !important;
}
}
}
@ -754,11 +777,11 @@ body {
text-align: center;
.navbar-brand {
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: 700;
display: block;
margin-right: 0em;
padding: 7px 0 10px;
padding: 10px 0 15px;
text-transform: uppercase;
.docs-logo {
@ -971,6 +994,7 @@ div.code-toolbar > .toolbar a {
background-color: #bddcfd;
border: 1px solid #bddcfd;
@media screen and (max-width: 1366px) {
display: none;
}
@ -1009,4 +1033,5 @@ div.code-toolbar > .toolbar a {
display: none;
}
}
}
}

@ -19,6 +19,7 @@
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap\Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Packages\Volo.Abp.AspNetCore.Mvc.UI.Packages.csproj" />
<ProjectReference Include="..\..\..\..\framework\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj" />
<ProjectReference Include="..\Volo.Docs.Domain\Volo.Docs.Domain.csproj" />
<ProjectReference Include="..\Volo.Docs.HttpApi\Volo.Docs.HttpApi.csproj" />
<PackageReference Include="Markdig.Signed" Version="0.18.0" />
<PackageReference Include="Scriban" Version="2.1.1" />

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

@ -1,8 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Identity
{
public class IdentityRoleCreateOrUpdateDtoBase
public class IdentityRoleCreateOrUpdateDtoBase : ExtensibleObject
{
[Required]
[StringLength(IdentityRoleConsts.MaxNameLength)]

@ -4,12 +4,12 @@ using Volo.Abp.Domain.Entities;
namespace Volo.Abp.Identity
{
public class IdentityRoleDto : EntityDto<Guid>, IHasConcurrencyStamp
public class IdentityRoleDto : ExtensibleEntityDto<Guid>, IHasConcurrencyStamp
{
public string Name { get; set; }
public bool IsDefault { get; set; }
public bool IsStatic { get; set; }
public bool IsPublic { get; set; }

@ -1,9 +1,10 @@
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Identity
{
public abstract class IdentityUserCreateOrUpdateDtoBase
public abstract class IdentityUserCreateOrUpdateDtoBase : ExtensibleObject
{
[Required]
[StringLength(IdentityUserConsts.MaxUserNameLength)]

@ -5,7 +5,7 @@ using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Identity
{
public class IdentityUserDto : FullAuditedEntityDto<Guid>, IMultiTenant, IHasConcurrencyStamp
public class IdentityUserDto : ExtensibleFullAuditedEntityDto<Guid>, IMultiTenant, IHasConcurrencyStamp
{
public Guid? TenantId { get; set; }

@ -1,6 +1,8 @@
namespace Volo.Abp.Identity
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Identity
{
public class ProfileDto
public class ProfileDto : ExtensibleObject
{
public string UserName { get; set; }

@ -1,8 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Identity
{
public class UpdateProfileDto
public class UpdateProfileDto : ExtensibleObject
{
[StringLength(IdentityUserConsts.MaxUserNameLength)]
public string UserName { get; set; }

@ -6,9 +6,14 @@ namespace Volo.Abp.Identity
{
public AbpIdentityApplicationModuleAutoMapperProfile()
{
CreateMap<IdentityUser, IdentityUserDto>();
CreateMap<IdentityRole, IdentityRoleDto>();
CreateMap<IdentityUser, ProfileDto>();
CreateMap<IdentityUser, IdentityUserDto>()
.MapExtraProperties();
CreateMap<IdentityRole, IdentityRoleDto>()
.MapExtraProperties();
CreateMap<IdentityUser, ProfileDto>()
.MapExtraProperties();
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save