Merge branch 'dev' into maliming/Interceptors-ignore-types

pull/3348/head
maliming 5 years ago
commit 651ca79ce0

@ -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"
}
}

@ -89,13 +89,15 @@ public static class IdentityDbContextModelBuilderExtensions
builder.Entity<IdentityUser>(b =>
{
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity
});
builder.Entity<IdentityUserClaim>(b =>
{
b.ToTable(options.TablePrefix + "UserClaims", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity
});
@ -104,6 +106,7 @@ public static class IdentityDbContextModelBuilderExtensions
}
````
* **Do** call `b.ConfigureByConvention();` for each entity mapping (as shown above).
* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example:
````C#

@ -4,7 +4,7 @@ In some cases, you may want to add some additional properties (and database fiel
## Extra Properties
[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these entities.
[Extra properties](Entities.md) is a way of storing some additional data on an entity without changing it. The entity should implement the `IHasExtraProperties` interface to allow it. All the aggregate root entities defined in the pre-built modules implement the `IHasExtraProperties` interface, so you can store extra properties on these objects.
Example:
@ -25,7 +25,35 @@ Extra properties are stored as a single `JSON` formatted string value in the dat
See the [entities document](Entities.md) for more about the extra properties system.
> It is possible to perform a **business logic** based on the value of an extra property. You can **override** a service method and get or set the value as shown above. Overriding services will be discussed below.
> It is possible to perform a **business logic** based on the value of an extra property. You can [override a service method](Customizing-Application-Modules-Overriding-Services.md), then get or set the value as shown above.
## Entity Extensions (EF Core)
As mentioned above, all extra properties of an entity are stored as a single JSON object in the database table. This is not so natural especially when you want to;
* Create **indexes** and **foreign keys** for an extra property.
* Write **SQL** or **LINQ** using the extra property (search table by the property value, for example).
* Creating your **own entity** maps to the same table, but defines an extra property as a **regular property** in the entity (see the [EF Core migration document](Entity-Framework-Core-Migrations.md) for more).
To overcome the difficulties described above, ABP Framework entity extension system for the Entity Framework Core that allows you to use the same extra properties API defined above, but store a desired property as a separate field in the database table.
Assume that you want to add a `SocialSecurityNumber` to the `IdentityUser` entity of the [Identity Module](Modules/Identity.md). You can use the `EntityExtensionManager` static class:
````csharp
EntityExtensionManager.AddProperty<IdentityUser, string>(
"SocialSecurityNumber",
b => { b.HasMaxLength(32); }
);
````
* You provide the `IdentityUser` as the entity name, `string` as the type of the new property, `SocialSecurityNumber` as the property name (also, the field name in the database table).
* You also need to provide an action that defines the database mapping properties using the [EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties).
> This code part must be executed before the related `DbContext` used. The [application startup template](Startup-Templates/Application.md) defines a static class named `YourProjectNameEntityExtensions`. You can define your extensions in this class to ensure that it is executed in the proper time. Otherwise, you should handle it yourself.
Once you define an entity extension, you then need to use the standard [Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration) and [Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database) commands of the EF Core to create a code first migration class and update your database.
You can then use the same extra properties system defined in the previous section to manipulate the property over the entity.
## Creating a New Entity Maps to the Same Database Table/Collection
@ -146,4 +174,5 @@ public class MyDistributedIdentityUserCreatedEventHandler :
## See Also
* [Migration System for the EF Core](Entity-Framework-Core-Migrations.md)
* [Customizing the Existing Modules](Customizing-Application-Modules-Guide.md)

@ -373,16 +373,19 @@ So, you can directly use the `ExtraProperties` property to use the dictionary A
The way to store this dictionary in the database depends on the database provider you're using.
* For [Entity Framework Core](Entity-Framework-Core.md), it is stored in a single `ExtraProperties` field as a `JSON` string. 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.
* 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`.
* 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
Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way. You normally **don't need** to this system for your own entities, because it has the following drawbacks:
Extra Properties system is especially useful if you are using a **re-usable module** that defines an entity inside and you want to get/set some data related to this entity in an easy way.
* It is **not fully type safe**.
You normally **don't need** to this system for your own entities, because it has the following drawbacks:
* It is **not fully type safe** since it works with strings as property names.
* It is **not easy to [auto map](Object-To-Object-Mapping.md)** these properties from/to other objects.
* It **doesn't create fields** in the database table for EF Core, so it will not be easy to create indexes or search/order by this field in the database side.
### Extra Properties Behind Entities

@ -93,7 +93,7 @@ From the database point of view, there are three important projects those will b
This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application.
**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your custom [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity:
**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity:
````csharp
[ConnectionStringName("Default")]
@ -117,15 +117,15 @@ public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
builder.Entity<AppUser>(b =>
{
//Sharing the same table "AbpUsers" with the IdentityUser
b.ToTable("AbpUsers");
//Configure base properties
//Sharing the same Users table with the IdentityUser
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users");
b.ConfigureByConvention();
b.ConfigureAbpUser();
//Moved customization of the "AbpUsers" table to an extension method
b.ConfigureCustomUserProperties();
/* Configure mappings for your additional properties
* Also see the MyProjectNameEntityExtensions class
*/
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
@ -188,12 +188,6 @@ public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbCo
builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();
}
@ -274,7 +268,7 @@ In this way, the mapping configuration of a module can be shared between `DbCont
You may want to **reuse a table** of a depended module in your application. In this case, you have two options:
1. You can **directly use the entity** defined by the module.
1. You can **directly use the entity** defined by the module (you can still [extend the entity](Customizing-Application-Modules-Extending-Entities.md) in some level).
2. You can **create a new entity** mapping to the same database table.
###### Use the Entity Defined by a Module
@ -376,10 +370,8 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<AppRole>(b =>
{
b.ToTable("AbpRoles");
b.ConfigureByConvention();
b.ConfigureCustomRoleProperties();
b.Property(x => x.Title).HasMaxLength(128);
});
...
@ -395,69 +387,44 @@ We added the following lines:
````csharp
builder.Entity<AppRole>(b =>
{
b.ToTable("AbpRoles");
b.ToTable("AbpRoles");
b.ConfigureByConvention();
b.ConfigureCustomRoleProperties();
b.Property(x => x.Title).HasMaxLength(128);
});
````
* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity.
* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it.
`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project):
````csharp
public static void ConfigureCustomRoleProperties<TRole>(this EntityTypeBuilder<TRole> b)
where TRole : class, IEntity<Guid>
{
b.Property<string>(nameof(AppRole.Title)).HasMaxLength(128);
}
````
* This method only defines the **custom properties** of your entity.
* Unfortunately, we can not utilize the fully **type safety** here (by referencing the `AppRole` entity). The best we can do is to use the `Title` name as type safe. This is because of EF Core migration system can not map two unrelated entity classes to the same database table.
You've configured the custom property for your `DbContext` that is used by your application on the runtime. We also need to configure the `MigrationsDbContext`.
You've configured the custom property for your `DbContext` used by your application on the runtime. We also need to configure the `MigrationsDbContext`.
Open the `MigrationsDbContext` (`BookStoreMigrationsDbContext` for this example) and change as shown below:
Instead of directly changing the `MigrationsDbContext`, we should use the entity extension system of the ABP Framework. Find the `YourProjectNameEntityExtensions` class in the `.EntityFrameworkCore` project of your solution (`BookStoreEntityExtensions` for this example) and change it as shown below:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
public static class MyProjectNameEntityExtensions
{
base.OnModelCreating(builder);
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
/* Include modules to your migration db context */
...
/* Configure customizations for entities from the modules included */
//CONFIGURE THE CUSTOM ROLE PROPERTIES
builder.Entity<IdentityRole>(b =>
public static void Configure()
{
b.ConfigureCustomRoleProperties();
});
...
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();
OneTimeRunner.Run(() =>
{
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
});
}
}
````
Only added the following lines:
> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)`.
````csharp
builder.Entity<IdentityRole>(b =>
{
b.ConfigureCustomRoleProperties();
});
````
`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.
See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system.
In this way, we re-used the extension method that is used to configure custom property mappings for the role. But, this time, did the same customization for the `IdentityRole` entity.
> We've repeated a similar database mapping code, like `HasMaxLength(128)`, in both classes.
Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore.DbMigrations` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project):
@ -536,7 +503,7 @@ Instead of creating a new entity class to add a custom property, you can use the
###### Using the ExtraProperties
All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property, which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity.
All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property (because they implement the `IHasExtraProperties` interface), which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity.
For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example:
@ -553,16 +520,13 @@ public class IdentityRoleExtendingService : ITransientDependency
public async Task<string> GetTitleAsync(Guid id)
{
var role = await _identityRoleRepository.GetAsync(id);
return role.GetProperty<string>("Title");
}
public async Task SetTitleAsync(Guid id, string newTitle)
{
var role = await _identityRoleRepository.GetAsync(id);
role.SetProperty("Title", newTitle);
await _identityRoleRepository.UpdateAsync(role);
}
}
@ -575,12 +539,20 @@ In this way, you can easily attach any type of value to an entity of a depended
* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields.
* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors.
###### Using the Entity Extensions System
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.
###### Creating a New Table
Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table.
In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here).
> See the "[extending entities](Customizing-Application-Modules-Extending-Entities.md)" guide for more details on extending entities, including data duplication and synchronization tips.
#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path
As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path.

@ -58,6 +58,53 @@ namespace MyCompany.MyProject
}
````
### About the EF Core Fluent Mapping
The [application startup template](Startup-Templates/Application.md) has been configured to use the [EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/) to map your entities to your database tables.
You can still use the **data annotation attributes** (like `[Required]`) on the properties of your entity while the ABP documentation generally follows the **fluent mapping API** approach. It is up to you.
ABP Framework has some **base entity classes** and **conventions** (see the [entities document](Entities.md)) and it provides some useful **extension methods** to configure the properties inherited from the base entity classes.
#### ConfigureByConvention Method
`ConfigureByConvention()` is the main extension method that **configures all the base properties** and conventions for your entities. So, it is a **best practice** to call this method for all your entities, in your fluent mapping code.
**Example**: Assume that you've a `Book` entity derived from `AggregateRoot<Guid>` base class:
````csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
}
````
You can override the `OnModelCreating` method in your `DbContext` and configure the mapping as shown below:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
//Always call the base method
base.OnModelCreating(builder);
builder.Entity<Book>(b =>
{
b.ToTable("Books");
//Configure the base properties
b.ConfigureByConvention();
//Configure other properties (if you are using the fluent API)
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
````
* Calling `b.ConfigureByConvention()` is important here to properly **configure the base properties**.
* You can configure the `Name` property here or you can use the **data annotation attributes** (see the [EF Core document](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)).
> While there are many extension methods to configure your base properties, `ConfigureByConvention()` internally calls them if necessary. So, it is enough to call it.
### Configure the Connection String Selection
If you have multiple databases in your application, you can configure the connection string name for your DbContext using the `[ConnectionStringName]` attribute. Example:
@ -225,7 +272,7 @@ public override async Task DeleteAsync(
}
````
### Access to the EF Core API
## Access to the EF Core API
In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example:
@ -251,9 +298,59 @@ 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.
### Advanced Topics
## Extra Properties & Entity 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.
#### Set Default Repository Classes
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.
### AddProperty Method
`AddProperty` method of the `EntityExtensionManager` allows you to define additional properties for an entity type.
**Example**: Add `Title` property (database field) to the `IdentityRole` entity:
````csharp
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
````
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.
>`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.
### ConfigureExtensions
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:
````csharp
builder.Entity<YourEntity>(b =>
{
b.ConfigureExtensions();
//...
});
````
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.
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
Default generic repositories are implemented by `EfCoreRepository` class by default. You can create your own implementation and use it for all the default repository implementations.
@ -299,7 +396,7 @@ context.Services.AddAbpDbContext<BookStoreDbContext>(options =>
});
```
#### Set Base DbContext Class or Interface for Default Repositories
### Set Base DbContext Class or Interface for Default Repositories
If your DbContext inherits from another DbContext or implements an interface, you can use that base class or interface as DbContext for default repositories. Example:
@ -331,7 +428,7 @@ public class BookRepository : EfCoreRepository<IBookStoreDbContext, Book, Guid>,
One advantage of using an interface for a DbContext is then it will be replaceable by another implementation.
#### Replace Other DbContextes
### Replace Other DbContextes
Once you properly define and use an interface for DbContext, then any other implementation can replace it using the `ReplaceDbContext` option:
@ -344,3 +441,7 @@ context.Services.AddAbpDbContext<OtherDbContext>(options =>
````
In this example, `OtherDbContext` implements `IBookStoreDbContext`. This feature allows you to have multiple DbContext (one per module) on development, but single DbContext (implements all interfaces of all DbContexts) on runtime.
## See Also
* [Entities](Entities.md)

@ -0,0 +1,6 @@
# Getting Started with the Startup Templates
See the following tutorials to learn how to get started with the ABP Framework using the pre-built application startup templates:
* [Getting Started With the ASP.NET Core MVC / Razor Pages UI](Getting-Started-AspNetCore-MVC-Template.md)
* [Getting Started with the Angular UI](Getting-Started-Angular-Template.md)

@ -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.

@ -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)

@ -0,0 +1,844 @@
# Linked List (Doubly)
The core module provides a useful data structure known as a [doubly linked list](https://en.wikipedia.org/wiki/Doubly_linked_list). Briefly, a doubly linked list is a series of records (a.k.a. nodes) which has information on the previous node, the next node, and its own value (or data).
## Getting Started
To create a doubly linked list, all you have to do is to import and create a new instance of it:
```js
import { LinkedList } from '@abp/ng.core';
const list = new LinkedList();
```
The constructor does not get any parameters.
## Usage
### 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.
#### addHead(value: T): ListNode\<T\>
Adds a node with given value as the first node in list:
```js
list.addHead('a');
// "a"
list.addHead('b');
// "b" <-> "a"
list.addHead('c');
// "c" <-> "b" <-> "a"
```
#### addTail(value: T): ListNode\<T\>
Adds a node with given value as the last node in list:
```js
list.addTail('a');
// "a"
list.addTail('b');
// "a" <-> "b"
list.addTail('c');
// "a" <-> "b" <-> "c"
```
#### addAfter(value: T, previousValue: T, compareFn = compare): ListNode\<T\>
Adds a node with given value after the first node that has the previous value:
```js
list.addTail('a');
list.addTail('b');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "b" <-> "c"
list.addAfter('x', 'b');
// "a" <-> "b" <-> "x" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addTail({ x: 1 });
list.addTail({ x: 2 });
list.addTail({ x: 3 });
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addAfter({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"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:
```js
list.addTail('a');
list.addTail('b');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "b" <-> "c"
list.addBefore('x', 'b');
// "a" <-> "x" <-> "b" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.addTail({ x: 1 });
list.addTail({ x: 2 });
list.addTail({ x: 3 });
// {"x":1} <-> {"x":2} <-> {"x":3}
list.addBefore({ x: 0 }, { x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"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:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
list.addByIndex('x', 2);
// "a" <-> "b" <-> "x" <-> "c"
```
#### add(value: T).head(): ListNode\<T\>
Adds a node with given value as the first node in list:
```js
list.add('a').head();
// "a"
list.add('b').head();
// "b" <-> "a"
list.add('c').head();
// "c" <-> "b" <-> "a"
```
> This is an alternative API for `addHead`.
#### add(value: T).tail(): ListNode\<T\>
Adds a node with given value as the last node in list:
```js
list.add('a').tail();
// "a"
list.add('b').tail();
// "a" <-> "b"
list.add('c').tail();
// "a" <-> "b" <-> "c"
```
> This is an alternative API for `addTail`.
#### add(value: T).after(previousValue: T, compareFn = compare): ListNode\<T\>
Adds a node with given value after the first node that has the previous value:
```js
list.add('a').tail();
list.add('b').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "b" <-> "c"
list.add('x').after('b');
// "a" <-> "b" <-> "x" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).after({ x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
> This is an alternative API for `addAfter`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### add(value: T).before(nextValue: T, compareFn = compare): ListNode\<T\>
Adds a node with given value before the first node that has the next value:
```js
list.add('a').tail();
list.add('b').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "b" <-> "c"
list.add('x').before('b');
// "a" <-> "x" <-> "b" <-> "b" <-> "c"
```
You may pass a custom compare function to detect the searched value:
```js
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).before({ x: 2 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
```
> This is an alternative API for `addBefore`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### add(value: T).byIndex(position: number): ListNode\<T\>
Adds a node with given value at the specified position in the list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').byIndex(2);
// "a" <-> "b" <-> "x" <-> "c"
```
> This is an alternative API for `addByIndex`.
### 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.
#### dropHead(): ListNode\<T\> | undefined
Removes the first node from the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
list.dropHead();
// "b" <-> "c"
```
#### dropTail(): ListNode\<T\> | undefined
Removes the last node from the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
list.dropTail();
// "a" <-> "b"
```
#### 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');
// "a" <-> "b" <-> "c"
list.dropByIndex(1);
// "a" <-> "c"
```
#### 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');
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.dropByValue('x');
// "a" <-> "b" <-> "x" <-> "c"
```
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 });
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### dropByValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
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');
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.dropByValueAll('x');
// "a" <-> "b" <-> "c"
```
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 });
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.dropByValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":3}
```
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### drop().head(): ListNode\<T\> | undefined
Removes the first node in list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().head();
// "b" <-> "c"
```
> This is an alternative API for `dropHead`.
#### drop().tail(): ListNode\<T\> | undefined
Removes the last node in list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().tail();
// "a" <-> "b"
```
> This is an alternative API for `dropTail`.
#### drop().byIndex(position: number): ListNode\<T\> | undefined
Removes the node with the specified position from the list:
```js
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().byIndex(1);
// "a" <-> "c"
```
> This is an alternative API for `dropByIndex`.
#### drop().byValue(value: T, compareFn = compare): ListNode\<T\> | undefined
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();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.drop().byValue('x');
// "a" <-> "b" <-> "x" <-> "c"
```
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();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.drop().byValue({ x: 0 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":0} <-> {"x":3}
```
> This is an alternative API for `dropByValue`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
#### drop().byValueAll(value: T, compareFn = compare): ListNode\<T\>\[\]
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();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.drop().byValueAll('x');
// "a" <-> "b" <-> "c"
```
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();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
list.drop().byValueAll({ x: 0 }, (v1, v2) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":3}
```
> This is an alternative API for `dropByValueAll`.
>
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
### How to Find Nodes
There are a few methods to find specific nodes in a linked list.
#### find(predicate: ListIteratorFunction\<T\>): ListNode\<T\> | undefined
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');
// "a" <-> "b" <-> "b" <-> "c"
const found = list.find(node => node.value === 'b');
/*
found.value === "b"
found.previous.value === "a"
found.next.value === "b"
*/
```
#### findIndex(predicate: ListIteratorFunction\<T\>): number
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');
// "a" <-> "b" <-> "b" <-> "c"
const i0 = list.findIndex(node => node.next && node.next.value === 'b');
const i1 = list.findIndex(node => node.value === 'b');
const i2 = list.findIndex(node => node.previous && node.previous.value === 'b');
const i3 = list.findIndex(node => node.value === 'x');
/*
i0 === 0
i1 === 1
i2 === 2
i3 === -1
*/
```
#### get(position: number): ListNode\<T\> | undefined
Finds and returns the node with specific position in the list:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
const found = list.get(1);
/*
found.value === "b"
found.previous.value === "a"
found.next.value === "c"
*/
```
#### indexOf(value: T, compareFn = compare): number
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');
// "a" <-> "b" <-> "b" <-> "c"
const i0 = list.indexOf('a');
const i1 = list.indexOf('b');
const i2 = list.indexOf('c');
const i3 = list.indexOf('x');
/*
i0 === 0
i1 === 1
i2 === 3
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 });
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
const i0 = indexOf({ x: 1 }, (v1, v2) => v1.x === v2.x);
const i1 = indexOf({ x: 2 }, (v1, v2) => v1.x === v2.x);
const i2 = indexOf({ x: 3 }, (v1, v2) => v1.x === v2.x);
const i3 = indexOf({ x: 0 }, (v1, v2) => v1.x === v2.x);
const i4 = indexOf({ x: 4 }, (v1, v2) => v1.x === v2.x);
/*
i0 === 0
i1 === 2
i2 === 4
i3 === 1
i4 === -1
*/
```
> The default compare function checks deep equality, so you will rarely need to pass that parameter.
### How to Check All Nodes
There are a few ways to iterate over or display a linked list.
#### forEach(callback: ListIteratorFunction\<T\>): void
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');
// "a" <-> "b" <-> "c"
list.forEach((node, index) => console.log(node.value + index));
// 'a0'
// 'b1'
// 'c2'
```
#### \*\[Symbol.iterator\]\(\)
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');
// "a" <-> "b" <-> "c"
for(const node of list) {
console.log(node.value);
}
// 'a'
// 'b'
// 'c'
```
#### toArray(): T[]
Converts a linked list to an array:
```js
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
const arr = list.toArray();
/*
arr === ['a', 'b', 'c']
*/
```
#### 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' });
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
const str = list.toString();
/*
str === '"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}'
*/
```

@ -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.md#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.md)

@ -112,3 +112,6 @@ class DemoComponent {
}
```
## What's Next?
* [Linked List (Doubly)](./Linked-List.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

@ -30,7 +30,7 @@ Basic usage:
## Demo
See the [cards demo page](https://bootstrap-taghelpers.abp.io/Components/Cards) to see it in action.
See the [tabs demo page](https://bootstrap-taghelpers.abp.io/Components/Tabs) to see it in action.
## abp-tab Attributes

@ -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"
@ -339,6 +344,10 @@
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"
},
{
"text": "Linked List (Doubly)",
"path": "UI/Angular/Linked-List.md"
}
]
}

@ -89,20 +89,23 @@ public static class IdentityDbContextModelBuilderExtensions
builder.Entity<IdentityUser>(b =>
{
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity
});
builder.Entity<IdentityUserClaim>(b =>
{
b.ToTable(options.TablePrefix + "UserClaims", options.Schema);
b.ConfigureByConvention();
//code omitted for brevity
});
});
//code omitted for brevity
}
}
````
* **推荐** 为每个Enttiy映射调用 `b.ConfigureByConvention();`(如上所示).
* **推荐** 通过继承 `ModelBuilderConfigurationOptions` 来创建 **configuration Options** 类. 例如:
````C#

@ -25,7 +25,35 @@ return user.GetProperty<string>("Title");
参阅[实体文档](Entities.md)了解更多关于额外系统.
> 可以基于额外的属性执行**业务逻辑**. 你可以**override**服务方法获取或设置值. 重写服务在下面进行讨论.
> 可以基于额外的属性执行**业务逻辑**. 你可以[重写服务方法](Customizing-Application-Modules-Overriding-Services.md). 然后获取或设置如上所示的值.
## 实体扩展 (EF Core)
如上所述,实体所有的额外属性都作为单个JSON对象存储在数据库表中. 它不适用复杂的场景,特别是在你需要的时候.
* 使用额外属性创建**索引**和**外键**.
* 使用额外属性编写**SQL**或**LINQ**(例如根据属性值搜索).
* 创建你**自己的实体**映射到相同的表,但在实体中定义一个额外属性做为 **常规属性**(参阅 [EF Core迁移文档](Entity-Framework-Core-Migrations.md)了解更多).
为了解决上面的问题,用于EF Core的ABP框架实体扩展系统允许你使用上面定义相同的额外属性API,但将所需的属性存储在单独的数据库表字段中.
假设你想要添加 `SocialSecurityNumber` 到[身份模块](Modules/Identity.md)的 `IdentityUser` 实体. 你可以使用 `EntityExtensionManager` 静态类:
````csharp
EntityExtensionManager.AddProperty<IdentityUser, string>(
"SocialSecurityNumber",
b => { b.HasMaxLength(32); }
);
````
* 你提供了 `IdentityUser` 作为实体名(泛型参数), `string` 做为新属性的类型, `SocialSecurityNumber` 做为属性名(也是数据库表的字段名).
* 你还需要提供一个使用[EF Core Fluent API](https://docs.microsoft.com/en-us/ef/core/modeling/entity-properties)定义数据库映射属性的操作.
> 必须在使用相关的 `DbContext` 之前执行此代码. 应用程序启动模板定义了一个名为 `YourProjectNameEntityExtensions` 的静态类. 你可以在此类中定义扩展确保在正确的时间执行它. 否则你需要自己处理.
定义实体扩展后你需要使用EF Core的[Add-Migration](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#add-migration)和[Update-Database](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell#update-database)命令来创建code first迁移类并更新数据库.
然后你可以使用上一部分中定义的相同额外属性系统来操纵实体上的属性.
## 创建新实体映射到同一个数据库表/Collection

@ -365,14 +365,17 @@ public static class IdentityUserExtensions
存储字典的方式取决于你使用的数据库提供程序.
* 对于 [Entity Framework Core](Entity-Framework-Core.md), 它以 `JSON` 字符串形式存储在 `ExtraProperties` 字段中. 序列化到 `JSON` 和反序列化到 `JSON` 由ABP使用EF Core的[值转换](https://docs.microsoft.com/zh-cn/ef/core/modeling/value-conversions)系统自动完成.
* 对于 [Entity Framework Core](Entity-Framework-Core.md),这是两种类型的配置;
* 默认它以 `JSON` 字符串形式存储在 `ExtraProperties` 字段中. 序列化到 `JSON` 和反序列化到 `JSON` 由ABP使用EF Core的[值转换](https://docs.microsoft.com/zh-cn/ef/core/modeling/value-conversions)系统自动完成.
* 如果需要,你可以使用 `EntityExtensionManager` 为所需的额外属性定义一个单独的数据库字段. 那些使用 `EntityExtensionManager` 配置的属性继续使用单个 `JSON` 字段. 当你使用预构建的[应用模块](Modules/Index.md)并且想要[扩展模块的实体](Customizing-Application-Modules-Extending-Entities.md). 参阅[EF Core迁移文档](Entity-Framework-Core.md)了解如何使用 `EntityExtensionManager`.
* 对于 [MongoDB](MongoDB.md), 它以 **常规字段** 存储, 因为 MongoDB 天生支持这种 [额外](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) 系统.
### 讨论额外的属性
如果你使用**可重复使用的模块**,其中定义了一个实体,你想使用简单的方式get/set此实体相关的一些数据,那么额外的属性系统是非常有用的. 通常 **不需要** 为自己的实体使用这个系统,是因为它有以下缺点:
如果你使用**可重复使用的模块**,其中定义了一个实体,你想使用简单的方式get/set此实体相关的一些数据,那么额外的属性系统是非常有用的.
你通常 **不需要** 为自己的实体使用这个系统,是因为它有以下缺点:
* 它不是**完全类型安全的**.
* 它不是**完全类型安全的**,因为它使用字符串用作属性名称.
* 这些属性**不容易[自动映射](Object-To-Object-Mapping.md)到其他对象**.
* 它**不会**为EF Core在数据库表中**创建字段**,因此在数据库中针对这个字段创建索引或搜索/排序并不容易.

@ -95,7 +95,7 @@ Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids";
这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ).
**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在自定义[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体:
**每个模块都使用自己的 `DbContext` 类**来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体:
````csharp
[ConnectionStringName("Default")]
@ -119,15 +119,15 @@ public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
builder.Entity<AppUser>(b =>
{
//Sharing the same table "AbpUsers" with the IdentityUser
b.ToTable("AbpUsers");
//Sharing the same Users table with the IdentityUser
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users");
//Configure base properties
b.ConfigureByConvention();
b.ConfigureAbpUser();
//Moved customization of the "AbpUsers" table to an extension method
b.ConfigureCustomUserProperties();
/* Configure mappings for your additional properties
* Also see the MyProjectNameEntityExtensions class
*/
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
@ -190,12 +190,6 @@ public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbCo
builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();
}
@ -276,7 +270,7 @@ public class BackgroundJobsDbContext
您可能想在应用程序中**重用依赖模块的表**. 在这种情况下你有两个选择:
1. 你可以**直接使用模块定义的实体**.
1. 你可以**直接使用模块定义的实体**(你仍然可以在某种程度上[扩展实体](Customizing-Application-Modules-Extending-Entities.md)).
2. 你可以**创建一个新的实体**映射到同一个数据库表。
###### 使用由模块定义的实体
@ -379,10 +373,8 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<AppRole>(b =>
{
b.ToTable("AbpRoles");
b.ConfigureByConvention();
b.ConfigureCustomRoleProperties();
b.Property(x => x.Title).HasMaxLength(128);
});
...
@ -399,68 +391,42 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<AppRole>(b =>
{
b.ToTable("AbpRoles");
b.ConfigureByConvention();
b.ConfigureCustomRoleProperties();
b.Property(x => x.Title).HasMaxLength(128);
});
````
* 它映射到 `AbpRoles` 表,与 `IdentityRole` 实体共享.
* `ConfigureByConvention()` 配置了标准/基本属性(像`TenantId`),建议总是调用它.
`ConfigureCustomRoleProperties()` 还不存在. 在 `BookStoreDbContextModelCreatingExtensions` 类中定义它 (在 `.EntityFrameworkCore` 项目的 `DbContext` 附近):
你已经为你的 `DbContext` 配置自定义属性,该属性在应用程序运行时使用.
与其直接更改 `MigrationsDbContext`,我们应该使用ABP框架的实体扩展系统,在解决方案的 `.EntityFrameworkCore` 项目中找到 `YourProjectNameEntityExtensions` 类(本示例中是 `BookStoreEntityExtensions`)并且进行以下更改:
````csharp
public static void ConfigureCustomRoleProperties<TRole>(this EntityTypeBuilder<TRole> b)
where TRole : class, IEntity<Guid>
public static class MyProjectNameEntityExtensions
{
b.Property<string>(nameof(AppRole.Title)).HasMaxLength(128);
}
````
* 这个方法只定义实体的**自定义属性**.
* 遗憾的是,我们不能在这里充分的利用**类型安全**(通过引用`AppRole`实体). 我们能做的最好就是使用 `Title` 名称做为类型安全。
你已经为运行应用程序使用的 `DbContext` 配置了自定义属性. 我们还需要配置 `MigrationsDbContext`.
打开`MigrationsDbContext`(本例是 `BookStoreMigrationsDbContext`)进行以下更改:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
...
/* Configure customizations for entities from the modules included */
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
//CONFIGURE THE CUSTOM ROLE PROPERTIES
builder.Entity<IdentityRole>(b =>
public static void Configure()
{
b.ConfigureCustomRoleProperties();
});
...
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();
OneTimeRunner.Run(() =>
{
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
});
}
}
````
只增加下面几行:
> 我们建议使用 `nameof(AppRole.Title)` 而不是硬编码 "Title" 字符串
````csharp
builder.Entity<IdentityRole>(b =>
{
b.ConfigureCustomRoleProperties();
});
````
`EntityExtensionManager` 用于添加属性到现有的实体. 由于 `EntityExtensionManager` 是静态的,因此应调用一次. `OneTimeRunner` 是ABP框架定义简单的工具类.
通过这种方式,我们重用了用于为角色配置自定义属性映射的扩展方法. 但是对 `IdentityRole` 实体进行了相同的自定义.
参阅[EF Core集成文档](Entity-Framework-Core.md)了解更多关于实体扩展系统.
我们在两个类中都重复了类似的数据库映射代码,例如 `HasMaxLength(128)`.
现在你可以在包管理控制台(记得选择 `.EntityFrameworkCore.DbMigrations` 做为PMC的默认项目并将 `.Web` 项目设置为启动项目)使用标准的 `Add-Migration` 命令添加一个新的EF Core数据库迁移.
@ -540,7 +506,7 @@ public class AppRoleAppService : ApplicationService, IAppRoleAppService
###### 使用ExtraProperties
所有从 `AggregateRoot` 派生的实体都可以在 `ExtraProperties` 属性中存储键值对, 它是 `Dictionary<string, object>` 类型在数据库中被序列化为JSON. 所以你可以在字典中添加值用于查询,无需更改实体.
所有从 `AggregateRoot` 派生的实体都可以在 `ExtraProperties` 属性(因为它们都实现了 `IHasExtraProperties` 接口)中存储键值对, 它是 `Dictionary<string, object>` 类型在数据库中被序列化为JSON. 所以你可以在字典中添加值用于查询,无需更改实体.
例如你可以将查询属性 `Title` 存储在 `IdentityRole` 中,而不是创建一个新的实体.
例:
@ -558,16 +524,13 @@ public class IdentityRoleExtendingService : ITransientDependency
public async Task<string> GetTitleAsync(Guid id)
{
var role = await _identityRoleRepository.GetAsync(id);
return role.GetProperty<string>("Title");
}
public async Task SetTitleAsync(Guid id, string newTitle)
{
var role = await _identityRoleRepository.GetAsync(id);
role.SetProperty("Title", newTitle);
await _identityRoleRepository.UpdateAsync(role);
}
}
@ -580,6 +543,12 @@ public class IdentityRoleExtendingService : ITransientDependency
* 所有的额外属性都存储在数据库中的一个**JSON对象**,它们不是作为表的字段存储,与简单的表字段相比创建索引和针对此属性使用SQL查询将更加困难.
* 属性名称是字符串,他们**不是类型安全的**. 建议这些类型的属性定义常量,以防止拼写错误.
###### 使用实体扩展系统
实体扩展系统解决了额外属性主要的问题: 它可以将额外属性做为**标准表字段**存储到数据库.
你需要做的就是如上所诉使用 `EntityExtensionManager` 定义额外属性, 然后你就可以使得 `GetProperty``SetProperty` 方法对实体的属性进行get/set,但是这时它存储在数据库表的单独字段中.
###### 创建新表
你可以创建**自己的表**来存储属性,而不是创建新实体并映射到同一表. 你通常复制原始实体的一些值. 例如可以将 `Name` 字段添加到你自己的表中,它是原表中 `Name` 字段的副本.

@ -58,6 +58,53 @@ namespace MyCompany.MyProject
}
````
### 关于EF Core Fluent Mapping
[应用程序启动模板](Startup-Templates/Application.md)已配置使用[EF Core fluent configuration API](https://docs.microsoft.com/en-us/ef/core/modeling/)映射你的实体到数据库表.
你依然为你的实体属性使用**data annotation attributes**(像`[Required]`),而ABP文档通常遵循**fluent mapping API** approach方法. 如何使用取决与你.
ABP框架有一些**实体基类**和**约定**(参阅[实体文档](Entities.md))提供了一些有用的扩展方法来配置从基本实体类继承的属性.
#### ConfigureByConvention 方法
`ConfigureByConvention()` 是主要的扩展方法,它对你的实体**配置所有的基本属性**和约定. 所以在你的流利映射代码中为你所有的实体调用这个方法是 **最佳实践**,
**示例**: 假设你有一个直接继承 `AggregateRoot<Guid>` 基类的 `Book` 实体:
````csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
}
````
你可以在你的 `DbContext` 重写 `OnModelCreating` 方法并且做以下配置:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
//Always call the base method
base.OnModelCreating(builder);
builder.Entity<Book>(b =>
{
b.ToTable("Books");
//Configure the base properties
b.ConfigureByConvention();
//Configure other properties (if you are using the fluent API)
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
````
* 这里调用了 `b.ConfigureByConvention()` 它对于**配置基本属性**非常重要.
* 你可以在这里配置 `Name` 属性或者使用**data annotation attributes**(参阅[EF Core 文档](https://docs.microsoft.com/zh-cn/ef/core/modeling/entity-properties)).
> 尽管有许多扩展方法可以配置基本属性,但如果需要 `ConfigureByConvention()` 内部会调用它们. 因此仅调用它就足够了.
### 配置连接字符串选择
如果你的应用程序有多个数据库,你可以使用 `connectionStringName]` Attribute为你的DbContext配置连接字符串名称.
@ -225,7 +272,7 @@ public override async Task DeleteAsync(
}
````
### 访问 EF Core API
## 访问 EF Core API
大多数情况下应该隐藏仓储后面的EF Core API(这也是仓储的设计目地). 但是如果想要通过仓储访问DbContext实现,则可以使用`GetDbContext()`或`GetDbSet()`扩展方法. 例:
@ -251,9 +298,59 @@ public class BookService
> 要点: 你必须在使用`DbContext`的项目里引用`Volo.Abp.EntityFrameworkCore`包. 这会破坏封装,但在这种情况下,这就是你需要的.
### 高级主题
## Extra Properties & Entity Extension Manager
额外属性系统允许你为实现了 `IHasExtraProperties` 的实体set/get动态属性. 当你想将自定义属性添加到[应用程序模块](Modules/Index.md)中定义的实体时,它特别有用.
默认,实体的所有额外属性存储在数据库的一个 `JSON` 对象中. 实体扩展系统允许你存储额外属性在数据库的单独字段中.
#### 设置默认仓储类
有关额外属性和实体扩展系统的更多信息,请参阅下列文档:
* [自定义应用模块: 扩展实体](Customizing-Application-Modules-Extending-Entities.md)
* [实体](Entities.md)
本节只解释了 `EntityExtensionManager` 及其用法.
### AddProperty 方法
`EntityExtensionManager``AddProperty` 方法允许你实体定义附加的属性.
**示例**: 添加 `Title` 属性 (数据库字段)到 `IdentityRole` 实体:
````csharp
EntityExtensionManager.AddProperty<IdentityRole, string>(
"Title",
b => { b.HasMaxLength(128); }
);
````
如果相关模块已实现此功能(通过使用下面说明的 `ConfigureExtensions`)则将新属性添加到模型中. 然后你需要运行标准的 `Add-Migration``Update-Database` 命令更新数据库以添加新字段.
>`AddProperty` 方法必须在使用相关的 `DbContext` 之前调用,它是一个静态方法. 最好的方法是尽早的应用程序中使用它. 应用程序启动模板含有 `YourProjectNameEntityExtensions` 类,可以在放心的在此类中使用此方法.
### ConfigureExtensions
如果你正在开发一个可重用使用的模块,并允许应用程序开发人员将属性添加到你的实体,你可以在实体映射使用 `ConfigureExtensions` 扩展方法:
````csharp
builder.Entity<YourEntity>(b =>
{
b.ConfigureExtensions();
//...
});
````
如果你调用 `ConfigureByConvention()` 扩展方法(在此示例中 `b.ConfigureByConvention`),ABP框架内部会调用 `ConfigureExtensions` 方法. 使用 `ConfigureByConvention` 方法是**最佳实践**,因为它还按照约定配置基本属性的数据库映射.
参阅上面提到的 "*ConfigureByConvention 方法*" 了解更多信息.
### GetPropertyNames
`EntityExtensionManager.GetPropertyNames` 静态方法可以用作为此实体定义的扩展属性的名称. 应用程序代码通常不需要,但是ABP框架在内部使用它.
## 高级主题
### 设置默认仓储类
默认的通用仓储的默认实现是`EfCoreRepository`类,你可以创建自己的实现,并将其做为默认实现
@ -343,3 +440,7 @@ context.Services.AddAbpDbContext<OtherDbContext>(options =>
````
在这个例子中,`OtherDbContext`实现了`IBookStoreDbContext`. 此功能允许你在开发时使用多个DbContext(每个模块一个),但在运行时可以使用单个DbContext(实现所有DbContext的所有接口).
## 另请参阅
* [实体](Entities.md)

@ -28,7 +28,7 @@
## Demo
参阅[卡面demo页面](https://bootstrap-taghelpers.abp.io/Components/Cards)查看示例.
参阅[标签页demo页面](https://bootstrap-taghelpers.abp.io/Components/Tabs)查看示例.
## abp-tab Attributes

@ -57,9 +57,19 @@ namespace Volo.Abp.Cli.Commands
var uiFramework = GetUiFramework(commandLineArgs);
WebClient client = new WebClient();
string json = client.DownloadString(apiUrl);
//var sr = File.OpenText("api-definition.json");
//var json = sr.ReadToEnd();
string json = "";
try
{
json = client.DownloadString(apiUrl);
}
catch (Exception ex)
{
throw new CliUsageException(
"Cannot connect to the host {" + apiUrl + "}! Check that the host is up and running." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
Logger.LogInformation("Downloading api definition...");
Logger.LogInformation("Api Url: " + apiUrl);
@ -107,7 +117,7 @@ namespace Volo.Abp.Cli.Commands
serviceFileText.AppendLine("");
serviceFileText.AppendLine("@Injectable({providedIn: 'root'})");
serviceFileText.AppendLine("export class [controllerName]Service {");
serviceFileText.AppendLine(" apiName = '"+ apiName + "';");
serviceFileText.AppendLine(" apiName = '" + apiName + "';");
serviceFileText.AppendLine("");
serviceFileText.AppendLine(" constructor(private restService: RestService) {}");
serviceFileText.AppendLine("");
@ -125,7 +135,7 @@ namespace Volo.Abp.Cli.Commands
actionName = (char.ToLower(actionName[0]) + actionName.Substring(1)).Replace("Async", "").Replace("Controller", "");
var returnValueType = (string)action["returnValue"]["type"];
var returnValueType = (string)action["returnValue"]["type"];
var parameters = action["parameters"];
var parametersText = new StringBuilder();
@ -140,7 +150,7 @@ namespace Volo.Abp.Cli.Commands
var bindingSourceId = (string)parameter["bindingSourceId"];
bindingSourceId = char.ToLower(bindingSourceId[0]) + bindingSourceId.Substring(1);
var name = (string)parameter["name"];
var name = (string)parameter["name"];
var typeSimple = (string)parameter["typeSimple"];
var typeArray = ((string)parameter["type"]).Split(".");
var type = (typeArray[typeArray.Length - 1]).TrimEnd('>');
@ -201,7 +211,8 @@ namespace Volo.Abp.Cli.Commands
{
secondTypeList.Add(type);
}
else {
else
{
firstTypeList.Add(type);
}
break;
@ -217,7 +228,7 @@ namespace Volo.Abp.Cli.Commands
var parameterItemModelPath = $"src/app/{rootPath}/shared/models/{parameterItemModelName}";
if (parameterItem.BindingSourceId == "body" && !File.Exists(parameterItemModelPath))
{
parameterItem.Type = "any";
parameterItem.Type = "any";
}
parametersIndex++;
@ -248,7 +259,7 @@ namespace Volo.Abp.Cli.Commands
var firstType = firstTypeArray[firstTypeArray.Length - 1];
var secondTypeArray = returnValueType.Split("<")[1].Split(".");
var secondType = secondTypeArray[secondTypeArray.Length - 1].TrimEnd('>');
var secondType = secondTypeArray[secondTypeArray.Length - 1].TrimEnd('>');
var secondTypeModelName = secondType.PascalToKebabCase() + ".ts";
var secondTypeModelPath = $"src/app/{rootPath}/shared/models/{secondTypeModelName}";
@ -405,7 +416,7 @@ namespace Volo.Abp.Cli.Commands
}
private static string CreateType(JObject data, string returnValueType, string rootPath, List<string> modelIndexList)
{
{
var type = data["types"][returnValueType];
if (type == null)
@ -435,9 +446,9 @@ namespace Volo.Abp.Cli.Commands
}
var typeModelName = typeName.Replace("<", "").Replace(">", "").Replace("?","").PascalToKebabCase() + ".ts";
var typeModelName = typeName.Replace("<", "").Replace(">", "").Replace("?", "").PascalToKebabCase() + ".ts";
var path = $"src/app/{rootPath}/shared/models/{typeModelName}";
var path = $"src/app/{rootPath}/shared/models/{typeModelName}";
var modelFileText = new StringBuilder();
@ -474,7 +485,7 @@ namespace Volo.Abp.Cli.Commands
if (!string.IsNullOrWhiteSpace(modelIndex))
{
modelIndexList.Add(modelIndex);
}
}
}
}
@ -501,7 +512,7 @@ namespace Volo.Abp.Cli.Commands
{
var propertyName = (string)property["name"];
propertyName = (char.ToLower(propertyName[0]) + propertyName.Substring(1));
var typeSimple = (string)property["typeSimple"];
var typeSimple = (string)property["typeSimple"];
var modelIndex = CreateType(data, (string)property["type"], rootPath, modelIndexList);
@ -536,11 +547,11 @@ namespace Volo.Abp.Cli.Commands
)
{
var typeSimpleModelName = typeSimple.PascalToKebabCase() + ".ts";
var modelPath = $"src/app/{rootPath}/shared/models/{typeSimpleModelName}";
var modelPath = $"src/app/{rootPath}/shared/models/{typeSimpleModelName}";
if (!File.Exists(modelPath))
{
typeSimple = "any" + (typeSimple.Contains("[]") ? "[]" : "");
}
}
}
if (propertyList.Any(p => p.Key == baseTypeName && p.Value.Any(q => q.Key == propertyName && q.Value == typeSimple)))

@ -2,11 +2,16 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.IO;
@ -48,16 +53,33 @@ namespace Volo.Abp.Cli.ProjectBuilding
string templateSource = null)
{
DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache);
var latestVersion = await GetLatestSourceCodeVersionAsync(name, type);
if (version == null)
{
if (latestVersion == null)
{
Logger.LogWarning("The remote service is currently unavailable, please specify the version.");
Logger.LogWarning(string.Empty);
Logger.LogWarning("Find the following template in your cache directory: ");
Logger.LogWarning("\t Template Name\tVersion");
var templateList = GetLocalTemplates();
foreach (var cacheFile in templateList)
{
Logger.LogWarning($"\t {cacheFile.TemplateName}\t\t{cacheFile.Version}");
}
Logger.LogWarning(string.Empty);
throw new CliUsageException("Use command: abp new Acme.BookStore -v version");
}
version = latestVersion;
}
var nugetVersion = (await GetTemplateNugetVersionAsync(name, type, version)) ?? version;
DirectoryHelper.CreateIfNotExists(CliPaths.TemplateCache);
if (!string.IsNullOrWhiteSpace(templateSource) && !IsNetworkSource(templateSource))
{
Logger.LogInformation("Using local " + type + ": " + name + ", version: " + version);
@ -122,7 +144,7 @@ namespace Volo.Abp.Cli.ProjectBuilding
catch (Exception ex)
{
Console.WriteLine("Error occured while getting the latest version from {0} : {1}", url, ex.Message);
throw;
return null;
}
}
@ -195,11 +217,30 @@ namespace Volo.Abp.Cli.ProjectBuilding
}
}
private static bool IsNetworkSource(string source)
private bool IsNetworkSource(string source)
{
return source.ToLower().StartsWith("http");
}
private List<(string TemplateName, string Version)> GetLocalTemplates()
{
var templateList = new List<(string TemplateName, string Version)>();
var stringBuilder = new StringBuilder();
foreach (var cacheFile in Directory.GetFiles(CliPaths.TemplateCache))
{
stringBuilder.AppendLine(cacheFile);
}
var matches = Regex.Matches(stringBuilder.ToString(),$"({AppTemplate.TemplateName}|{AppProTemplate.TemplateName}|{ModuleTemplate.TemplateName}|{ModuleProTemplate.TemplateName})-(.+).zip");
foreach (Match match in matches)
{
templateList.Add((match.Groups[1].Value, match.Groups[2].Value));
}
return templateList;
}
public class SourceCodeDownloadInputDto
{
public string Name { get; set; }

@ -1,4 +1,5 @@
using System.Net.Http;
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -32,27 +33,36 @@ namespace Volo.Abp.Cli.ProjectBuilding.Analyticses
public async Task CollectAsync(CliAnalyticsCollectInputDto input)
{
var postData = _jsonSerializer.Serialize(input);
using (var client = new CliHttpClient())
var url = $"{CliUrls.WwwAbpIo}api/clianalytics/collect";
try
{
var responseMessage = await client.PostAsync(
$"{CliUrls.WwwAbpIo}api/clianalytics/collect",
new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json),
_cancellationTokenProvider.Token
);
if (!responseMessage.IsSuccessStatusCode)
using (var client = new CliHttpClient())
{
var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. ";
var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage);
var responseMessage = await client.PostAsync(
url,
new StringContent(postData, Encoding.UTF8, MimeTypes.Application.Json),
_cancellationTokenProvider.Token
);
if (remoteServiceErrorMessage != null)
if (!responseMessage.IsSuccessStatusCode)
{
exceptionMessage += remoteServiceErrorMessage;
}
var exceptionMessage = "Remote server returns '" + (int)responseMessage.StatusCode + "-" + responseMessage.ReasonPhrase + "'. ";
var remoteServiceErrorMessage = await _remoteServiceExceptionHandler.GetAbpRemoteServiceErrorAsync(responseMessage);
if (remoteServiceErrorMessage != null)
{
exceptionMessage += remoteServiceErrorMessage;
}
_logger.LogInformation(exceptionMessage);
_logger.LogInformation(exceptionMessage);
}
}
}
catch (Exception ex)
{
// ignored
}
}
}
}

@ -18,6 +18,7 @@ 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;
@ -155,6 +156,50 @@ namespace Volo.Abp.EntityFrameworkCore
ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
ChangeTracker.Tracked += ChangeTracker_Tracked;
}
protected virtual void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e)
{
FillExtraPropertiesForTrackedEntities(e);
}
protected virtual void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e)
{
var entityType = e.Entry.Metadata.ClrType;
if (entityType == null)
{
return;
}
if (!(e.Entry.Entity is IHasExtraProperties entity))
{
return;
}
if (!e.FromQuery)
{
return;
}
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
foreach (var propertyName in propertyNames)
{
/* 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.
* In this way, it prevents to delete old value in the JSON and
* updates the field on the next save!
*/
var currentValue = e.Entry.CurrentValues[propertyName];
if (currentValue != null)
{
entity.SetProperty(propertyName, currentValue);
}
}
}
protected virtual EntityChangeReport ApplyAbpConcepts()
@ -184,9 +229,37 @@ namespace Volo.Abp.EntityFrameworkCore
break;
}
HandleExtraPropertiesOnSave(entry);
AddDomainEvents(changeReport, entry.Entity);
}
protected virtual void HandleExtraPropertiesOnSave(EntityEntry entry)
{
if (entry.State.IsIn(EntityState.Deleted, EntityState.Unchanged))
{
return;
}
var entityType = entry.Metadata.ClrType;
if (entityType == null)
{
return;
}
if (!(entry.Entity is IHasExtraProperties entity))
{
return;
}
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
foreach (var propertyName in propertyNames)
{
entry.Property(propertyName).CurrentValue = entity.GetProperty(propertyName);
}
}
protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport)
{
CheckAndSetId(entry);

@ -0,0 +1,14 @@
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>();
}
}
}

@ -0,0 +1,133 @@
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();
}
}
}

@ -0,0 +1,17 @@
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,6 +5,7 @@ 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;
@ -25,7 +26,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
b.TryConfigureCreationTime();
b.TryConfigureLastModificationTime();
b.TryConfigureModificationAudited();
b.TryConfigureMultiTenant();
b.TryConfigureMultiTenant();
}
public static void ConfigureConcurrencyStamp<T>(this EntityTypeBuilder<T> b)
@ -58,8 +59,10 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
{
b.Property<Dictionary<string, object>>(nameof(IHasExtraProperties.ExtraProperties))
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(new AbpJsonValueConverter<Dictionary<string, object>>())
.HasConversion(new ExtraPropertiesValueConverter(b.Metadata.ClrType))
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, object>());
EntityExtensionManager.ConfigureExtensions(b.Metadata.ClrType, b);
}
}
@ -276,7 +279,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
}
public static void ConfigureFullAuditedAggregateRoot<T>(this EntityTypeBuilder<T> b)
where T : class
where T : class
{
b.As<EntityTypeBuilder>().TryConfigureFullAudited();
b.As<EntityTypeBuilder>().TryConfigureExtraProperties();

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
using Volo.Abp.Data;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
@ -7,9 +8,20 @@ namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
public AbpJsonValueConverter()
: base(
d => JsonConvert.SerializeObject(d, Formatting.None),
s => JsonConvert.DeserializeObject<TPropertyType>(s))
d => SerializeObject(d),
s => DeserializeObject(s))
{
}
private static string SerializeObject(TPropertyType d)
{
return JsonConvert.SerializeObject(d, Formatting.None);
}
private static TPropertyType DeserializeObject(string s)
{
return JsonConvert.DeserializeObject<TPropertyType>(s);
}
}
}

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
using Volo.Abp.EntityFrameworkCore.Extensions;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
public class ExtraPropertiesValueConverter : ValueConverter<Dictionary<string, object>, string>
{
public ExtraPropertiesValueConverter(Type entityType)
: base(
d => SerializeObject(d, entityType),
s => DeserializeObject(s))
{
}
private static string SerializeObject(Dictionary<string, object> extraProperties, Type entityType)
{
var copyDictionary = new Dictionary<string, object>(extraProperties);
if (entityType != null)
{
var propertyNames = EntityExtensionManager.GetPropertyNames(entityType);
foreach (var propertyName in propertyNames)
{
copyDictionary.Remove(propertyName);
}
}
return JsonConvert.SerializeObject(copyDictionary, Formatting.None);
}
private static Dictionary<string, object> DeserializeObject(string extraPropertiesAsJson)
{
return JsonConvert.DeserializeObject<Dictionary<string, object>>(extraPropertiesAsJson);
}
}
}

@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Autofac;
using Volo.Abp.EntityFrameworkCore.Domain;
using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext;
using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext;
using Volo.Abp.Modularity;
@ -21,6 +22,11 @@ namespace Volo.Abp.EntityFrameworkCore
[DependsOn(typeof(AbpEfCoreTestSecondContextModule))]
public class AbpEntityFrameworkCoreTestModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
TestEntityExtensionConfigurator.Configure();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<TestAppDbContext>(options =>

@ -1,9 +1,32 @@
using Volo.Abp.TestApp.Testing;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Data;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.EntityFrameworkCore.Domain
{
public class ExtraProperties_Tests : ExtraProperties_Tests<AbpEntityFrameworkCoreTestModule>
{
[Fact]
public async Task Should_Get_An_Extra_Property_Configured_As_Extension()
{
var london = await CityRepository.FindByNameAsync("London");
london.HasProperty("PhoneCode").ShouldBeTrue();
london.GetProperty<string>("PhoneCode").ShouldBe("42");
}
[Fact]
public async Task Should_Update_An_Existing_Extra_Property_Configured_As_Extension()
{
var london = await CityRepository.FindByNameAsync("London");
london.GetProperty<string>("PhoneCode").ShouldBe("42");
london.ExtraProperties["PhoneCode"] = "53";
await CityRepository.UpdateAsync(london);
var london2 = await CityRepository.FindByNameAsync("London");
london2.GetProperty<string>("PhoneCode").ShouldBe("53");
}
}
}

@ -0,0 +1,22 @@
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Threading;
namespace Volo.Abp.EntityFrameworkCore.Domain
{
public static class TestEntityExtensionConfigurator
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public static void Configure()
{
OneTimeRunner.Run(() =>
{
EntityExtensionManager.AddProperty<City, string>(
"PhoneCode",
p => p.HasMaxLength(8)
);
});
}
}
}

@ -1,4 +1,5 @@
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;
@ -36,6 +37,8 @@ namespace Volo.Abp.EntityFrameworkCore
modelBuilder.Entity<City>(b =>
{
//b.ConfigureExtensions();
b.OwnsMany(c => c.Districts, d =>
{
d.WithOwner().HasForeignKey(x => x.CityId);

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

@ -21,7 +21,7 @@ namespace Volo.Abp.TestApp
private readonly IRepository<EntityWithIntPk, int> _entityWithIntPksRepository;
public TestDataBuilder(
IBasicRepository<Person, Guid> personRepository,
IBasicRepository<Person, Guid> personRepository,
ICityRepository cityRepository,
IRepository<EntityWithIntPk, int> entityWithIntPksRepository)
{
@ -46,7 +46,7 @@ namespace Volo.Abp.TestApp
await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Tokyo"));
await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Madrid"));
await _cityRepository.InsertAsync(new City(LondonCityId, "London") { ExtraProperties = { { "Population", 10_470_000 } } });
await _cityRepository.InsertAsync(new City(LondonCityId, "London") { ExtraProperties = { { "Population", 10_470_000 }, { "PhoneCode", "42" } } });
await _cityRepository.InsertAsync(istanbul);
await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Paris"));
await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Washington"));

@ -41,6 +41,7 @@ namespace Volo.Abp.TestApp.Testing
public async Task Should_Update_An_Existing_Extra_Property()
{
var london = await CityRepository.FindByNameAsync("London");
london.GetProperty<int>("Population").ShouldBe(10_470_000);
london.ExtraProperties["Population"] = 11_000_042;
await CityRepository.UpdateAsync(london);

@ -25,7 +25,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "AuditLogs", options.Schema);
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(x => x.ApplicationName).HasMaxLength(AuditLogConsts.MaxApplicationNameLength).HasColumnName(nameof(AuditLog.ApplicationName));
b.Property(x => x.ClientIpAddress).HasMaxLength(AuditLogConsts.MaxClientIpAddressLength).HasColumnName(nameof(AuditLog.ClientIpAddress));
@ -56,7 +56,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "AuditLogActions", options.Schema);
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(x => x.AuditLogId).HasColumnName(nameof(AuditLogAction.AuditLogId));
b.Property(x => x.ServiceName).HasMaxLength(AuditLogActionConsts.MaxServiceNameLength).HasColumnName(nameof(AuditLogAction.ServiceName));
@ -73,7 +73,7 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "EntityChanges", options.Schema);
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(x => x.EntityTypeFullName).HasMaxLength(EntityChangeConsts.MaxEntityTypeFullNameLength).IsRequired().HasColumnName(nameof(EntityChange.EntityTypeFullName));
b.Property(x => x.EntityId).HasMaxLength(EntityChangeConsts.MaxEntityIdLength).IsRequired().HasColumnName(nameof(EntityChange.EntityId));
@ -92,6 +92,8 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "EntityPropertyChanges", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.NewValue).HasMaxLength(EntityPropertyChangeConsts.MaxNewValueLength).HasColumnName(nameof(EntityPropertyChange.NewValue));
b.Property(x => x.PropertyName).HasMaxLength(EntityPropertyChangeConsts.MaxPropertyNameLength).IsRequired().HasColumnName(nameof(EntityPropertyChange.PropertyName));
b.Property(x => x.PropertyTypeFullName).HasMaxLength(EntityPropertyChangeConsts.MaxPropertyTypeFullNameLength).IsRequired().HasColumnName(nameof(EntityPropertyChange.PropertyTypeFullName));

@ -23,8 +23,7 @@ namespace Volo.Abp.BackgroundJobs.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema);
b.ConfigureCreationTime();
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(x => x.JobName).IsRequired().HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength);
b.Property(x => x.JobArgs).IsRequired().HasMaxLength(BackgroundJobRecordConsts.MaxJobArgsLength);

@ -31,15 +31,15 @@ namespace Volo.Blogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ConfigureByConvention();
b.ConfigureAbpUser();
b.ConfigureExtraProperties();
});
builder.Entity<Blog>(b =>
{
b.ToTable(options.TablePrefix + "Blogs", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(BlogConsts.MaxNameLength).HasColumnName(nameof(Blog.Name));
b.Property(x => x.ShortName).IsRequired().HasMaxLength(BlogConsts.MaxShortNameLength).HasColumnName(nameof(Blog.ShortName));
@ -50,7 +50,7 @@ namespace Volo.Blogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Posts", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.Property(x => x.BlogId).HasColumnName(nameof(Post.BlogId));
b.Property(x => x.Title).IsRequired().HasMaxLength(PostConsts.MaxTitleLength).HasColumnName(nameof(Post.Title));
@ -67,7 +67,7 @@ namespace Volo.Blogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Comments", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.Property(x => x.Text).IsRequired().HasMaxLength(CommentConsts.MaxTextLength).HasColumnName(nameof(Comment.Text));
b.Property(x => x.RepliedCommentId).HasColumnName(nameof(Comment.RepliedCommentId));
@ -81,7 +81,7 @@ namespace Volo.Blogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Tags", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(TagConsts.MaxNameLength).HasColumnName(nameof(Tag.Name));
b.Property(x => x.Description).HasMaxLength(TagConsts.MaxDescriptionLength).HasColumnName(nameof(Tag.Description));
@ -94,6 +94,8 @@ namespace Volo.Blogging.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "PostTags", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.PostId).HasColumnName(nameof(PostTag.PostId));
b.Property(x => x.TagId).HasColumnName(nameof(PostTag.TagId));

@ -63,6 +63,8 @@ namespace Volo.Docs.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "DocumentContributors", options.Schema);
b.ConfigureByConvention();
b.HasKey(x => new { x.DocumentId, x.Username });
});
}

@ -1,5 +1,6 @@
using System;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
namespace Volo.Abp.FeatureManagement.EntityFrameworkCore
{
@ -22,6 +23,8 @@ namespace Volo.Abp.FeatureManagement.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "FeatureValues", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.Name).HasMaxLength(FeatureValueConsts.MaxNameLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(FeatureValueConsts.MaxValueLength).IsRequired();
b.Property(x => x.ProviderName).HasMaxLength(FeatureValueConsts.MaxProviderNameLength);

@ -1,6 +1,7 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Users.EntityFrameworkCore;
@ -25,7 +26,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Users", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.ConfigureAbpUser();
b.Property(u => u.NormalizedUserName).IsRequired().HasMaxLength(IdentityUserConsts.MaxNormalizedUserNameLength).HasColumnName(nameof(IdentityUser.NormalizedUserName));
@ -35,12 +36,12 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
b.Property(u => u.TwoFactorEnabled).HasDefaultValue(false).HasColumnName(nameof(IdentityUser.TwoFactorEnabled));
b.Property(u => u.LockoutEnabled).HasDefaultValue(false).HasColumnName(nameof(IdentityUser.LockoutEnabled));
b.Property(u => u.AccessFailedCount).HasDefaultValue(0).HasColumnName(nameof(IdentityUser.AccessFailedCount));
b.HasMany(u => u.Claims).WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
b.HasMany(u => u.Logins).WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
b.HasMany(u => u.Roles).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasMany(u => u.Tokens).WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
b.HasIndex(u => u.NormalizedUserName);
b.HasIndex(u => u.NormalizedEmail);
b.HasIndex(u => u.UserName);
@ -51,6 +52,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "UserClaims", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.Id).ValueGeneratedNever();
b.Property(uc => uc.ClaimType).HasMaxLength(IdentityUserClaimConsts.MaxClaimTypeLength).IsRequired();
@ -63,6 +66,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "UserRoles", options.Schema);
b.ConfigureByConvention();
b.HasKey(ur => new { ur.UserId, ur.RoleId });
b.HasOne<IdentityRole>().WithMany().HasForeignKey(ur => ur.RoleId).IsRequired();
@ -75,6 +80,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "UserLogins", options.Schema);
b.ConfigureByConvention();
b.HasKey(x => new { x.UserId, x.LoginProvider });
b.Property(ul => ul.LoginProvider).HasMaxLength(IdentityUserLoginConsts.MaxLoginProviderLength).IsRequired();
@ -88,6 +95,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "UserTokens", options.Schema);
b.ConfigureByConvention();
b.HasKey(l => new { l.UserId, l.LoginProvider, l.Name });
b.Property(ul => ul.LoginProvider).HasMaxLength(IdentityUserTokenConsts.MaxLoginProviderLength).IsRequired();
@ -98,8 +107,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Roles", options.Schema);
b.ConfigureConcurrencyStamp();
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(r => r.Name).IsRequired().HasMaxLength(IdentityRoleConsts.MaxNameLength);
b.Property(r => r.NormalizedName).IsRequired().HasMaxLength(IdentityRoleConsts.MaxNormalizedNameLength);
@ -117,6 +125,8 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "RoleClaims", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.Id).ValueGeneratedNever();
b.Property(uc => uc.ClaimType).HasMaxLength(IdentityRoleClaimConsts.MaxClaimTypeLength).IsRequired();
@ -129,7 +139,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "ClaimTypes", options.Schema);
b.ConfigureExtraProperties();
b.ConfigureByConvention();
b.Property(uc => uc.Name).HasMaxLength(IdentityClaimTypeConsts.MaxNameLength).IsRequired(); // make unique
b.Property(uc => uc.Regex).HasMaxLength(IdentityClaimTypeConsts.MaxRegexLength);

@ -28,264 +28,292 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore
optionsAction?.Invoke(options);
builder.Entity<Client>(client =>
builder.Entity<Client>(b =>
{
client.ToTable(options.TablePrefix + "Clients", options.Schema);
client.ConfigureFullAuditedAggregateRoot();
client.Property(x => x.ClientId).HasMaxLength(ClientConsts.ClientIdMaxLength).IsRequired();
client.Property(x => x.ProtocolType).HasMaxLength(ClientConsts.ProtocolTypeMaxLength).IsRequired();
client.Property(x => x.ClientName).HasMaxLength(ClientConsts.ClientNameMaxLength);
client.Property(x => x.ClientUri).HasMaxLength(ClientConsts.ClientUriMaxLength);
client.Property(x => x.LogoUri).HasMaxLength(ClientConsts.LogoUriMaxLength);
client.Property(x => x.Description).HasMaxLength(ClientConsts.DescriptionMaxLength);
client.Property(x => x.FrontChannelLogoutUri).HasMaxLength(ClientConsts.FrontChannelLogoutUriMaxLength);
client.Property(x => x.BackChannelLogoutUri).HasMaxLength(ClientConsts.BackChannelLogoutUriMaxLength);
client.Property(x => x.ClientClaimsPrefix).HasMaxLength(ClientConsts.ClientClaimsPrefixMaxLength);
client.Property(x => x.PairWiseSubjectSalt).HasMaxLength(ClientConsts.PairWiseSubjectSaltMaxLength);
client.Property(x => x.UserCodeType).HasMaxLength(ClientConsts.UserCodeTypeMaxLength);
client.HasMany(x => x.AllowedScopes).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.ClientSecrets).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.AllowedGrantTypes).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.AllowedCorsOrigins).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.RedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.PostLogoutRedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.IdentityProviderRestrictions).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.Claims).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasMany(x => x.Properties).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
client.HasIndex(x => x.ClientId);
b.ToTable(options.TablePrefix + "Clients", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.ClientId).HasMaxLength(ClientConsts.ClientIdMaxLength).IsRequired();
b.Property(x => x.ProtocolType).HasMaxLength(ClientConsts.ProtocolTypeMaxLength).IsRequired();
b.Property(x => x.ClientName).HasMaxLength(ClientConsts.ClientNameMaxLength);
b.Property(x => x.ClientUri).HasMaxLength(ClientConsts.ClientUriMaxLength);
b.Property(x => x.LogoUri).HasMaxLength(ClientConsts.LogoUriMaxLength);
b.Property(x => x.Description).HasMaxLength(ClientConsts.DescriptionMaxLength);
b.Property(x => x.FrontChannelLogoutUri).HasMaxLength(ClientConsts.FrontChannelLogoutUriMaxLength);
b.Property(x => x.BackChannelLogoutUri).HasMaxLength(ClientConsts.BackChannelLogoutUriMaxLength);
b.Property(x => x.ClientClaimsPrefix).HasMaxLength(ClientConsts.ClientClaimsPrefixMaxLength);
b.Property(x => x.PairWiseSubjectSalt).HasMaxLength(ClientConsts.PairWiseSubjectSaltMaxLength);
b.Property(x => x.UserCodeType).HasMaxLength(ClientConsts.UserCodeTypeMaxLength);
b.HasMany(x => x.AllowedScopes).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.ClientSecrets).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.AllowedGrantTypes).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.AllowedCorsOrigins).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.RedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.PostLogoutRedirectUris).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.IdentityProviderRestrictions).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.Claims).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasMany(x => x.Properties).WithOne().HasForeignKey(x => x.ClientId).IsRequired();
b.HasIndex(x => x.ClientId);
});
builder.Entity<ClientGrantType>(grantType =>
builder.Entity<ClientGrantType>(b =>
{
grantType.ToTable(options.TablePrefix + "ClientGrantTypes", options.Schema);
b.ToTable(options.TablePrefix + "ClientGrantTypes", options.Schema);
b.ConfigureByConvention();
grantType.HasKey(x => new { x.ClientId, x.GrantType });
b.HasKey(x => new { x.ClientId, x.GrantType });
grantType.Property(x => x.GrantType).HasMaxLength(ClientGrantTypeConsts.GrantTypeMaxLength).IsRequired();
b.Property(x => x.GrantType).HasMaxLength(ClientGrantTypeConsts.GrantTypeMaxLength).IsRequired();
});
builder.Entity<ClientRedirectUri>(redirectUri =>
builder.Entity<ClientRedirectUri>(b =>
{
redirectUri.ToTable(options.TablePrefix + "ClientRedirectUris", options.Schema);
b.ToTable(options.TablePrefix + "ClientRedirectUris", options.Schema);
redirectUri.HasKey(x => new { x.ClientId, x.RedirectUri });
b.ConfigureByConvention();
b.HasKey(x => new { x.ClientId, x.RedirectUri });
if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql)
{
redirectUri.Property(x => x.RedirectUri).HasMaxLength(300).IsRequired();
b.Property(x => x.RedirectUri).HasMaxLength(300).IsRequired();
}
else
{
redirectUri.Property(x => x.RedirectUri).HasMaxLength(ClientRedirectUriConsts.RedirectUriMaxLength).IsRequired();
b.Property(x => x.RedirectUri).HasMaxLength(ClientRedirectUriConsts.RedirectUriMaxLength).IsRequired();
}
});
builder.Entity<ClientPostLogoutRedirectUri>(postLogoutRedirectUri =>
builder.Entity<ClientPostLogoutRedirectUri>(b =>
{
postLogoutRedirectUri.ToTable(options.TablePrefix + "ClientPostLogoutRedirectUris", options.Schema);
b.ToTable(options.TablePrefix + "ClientPostLogoutRedirectUris", options.Schema);
b.ConfigureByConvention();
postLogoutRedirectUri.HasKey(x => new { x.ClientId, x.PostLogoutRedirectUri });
b.HasKey(x => new { x.ClientId, x.PostLogoutRedirectUri });
if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql)
{
postLogoutRedirectUri.Property(x => x.PostLogoutRedirectUri).HasMaxLength(300).IsRequired();
b.Property(x => x.PostLogoutRedirectUri).HasMaxLength(300).IsRequired();
}
else
{
postLogoutRedirectUri.Property(x => x.PostLogoutRedirectUri).HasMaxLength(ClientPostLogoutRedirectUriConsts.PostLogoutRedirectUriMaxLength).IsRequired();
b.Property(x => x.PostLogoutRedirectUri).HasMaxLength(ClientPostLogoutRedirectUriConsts.PostLogoutRedirectUriMaxLength).IsRequired();
}
});
builder.Entity<ClientScope>(scope =>
builder.Entity<ClientScope>(b =>
{
scope.ToTable(options.TablePrefix + "ClientScopes", options.Schema);
b.ToTable(options.TablePrefix + "ClientScopes", options.Schema);
scope.HasKey(x => new { x.ClientId, x.Scope });
b.ConfigureByConvention();
b.HasKey(x => new { x.ClientId, x.Scope });
scope.Property(x => x.Scope).HasMaxLength(ClientScopeConsts.ScopeMaxLength).IsRequired();
b.Property(x => x.Scope).HasMaxLength(ClientScopeConsts.ScopeMaxLength).IsRequired();
});
builder.Entity<ClientSecret>(secret =>
builder.Entity<ClientSecret>(b =>
{
secret.ToTable(options.TablePrefix + "ClientSecrets", options.Schema);
b.ToTable(options.TablePrefix + "ClientSecrets", options.Schema);
secret.HasKey(x => new { x.ClientId, x.Type, x.Value });
b.ConfigureByConvention();
secret.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired();
b.HasKey(x => new { x.ClientId, x.Type, x.Value });
b.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired();
if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql)
{
secret.Property(x => x.Value).HasMaxLength(300).IsRequired();
b.Property(x => x.Value).HasMaxLength(300).IsRequired();
}
else
{
secret.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired();
}
secret.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength);
b.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength);
});
builder.Entity<ClientClaim>(claim =>
builder.Entity<ClientClaim>(b =>
{
claim.ToTable(options.TablePrefix + "ClientClaims", options.Schema);
b.ToTable(options.TablePrefix + "ClientClaims", options.Schema);
claim.HasKey(x => new { x.ClientId, x.Type, x.Value });
b.ConfigureByConvention();
claim.Property(x => x.Type).HasMaxLength(ClientClaimConsts.TypeMaxLength).IsRequired();
claim.Property(x => x.Value).HasMaxLength(ClientClaimConsts.ValueMaxLength).IsRequired();
b.HasKey(x => new { x.ClientId, x.Type, x.Value });
b.Property(x => x.Type).HasMaxLength(ClientClaimConsts.TypeMaxLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(ClientClaimConsts.ValueMaxLength).IsRequired();
});
builder.Entity<ClientIdPRestriction>(idPRestriction =>
builder.Entity<ClientIdPRestriction>(b =>
{
idPRestriction.ToTable(options.TablePrefix + "ClientIdPRestrictions", options.Schema);
b.ToTable(options.TablePrefix + "ClientIdPRestrictions", options.Schema);
b.ConfigureByConvention();
idPRestriction.HasKey(x => new { x.ClientId, x.Provider });
b.HasKey(x => new { x.ClientId, x.Provider });
idPRestriction.Property(x => x.Provider).HasMaxLength(ClientIdPRestrictionConsts.ProviderMaxLength).IsRequired();
b.Property(x => x.Provider).HasMaxLength(ClientIdPRestrictionConsts.ProviderMaxLength).IsRequired();
});
builder.Entity<ClientCorsOrigin>(corsOrigin =>
builder.Entity<ClientCorsOrigin>(b =>
{
corsOrigin.ToTable(options.TablePrefix + "ClientCorsOrigins", options.Schema);
b.ToTable(options.TablePrefix + "ClientCorsOrigins", options.Schema);
corsOrigin.HasKey(x => new { x.ClientId, x.Origin });
b.ConfigureByConvention();
corsOrigin.Property(x => x.Origin).HasMaxLength(ClientCorsOriginConsts.OriginMaxLength).IsRequired();
b.HasKey(x => new { x.ClientId, x.Origin });
b.Property(x => x.Origin).HasMaxLength(ClientCorsOriginConsts.OriginMaxLength).IsRequired();
});
builder.Entity<ClientProperty>(property =>
builder.Entity<ClientProperty>(b =>
{
property.ToTable(options.TablePrefix + "ClientProperties", options.Schema);
b.ToTable(options.TablePrefix + "ClientProperties", options.Schema);
b.ConfigureByConvention();
property.HasKey(x => new { x.ClientId, x.Key });
b.HasKey(x => new { x.ClientId, x.Key });
property.Property(x => x.Key).HasMaxLength(ClientPropertyConsts.KeyMaxLength).IsRequired();
property.Property(x => x.Value).HasMaxLength(ClientPropertyConsts.ValueMaxLength).IsRequired();
b.Property(x => x.Key).HasMaxLength(ClientPropertyConsts.KeyMaxLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(ClientPropertyConsts.ValueMaxLength).IsRequired();
});
builder.Entity<PersistedGrant>(grant =>
builder.Entity<PersistedGrant>(b =>
{
grant.ToTable(options.TablePrefix + "PersistedGrants", options.Schema);
b.ToTable(options.TablePrefix + "PersistedGrants", options.Schema);
grant.ConfigureExtraProperties();
b.ConfigureByConvention();
grant.Property(x => x.Key).HasMaxLength(PersistedGrantConsts.KeyMaxLength).ValueGeneratedNever();
grant.Property(x => x.Type).HasMaxLength(PersistedGrantConsts.TypeMaxLength).IsRequired();
grant.Property(x => x.SubjectId).HasMaxLength(PersistedGrantConsts.SubjectIdMaxLength);
grant.Property(x => x.ClientId).HasMaxLength(PersistedGrantConsts.ClientIdMaxLength).IsRequired();
grant.Property(x => x.CreationTime).IsRequired();
b.Property(x => x.Key).HasMaxLength(PersistedGrantConsts.KeyMaxLength).ValueGeneratedNever();
b.Property(x => x.Type).HasMaxLength(PersistedGrantConsts.TypeMaxLength).IsRequired();
b.Property(x => x.SubjectId).HasMaxLength(PersistedGrantConsts.SubjectIdMaxLength);
b.Property(x => x.ClientId).HasMaxLength(PersistedGrantConsts.ClientIdMaxLength).IsRequired();
b.Property(x => x.CreationTime).IsRequired();
if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql)
{
grant.Property(x => x.Data).HasMaxLength(10000).IsRequired();
b.Property(x => x.Data).HasMaxLength(10000).IsRequired();
}
else
{
grant.Property(x => x.Data).HasMaxLength(PersistedGrantConsts.DataMaxLength).IsRequired();
b.Property(x => x.Data).HasMaxLength(PersistedGrantConsts.DataMaxLength).IsRequired();
}
grant.HasKey(x => x.Key); //TODO: What about Id!!!
b.HasKey(x => x.Key); //TODO: What about Id!!!
grant.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type });
grant.HasIndex(x => x.Expiration);
b.HasIndex(x => new { x.SubjectId, x.ClientId, x.Type });
b.HasIndex(x => x.Expiration);
});
builder.Entity<IdentityResource>(identityResource =>
builder.Entity<IdentityResource>(b =>
{
identityResource.ToTable(options.TablePrefix + "IdentityResources", options.Schema);
b.ToTable(options.TablePrefix + "IdentityResources", options.Schema);
identityResource.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
identityResource.Property(x => x.Name).HasMaxLength(IdentityResourceConsts.NameMaxLength).IsRequired();
identityResource.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength);
identityResource.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength);
identityResource.Property(x => x.Properties)
b.Property(x => x.Name).HasMaxLength(IdentityResourceConsts.NameMaxLength).IsRequired();
b.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength);
b.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength);
b.Property(x => x.Properties)
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
identityResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired();
b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired();
});
builder.Entity<IdentityClaim>(claim =>
builder.Entity<IdentityClaim>(b =>
{
claim.ToTable(options.TablePrefix + "IdentityClaims", options.Schema);
b.ToTable(options.TablePrefix + "IdentityClaims", options.Schema);
b.ConfigureByConvention();
claim.HasKey(x => new { x.IdentityResourceId, x.Type });
b.HasKey(x => new { x.IdentityResourceId, x.Type });
claim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
});
builder.Entity<ApiResource>(apiResource =>
builder.Entity<ApiResource>(b =>
{
apiResource.ToTable(options.TablePrefix + "ApiResources", options.Schema);
b.ToTable(options.TablePrefix + "ApiResources", options.Schema);
apiResource.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
apiResource.Property(x => x.Name).HasMaxLength(ApiResourceConsts.NameMaxLength).IsRequired();
apiResource.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength);
apiResource.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength);
apiResource.Property(x => x.Properties)
b.Property(x => x.Name).HasMaxLength(ApiResourceConsts.NameMaxLength).IsRequired();
b.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength);
b.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength);
b.Property(x => x.Properties)
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
apiResource.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
apiResource.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
apiResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
b.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
b.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
});
builder.Entity<ApiSecret>(apiSecret =>
builder.Entity<ApiSecret>(b =>
{
apiSecret.ToTable(options.TablePrefix + "ApiSecrets", options.Schema);
b.ToTable(options.TablePrefix + "ApiSecrets", options.Schema);
apiSecret.HasKey(x => new { x.ApiResourceId, x.Type, x.Value });
b.ConfigureByConvention();
b.HasKey(x => new { x.ApiResourceId, x.Type, x.Value });
apiSecret.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired();
apiSecret.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength);
b.Property(x => x.Type).HasMaxLength(SecretConsts.TypeMaxLength).IsRequired();
b.Property(x => x.Description).HasMaxLength(SecretConsts.DescriptionMaxLength);
if (options.DatabaseProvider == EfCoreDatabaseProvider.MySql)
{
apiSecret.Property(x => x.Value).HasMaxLength(300).IsRequired();
b.Property(x => x.Value).HasMaxLength(300).IsRequired();
}
else
{
apiSecret.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(SecretConsts.ValueMaxLength).IsRequired();
}
});
builder.Entity<ApiResourceClaim>(apiClaim =>
builder.Entity<ApiResourceClaim>(b =>
{
apiClaim.ToTable(options.TablePrefix + "ApiClaims", options.Schema);
b.ToTable(options.TablePrefix + "ApiClaims", options.Schema);
b.ConfigureByConvention();
apiClaim.HasKey(x => new { x.ApiResourceId, x.Type });
b.HasKey(x => new { x.ApiResourceId, x.Type });
apiClaim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
});
builder.Entity<ApiScope>(apiScope =>
builder.Entity<ApiScope>(b =>
{
apiScope.ToTable(options.TablePrefix + "ApiScopes", options.Schema);
b.ToTable(options.TablePrefix + "ApiScopes", options.Schema);
apiScope.HasKey(x => new { x.ApiResourceId, x.Name });
b.ConfigureByConvention();
b.HasKey(x => new { x.ApiResourceId, x.Name });
apiScope.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired();
apiScope.Property(x => x.DisplayName).HasMaxLength(ApiScopeConsts.DisplayNameMaxLength);
apiScope.Property(x => x.Description).HasMaxLength(ApiScopeConsts.DescriptionMaxLength);
b.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired();
b.Property(x => x.DisplayName).HasMaxLength(ApiScopeConsts.DisplayNameMaxLength);
b.Property(x => x.Description).HasMaxLength(ApiScopeConsts.DescriptionMaxLength);
apiScope.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => new { x.ApiResourceId, x.Name }).IsRequired();
b.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => new { x.ApiResourceId, x.Name }).IsRequired();
});
builder.Entity<ApiScopeClaim>(apiScopeClaim =>
builder.Entity<ApiScopeClaim>(b =>
{
apiScopeClaim.ToTable(options.TablePrefix + "ApiScopeClaims", options.Schema);
b.ToTable(options.TablePrefix + "ApiScopeClaims", options.Schema);
b.ConfigureByConvention();
apiScopeClaim.HasKey(x => new { x.ApiResourceId, x.Name, x.Type });
b.HasKey(x => new { x.ApiResourceId, x.Name, x.Type });
apiScopeClaim.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
apiScopeClaim.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired();
b.Property(x => x.Type).HasMaxLength(UserClaimConsts.TypeMaxLength).IsRequired();
b.Property(x => x.Name).HasMaxLength(ApiScopeConsts.NameMaxLength).IsRequired();
});
builder.Entity<DeviceFlowCodes>(b =>

@ -1,6 +1,7 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
namespace Volo.Abp.PermissionManagement.EntityFrameworkCore
{
@ -23,6 +24,8 @@ namespace Volo.Abp.PermissionManagement.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "PermissionGrants", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.Name).HasMaxLength(PermissionGrantConsts.MaxNameLength).IsRequired();
b.Property(x => x.ProviderName).HasMaxLength(PermissionGrantConsts.MaxProviderNameLength).IsRequired();
b.Property(x => x.ProviderKey).HasMaxLength(PermissionGrantConsts.MaxProviderKeyLength).IsRequired();

@ -38,6 +38,8 @@ namespace Volo.Abp.SettingManagement.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Settings", options.Schema);
b.ConfigureByConvention();
b.Property(x => x.Name).HasMaxLength(SettingConsts.MaxNameLength).IsRequired();
b.Property(x => x.Value).HasMaxLength(SettingConsts.MaxValueLength).IsRequired();
b.Property(x => x.ProviderName).HasMaxLength(SettingConsts.MaxProviderNameLength);

@ -24,7 +24,7 @@ namespace Volo.Abp.TenantManagement.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "Tenants", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
b.Property(t => t.Name).IsRequired().HasMaxLength(TenantConsts.MaxNameLength);
@ -37,6 +37,8 @@ namespace Volo.Abp.TenantManagement.EntityFrameworkCore
{
b.ToTable(options.TablePrefix + "TenantConnectionStrings", options.Schema);
b.ConfigureByConvention();
b.HasKey(x => new { x.TenantId, x.Name });
b.Property(cs => cs.Name).IsRequired().HasMaxLength(TenantConnectionStringConsts.MaxNameLength);

@ -81,7 +81,7 @@
"protractor": "~5.4.0",
"rxjs": "~6.4.0",
"snq": "^1.0.3",
"symlink-manager": "^1.4.2",
"symlink-manager": "^1.4.3",
"ts-node": "~7.0.0",
"ts-toolbelt": "^6.3.6",
"tsickle": "^0.37.0",

@ -3,7 +3,8 @@ export * from './auth.service';
export * from './config-state.service';
export * from './lazy-load.service';
export * from './localization.service';
export * from './profile-state.service';
export * from './profile.service';
export * from './rest.service';
export * from './profile-state.service';
export * from './session-state.service';
export * from './track-by.service';

@ -301,7 +301,7 @@ export class ConfigState {
}
@Action(SetEnvironment)
setEnvironment({ patchState }: StateContext<Config.State>, environment: Config.Environment) {
setEnvironment({ patchState }: StateContext<Config.State>, { environment }:SetEnvironment) {
return patchState({
environment,
});

@ -0,0 +1,802 @@
import { LinkedList } from '../utils/linked-list';
describe('Linked List (Doubly)', () => {
let list: LinkedList;
beforeEach(() => (list = new LinkedList()));
describe('#length', () => {
it('should initially be 0', () => {
expect(list.length).toBe(0);
});
});
describe('#head', () => {
it('should initially be undefined', () => {
expect(list.head).toBeUndefined();
});
});
describe('#tail', () => {
it('should initially be undefined', () => {
expect(list.tail).toBeUndefined();
});
});
describe('#add', () => {
describe('#head', () => {
it('should add node to the head of the list', () => {
list.addHead('a');
// "a"
expect(list.head.value).toBe('a');
expect(list.tail.value).toBe('a');
});
it('should create reference to previous and next nodes', () => {
list.add('a').head();
list.add('b').head();
list.add('c').head();
// "c" <-> "b" <-> "a"
expect(list.length).toBe(3);
expect(list.head.value).toBe('c');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('a');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#tail', () => {
it('should add node to the tail of the list', () => {
list.addTail('a');
// "a"
expect(list.head.value).toBe('a');
expect(list.tail.value).toBe('a');
expect(list.tail.next).toBeUndefined();
});
it('should create reference to previous and next nodes', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#after', () => {
it('should place a node after node with given value', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').after('b');
// "a" <-> "b" <-> "x" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('x');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('x');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).after({ x: 1 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
expect(list.length).toBe(4);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(0);
expect(list.head.next.next.value.x).toBe(2);
expect(list.head.next.next.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(2);
expect(list.tail.previous.previous.value.x).toBe(0);
expect(list.tail.previous.previous.previous.value.x).toBe(1);
});
});
describe('#before', () => {
it('should place a node before node with given value', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').before('b');
// "a" <-> "x" <-> "b" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('x');
expect(list.head.next.next.value).toBe('b');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('x');
expect(list.tail.previous.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.add({ x: 0 }).before({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":3}
expect(list.length).toBe(4);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(0);
expect(list.head.next.next.value.x).toBe(2);
expect(list.head.next.next.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(2);
expect(list.tail.previous.previous.value.x).toBe(0);
expect(list.tail.previous.previous.previous.value.x).toBe(1);
});
});
describe('#byIndex', () => {
it('should place a node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.add('x').byIndex(1);
// "a" <-> "x" <-> "b" <-> "c"
list.add('y').byIndex(3);
// "a" <-> "x" <-> "b" <-> "y" <-> "c"
expect(list.length).toBe(5);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('x');
expect(list.head.next.next.value).toBe('b');
expect(list.head.next.next.next.value).toBe('y');
expect(list.head.next.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('y');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('x');
expect(list.tail.previous.previous.previous.previous.value).toBe('a');
});
});
});
describe('#find', () => {
it('should return the first node found based on given predicate', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const node1 = list.find(node => node.previous && node.previous.value === 'a');
expect(node1.value).toBe('x');
expect(node1.previous.value).toBe('a');
expect(node1.next.value).toBe('b');
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const node2 = list.find(node => node.next && node.next.value === 'c');
expect(node2.value).toBe('x');
expect(node2.previous.value).toBe('b');
expect(node2.next.value).toBe('c');
});
it('should return undefined when list is empty', () => {
const found = list.find(node => node.value === 'x');
expect(found).toBeUndefined();
});
it('should return undefined when predicate finds no match', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const found = list.find(node => node.value === 'x');
expect(found).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#findIndex', () => {
it('should return the index of the first node found based on given predicate', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const index1 = list.findIndex(node => node.previous && node.previous.value === 'a');
expect(index1).toBe(1);
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const index2 = list.findIndex(node => node.next && node.next.value === 'c');
expect(index2).toBe(3);
});
it('should return -1 when list is empty', () => {
const index = list.findIndex(node => node.value === 'x');
expect(index).toBe(-1);
});
it('should return -1 when no match is found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const index = list.findIndex(node => node.value === 'x');
expect(index).toBe(-1);
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#forEach', () => {
it('should call given function for each node of the list', () => {
const a = list.add('a').tail();
const b = list.add('b').tail();
const c = list.add('c').tail();
// "a" <-> "b" <-> "c"
const spy = jest.fn();
list.forEach(spy);
expect(spy.mock.calls).toEqual([
[a, 0, list],
[b, 1, list],
[c, 2, list],
]);
});
it('should not call given function when list is empty', () => {
const spy = jest.fn();
list.forEach(spy);
expect(spy).not.toHaveBeenCalled();
});
});
describe('#drop', () => {
describe('#head', () => {
it('should return undefined when there is no head', () => {
expect(list.drop().head()).toBeUndefined();
});
it('should remove the node from the head of the list', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().head();
// "b" <-> "c"
expect(list.length).toBe(2);
expect(list.head.value).toBe('b');
expect(list.head.next.value).toBe('c');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.next).toBeUndefined();
});
});
describe('#head', () => {
it('should return undefined when there is no tail', () => {
expect(list.drop().tail()).toBeUndefined();
});
it('should remove the node from the tail of the list', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
list.drop().tail();
// "a" <-> "b"
expect(list.length).toBe(2);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.previous).toBeUndefined();
expect(list.tail.value).toBe('b');
expect(list.tail.previous.value).toBe('a');
expect(list.tail.next).toBeUndefined();
});
});
describe('#byIndex', () => {
it('should remove the node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
list.add('d').tail();
list.add('e').tail();
// "a" <-> "b" <-> "c" <-> "d" <-> "e"
list.drop().byIndex(1);
// "a" <-> "c" <-> "d" <-> "e"
list.drop().byIndex(2);
// "a" <-> "c" <-> "e"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('c');
expect(list.head.next.next.value).toBe('e');
expect(list.tail.value).toBe('e');
expect(list.tail.previous.value).toBe('c');
expect(list.tail.previous.previous.value).toBe('a');
});
it('should return undefined when list is empty', () => {
const node = list.drop().byIndex(0);
expect(node).toBeUndefined();
});
it('should return undefined when given index does not exist', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node1 = list.drop().byIndex(4);
// "a" <-> "b" <-> "c"
expect(node1).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "c"
const node2 = list.drop().byIndex(-1);
// "a" <-> "b" <-> "c"
expect(node2).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#byValue', () => {
it('should remove the first node with given value', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
list.drop().byValue('x');
// "a" <-> "b" <-> "x" <-> "c"
expect(list.length).toBe(4);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('x');
expect(list.head.next.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('x');
expect(list.tail.previous.previous.value).toBe('b');
expect(list.tail.previous.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "x" <-> "c"
list.drop().byValue('x');
// "a" <-> "b" <-> "c"
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
list.drop().byValue({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":3}
expect(list.length).toBe(2);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(1);
});
it('should return undefined when list is empty', () => {
const node = list.drop().byValue('x');
expect(node).toBeUndefined();
});
it('should return undefined when given value is not found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node = list.drop().byValue('x');
// "a" <-> "b" <-> "c"
expect(node).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#byValueAll', () => {
it('should remove all nodes with given value', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const dropped = list.drop().byValueAll('x');
// "a" <-> "b" <-> "c"
expect(dropped.length).toBe(2);
expect(dropped[0].value).toEqual('x');
expect(dropped[0].previous.value).toEqual('a');
expect(dropped[0].next.value).toEqual('b');
expect(dropped[1].value).toEqual('x');
expect(dropped[1].previous.value).toEqual('b');
expect(dropped[1].next.value).toEqual('c');
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
it('should be able to receive a custom compareFn', () => {
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();
// {"x":1} <-> {"x":0} <-> {"x":2} <-> {"x":0} <-> {"x":3}
const dropped = list.drop().byValueAll({ x: 0 }, (v1: X, v2: X) => v1.x === v2.x);
// {"x":1} <-> {"x":2} <-> {"x":3}
expect(dropped.length).toBe(2);
expect(dropped[0].value.x).toEqual(0);
expect(dropped[0].previous.value.x).toEqual(1);
expect(dropped[0].next.value.x).toEqual(2);
expect(dropped[1].value.x).toEqual(0);
expect(dropped[1].previous.value.x).toEqual(2);
expect(dropped[1].next.value.x).toEqual(3);
expect(list.length).toBe(3);
expect(list.head.value.x).toBe(1);
expect(list.head.next.value.x).toBe(2);
expect(list.head.next.next.value.x).toBe(3);
expect(list.tail.value.x).toBe(3);
expect(list.tail.previous.value.x).toBe(2);
expect(list.tail.previous.previous.value.x).toBe(1);
});
it('should return empty array when list is empty', () => {
const dropped = list.drop().byValueAll('x');
expect(dropped).toEqual([]);
});
it('should return empty array when given value is not found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const dropped = list.drop().byValueAll('x');
// "a" <-> "b" <-> "c"
expect(dropped).toEqual([]);
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
});
describe('#get', () => {
it('should return node at given index', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node = list.get(1);
expect(node.value).toBe('b');
expect(node.previous.value).toBe('a');
expect(node.next.value).toBe('c');
});
it('should return undefined when list is empty', () => {
const node = list.get(1);
expect(node).toBeUndefined();
});
it('should return undefined when predicate finds no match', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const node1 = list.get(4);
expect(node1).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
// "a" <-> "b" <-> "c"
const node2 = list.get(-1);
expect(node2).toBeUndefined();
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#indexOf', () => {
it('should return the index of the first node found based on given value', () => {
list.add('a').tail();
list.add('x').tail();
list.add('b').tail();
list.add('x').tail();
list.add('c').tail();
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
const index1 = list.indexOf('x');
expect(index1).toBe(1);
// "a" <-> "x" <-> "b" <-> "x" <-> "c"
let timesFound = 0;
const index2 = list.indexOf('x', (v1: string, v2: string) => {
if (timesFound > 1) return false;
timesFound += Number(v1 === v2);
return timesFound > 1;
});
expect(index2).toBe(3);
});
it('should be able to receive a custom compareFn', () => {
list.add({ x: 1 }).tail();
list.add({ x: 2 }).tail();
list.add({ x: 3 }).tail();
// {"x":1} <-> {"x":2} <-> {"x":3}
const index = list.indexOf({ x: 2 }, (v1: X, v2: X) => v1.x === v2.x);
expect(index).toBe(1);
});
it('should return -1 when list is empty', () => {
const index = list.indexOf('x');
expect(index).toBe(-1);
});
it('should return -1 when no match is found', () => {
list.add('a').tail();
list.add('b').tail();
list.add('c').tail();
// "a" <-> "b" <-> "c"
const index = list.indexOf('x');
expect(index).toBe(-1);
expect(list.length).toBe(3);
expect(list.head.value).toBe('a');
expect(list.head.next.value).toBe('b');
expect(list.head.next.next.value).toBe('c');
expect(list.tail.value).toBe('c');
expect(list.tail.previous.value).toBe('b');
expect(list.tail.previous.previous.value).toBe('a');
});
});
describe('#toArray', () => {
it('should return array representation', () => {
list.addTail('a');
list.addTail(2);
list.addTail('c');
list.addTail({ k: 4, v: 'd' });
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
const arr = list.toArray();
expect(arr).toEqual(['a', 2, 'c', { k: 4, v: 'd' }]);
});
it('should return empty array when list is empty', () => {
const arr = list.toArray();
expect(arr).toEqual([]);
});
});
describe('#toString', () => {
it('should return string representation', () => {
list.addTail('a');
list.addTail(2);
list.addTail('c');
list.addTail({ k: 4, v: 'd' });
// "a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}
const str = list.toString();
expect(str).toBe('"a" <-> 2 <-> "c" <-> {"k":4,"v":"d"}');
});
it('should return empty string when list is empty', () => {
const str = list.toString();
expect(str).toBe('');
});
});
it('should be iterable', () => {
list.addTail('a');
list.addTail('b');
list.addTail('c');
// "a" <-> "b" <-> "c"
const arr = [];
for (const value of list) {
arr.push(value);
}
expect(arr).toEqual(['a', 'b', 'c']);
});
});
interface X {
[k: string]: any;
}

@ -1,5 +1,6 @@
export * from './common-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './linked-list';
export * from './route-utils';
export * from './rxjs-utils';

@ -0,0 +1,259 @@
/* tslint:disable:no-non-null-assertion */
import compare from 'just-compare';
export class ListNode<T = any> {
readonly value: T;
next: ListNode | undefined;
previous: ListNode | undefined;
constructor(value: T) {
this.value = value;
}
}
export class LinkedList<T = any> {
private first: ListNode<T> | undefined;
private last: ListNode<T> | undefined;
private size = 0;
get head(): ListNode<T> | undefined {
return this.first;
}
get tail(): ListNode<T> | undefined {
return this.last;
}
get length(): number {
return this.size;
}
private linkWith(
value: T,
previousNode: ListNode<T> | undefined,
nextNode: ListNode<T> | undefined,
): ListNode<T> {
const node = new ListNode(value);
if (!previousNode) return this.addHead(value);
if (!nextNode) return this.addTail(value);
node.previous = previousNode;
previousNode.next = node;
node.next = nextNode;
nextNode.previous = node;
this.size += 1;
return node;
}
add(value: T) {
return {
after: (previousValue: T, compareFn = compare) => {
return this.addAfter(value, previousValue, compareFn);
},
before: (nextValue: T, compareFn = compare) => {
return this.addBefore(value, nextValue, compareFn);
},
byIndex: (position: number): ListNode<T> => {
return this.addByIndex(value, position);
},
head: (): ListNode<T> => {
return this.addHead(value);
},
tail: (): ListNode<T> => {
return this.addTail(value);
},
};
}
addAfter(value: T, previousValue: T, compareFn = compare): ListNode<T> {
const previous = this.find(node => compareFn(node.value, previousValue));
return previous ? this.linkWith(value, previous, previous.next) : this.addTail(value);
}
addBefore(value: T, nextValue: T, compareFn = compare): ListNode<T> {
const next = this.find(node => compareFn(node.value, nextValue));
return next ? this.linkWith(value, next.previous, next) : this.addHead(value);
}
addByIndex(value: T, position: number): ListNode<T> {
if (position <= 0) return this.addHead(value);
if (position >= this.size) return this.addTail(value);
const next = this.get(position)!;
return this.linkWith(value, next.previous, next);
}
addHead(value: T): ListNode<T> {
const node = new ListNode(value);
node.next = this.first;
if (this.first) this.first.previous = node;
else this.last = node;
this.first = node;
this.size += 1;
return node;
}
addTail(value: T): ListNode<T> {
const node = new ListNode(value);
if (this.first) {
node.previous = this.last;
this.last!.next = node;
this.last = node;
} else {
this.first = node;
this.last = node;
}
this.size += 1;
return node;
}
drop() {
return {
byIndex: (position: number) => this.dropByIndex(position),
byValue: (value: T, compareFn = compare) => this.dropByValue(value, compareFn),
byValueAll: (value: T, compareFn = compare) => this.dropByValueAll(value, compareFn),
head: () => this.dropHead(),
tail: () => this.dropTail(),
};
}
dropByIndex(position: number): ListNode<T> | undefined {
if (position === 0) return this.dropHead();
else if (position === this.size - 1) return this.dropTail();
const current = this.get(position);
if (current) {
current.previous!.next = current.next;
current.next!.previous = current.previous;
this.size -= 1;
return current;
}
return undefined;
}
dropByValue(value: T, compareFn = compare): ListNode<T> | undefined {
const position = this.findIndex(node => compareFn(node.value, value));
if (position < 0) return undefined;
return this.dropByIndex(position);
}
dropByValueAll(value: T, compareFn = compare): ListNode<T>[] {
const dropped: ListNode<T>[] = [];
for (let current = this.first, position = 0; current; position += 1, current = current.next) {
if (compareFn(current.value, value)) {
dropped.push(this.dropByIndex(position - dropped.length)!);
}
}
return dropped;
}
dropHead(): ListNode<T> | undefined {
const head = this.first;
if (head) {
this.first = head.next;
if (this.first) this.first.previous = undefined;
else this.last = undefined;
this.size -= 1;
return head;
}
return undefined;
}
dropTail(): ListNode<T> | undefined {
const tail = this.last;
if (tail) {
this.last = tail.previous;
if (this.last) this.last.next = undefined;
else this.first = undefined;
this.size -= 1;
return tail;
}
return undefined;
}
find(predicate: ListIteratorFunction<T>): ListNode<T> | undefined {
for (let current = this.first, position = 0; current; position += 1, current = current.next) {
if (predicate(current, position, this)) return current;
}
return undefined;
}
findIndex(predicate: ListIteratorFunction<T>): number {
for (let current = this.first, position = 0; current; position += 1, current = current.next) {
if (predicate(current, position, this)) return position;
}
return -1;
}
forEach<R = boolean>(callback: ListIteratorFunction<T, R>) {
for (let node = this.first, position = 0; node; position += 1, node = node.next) {
callback(node, position, this);
}
}
get(position: number): ListNode<T> | undefined {
return this.find((_, index) => position === index);
}
indexOf(value: T, compareFn = compare): number {
return this.findIndex(node => compareFn(node.value, value));
}
toArray(): T[] {
const array = new Array(this.size);
this.forEach((node, index) => (array[index!] = node.value));
return array;
}
toString(): string {
return this.toArray()
.map(value => JSON.stringify(value))
.join(' <-> ');
}
*[Symbol.iterator]() {
for (let node = this.first, position = 0; node; position += 1, node = node.next) {
yield node.value;
}
}
}
export type ListIteratorFunction<T = any, R = boolean> = (
node: ListNode<T>,
index?: number,
list?: LinkedList,
) => R;

@ -5253,18 +5253,18 @@ execa@^1.0.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-2.1.0.tgz#e5d3ecd837d2a60ec50f3da78fd39767747bbe99"
integrity sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==
execa@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf"
integrity sha512-JbDUxwV3BoT5ZVXQrSVbAiaXhXUkIwvbhPIwZ0N13kX+5yCzOhUNdocxB/UQRuYOHRYYwAxKYwJYc0T4D12pDA==
dependencies:
cross-spawn "^7.0.0"
get-stream "^5.0.0"
human-signals "^1.1.1"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^3.0.0"
npm-run-path "^4.0.0"
onetime "^5.1.0"
p-finally "^2.0.0"
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
@ -6291,6 +6291,11 @@ https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3:
agent-base "^4.3.0"
debug "^3.1.0"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@ -8804,10 +8809,10 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
npm-run-path@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5"
integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==
npm-run-path@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
@ -9060,11 +9065,6 @@ p-finally@^1.0.0:
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
p-finally@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==
p-is-promise@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e"
@ -11376,16 +11376,16 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
symlink-manager@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/symlink-manager/-/symlink-manager-1.4.2.tgz#8ac78ed829637e435cfc61dcd181b26c3ddb61b1"
integrity sha512-FObjOy2UqeX84MqT0CtuincfIDwieYF85TdyffJhALhpvSvoSTdcWE7YCf1lPuJfrO3ezft/dEuvqy0/BGZkFg==
symlink-manager@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/symlink-manager/-/symlink-manager-1.4.3.tgz#c6ada630dd655eecdb7fb10805f54357d8d3dfbd"
integrity sha512-faiwvs0KkNKNdEEUtIXEHDZV/7fULToYONwOKrzVZ0Z4p5ajm7zSGhnKTJgm8WgOcUzhwImJ4Sxo2GOs5k/wSA==
dependencies:
arg "^4.1.0"
chokidar "^3.0.2"
color-support "^1.1.3"
esm "^3.2.25"
execa "^2.0.3"
execa "^4.0.0"
figlet "^1.2.3"
fs-extra "^8.1.0"
inquirer "^6.4.1"

@ -42,7 +42,17 @@ namespace MyCompanyName.MyProjectName.Users
/* Add your own properties here. Example:
*
* public virtual string MyProperty { get; set; }
* public string MyProperty { get; set; }
*
* If you add a property and using the EF Core, remember these;
*
* 1. update MyProjectNameDbContext.OnModelCreating
* to configure the mapping for your new property
* 2. Update MyProjectNameEntityExtensions to extend the IdentityUser entity
* and add your new property to the migration.
* 3. Use the Add-Migration to add a new database migration.
* 4. Run the .DbMigrator project (or use the Update-Database command) to apply
* schema change to the database.
*/
private AppUser()

@ -40,13 +40,6 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureMyProjectName method */
builder.ConfigureMyProjectName();

@ -11,6 +11,8 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
{
public MyProjectNameMigrationsDbContext CreateDbContext(string[] args)
{
MyProjectNameEntityExtensions.Configure();
var configuration = BuildConfiguration();
var builder = new DbContextOptionsBuilder<MyProjectNameMigrationsDbContext>()

@ -3,6 +3,7 @@ using MyCompanyName.MyProjectName.Users;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Identity;
using Volo.Abp.Users.EntityFrameworkCore;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore
@ -39,12 +40,14 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
builder.Entity<AppUser>(b =>
{
b.ToTable("AbpUsers"); //Sharing the same table "AbpUsers" with the IdentityUser
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser
b.ConfigureByConvention();
b.ConfigureAbpUser();
//Moved customization to a method so we can share it with the MyProjectNameMigrationsDbContext class
b.ConfigureCustomUserProperties();
/* Configure mappings for your additional properties
* Also see the MyProjectNameEntityExtensions class
*/
});
/* Configure your own tables/entities inside the ConfigureMyProjectName method */

@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp;
using Volo.Abp.Users;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore
{
@ -20,11 +18,5 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
// //...
//});
}
public static void ConfigureCustomUserProperties<TUser>(this EntityTypeBuilder<TUser> b)
where TUser: class, IUser
{
//b.Property<string>(nameof(AppUser.MyProperty))...
}
}
}

@ -0,0 +1,33 @@
using Volo.Abp.EntityFrameworkCore.Extensions;
using Volo.Abp.Identity;
using Volo.Abp.Threading;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore
{
public static class MyProjectNameEntityExtensions
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public static void Configure()
{
OneTimeRunner.Run(() =>
{
/* You can configure entity extension properties for the
* entities defined in the used modules.
*
* Example:
*
* EntityExtensionManager.AddProperty<IdentityUser, string>(
* "MyProperty",
* b =>
* {
* b.HasMaxLength(128);
* });
*
* See the documentation for more:
* https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities
*/
});
}
}
}

@ -27,6 +27,11 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
)]
public class MyProjectNameEntityFrameworkCoreModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
MyProjectNameEntityExtensions.Configure();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<MyProjectNameDbContext>(options =>

@ -26,7 +26,7 @@ namespace MyCompanyName.MyProjectName.EntityFrameworkCore
//Configure table & schema name
b.ToTable(options.TablePrefix + "Questions", options.Schema);
b.ConfigureFullAuditedAggregateRoot();
b.ConfigureByConvention();
//Properties
b.Property(q => q.Title).IsRequired().HasMaxLength(QuestionConsts.MaxTitleLength);

Loading…
Cancel
Save