@ -0,0 +1,485 @@ |
|||
# EF Core Advanced Database Migrations |
|||
|
|||
This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application. |
|||
|
|||
> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Index.md). |
|||
|
|||
## About the EF Core Code First Migrations |
|||
|
|||
Entity Framework Core provides an easy to use and powerful [database migration system](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/). ABP Framework [startup templates](Startup-Templates/Index.md) take the advantage of this system to allow you to develop your application in a standard way. |
|||
|
|||
However, EF Core migration system is **not so good in a modular environment** where each module maintains its **own database schema** while two or more modules may **share a single database** in practical. |
|||
|
|||
Since ABP Framework cares about modularity in all aspects, it provides a **solution** to this problem. It is important to understand this solution if you need to **customize your database structure**. |
|||
|
|||
> See [EF Core's own documentation](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to fully learn the EF Core Code First Migrations and why you need to such a system. |
|||
|
|||
## The Default Solution & Database Configuration |
|||
|
|||
When you [create a new web application](https://abp.io/get-started) (with EF Core, which is the default database provider), your solution structure will be similar to the picture below: |
|||
|
|||
 |
|||
|
|||
> Actual solution structure may be a bit different based on your preferences, but the database part will be same. |
|||
|
|||
### The Database Structure |
|||
|
|||
The startup template has some [application modules](Modules/Index.md) pre-installed. Each layer of the solution has corresponding module package references. So, the `.EntityFrameworkCore` project has the NuGet references for the `.EntityFrameworkCore` packages of the used modules: |
|||
|
|||
 |
|||
|
|||
In this way, you collect all the EF Core dependencies under the `.EntityFrameworkCore` project. |
|||
|
|||
> In addition to the module references, it references to the `Volo.Abp.EntityFrameworkCore.SqlServer` package since the startup template is pre-configured for the SQL Server. See the documentation if you want to [switch to another DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
While every module has its own `DbContext` class by design and can use its **own physical database**, the solution is configured to use a **single shared database** as shown in the figure below: |
|||
|
|||
 |
|||
|
|||
This is **the simplest configuration** and suitable for most of the applications. `appsettings.json` file has a **single connection string**, named `Default`: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "..." |
|||
} |
|||
```` |
|||
|
|||
So, you have a **single database schema** which contains all the tables of the modules **sharing** this database. |
|||
|
|||
ABP Framework's [connection string](Connection-Strings.md) system allows you to easily **set a different connection string** for a desired module: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "...", |
|||
"AbpAuditLogging": "..." |
|||
} |
|||
```` |
|||
|
|||
The example configuration about tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md). |
|||
|
|||
However, this is just the beginning. You also need to create the second database, create audit log tables inside it and maintain the database tables using the code first approach. One of the main purposes of this document is to guide you on such database separation scenarios. |
|||
|
|||
#### Module Tables |
|||
|
|||
Every module uses its own databases tables. For example, the [Identity Module](Modules/Identity.md) has some tables to manage the users and roles in the system. |
|||
|
|||
#### Table Prefixes |
|||
|
|||
Since it is allowed to share a single database by all modules (it is the default configuration), a module typically uses a prefix to group its own tables. |
|||
|
|||
The fundamental modules, like [Identity](Modules/Identity.md), [Tenant Management](Modules/Tenant-Management.md) and [Audit Logs](Modules/Audit-Logging.md), use the `Abp` prefix, while some other modules use their own prefixes. [Identity Server](Modules/IdentityServer.md) module uses the `IdentityServer` prefix for example. |
|||
|
|||
If you want, you can change the database table name prefix for a module for your application. Example: |
|||
|
|||
````csharp |
|||
Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; |
|||
```` |
|||
|
|||
This code changes the prefix of the [Identity Server](Modules/IdentityServer.md) module. Write this code at the very beginning in your application. |
|||
|
|||
> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage. |
|||
|
|||
### The Projects |
|||
|
|||
From the database point of view, there are three important projects those will be explained in the next sections. |
|||
|
|||
#### .EntityFrameworkCore Project |
|||
|
|||
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: |
|||
|
|||
````csharp |
|||
[ConnectionStringName("Default")] |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<AppUser> Users { get; set; } |
|||
|
|||
/* Add DbSet properties for your Aggregate Roots / Entities here. */ |
|||
|
|||
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Configure the shared tables (with included modules) here */ |
|||
|
|||
builder.Entity<AppUser>(b => |
|||
{ |
|||
//Sharing the same table "AbpUsers" with the IdentityUser |
|||
b.ToTable("AbpUsers"); |
|||
|
|||
//Configure base properties |
|||
b.ConfigureByConvention(); |
|||
b.ConfigureAbpUser(); |
|||
|
|||
//Moved customization of the "AbpUsers" table to an extension method |
|||
b.ConfigureCustomUserProperties(); |
|||
}); |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
builder.ConfigureBookStore(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This simple `DbContext` class still needs some explanations: |
|||
|
|||
* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`. |
|||
* It inherits from the `AbpDbContext<T>` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext<T>` base class implements some conventions of the ABP Framework to automate some common tasks for you. |
|||
* It declares a `DbSet` property for the `AppUser` entity. `AppUser` shares the same table (named `AbpUsers` by default) with the `IdentityUser` entity of the [Identity module](Modules/Identity.md). The startup template provides this entity inside the application since we think that the User entity is generally needs to be customized in your application. |
|||
* The constructor takes a `DbContextOptions<T>` instance. |
|||
* It overrides the `OnModelCreating` method to define the EF Core mappings. |
|||
* It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us. |
|||
* It then configures the mapping for the `AppUser` entity. There is a special case for this entity (it shares a table with the Identity module), which will be explained in the next sections. |
|||
* It finally calls the `builder.ConfigureBookStore()` extension method to configure other entities of your application. |
|||
|
|||
This design will be explained in more details after introducing the other database related projects. |
|||
|
|||
#### .EntityFrameworkCore.DbMigrations Project |
|||
|
|||
As mentioned in the previous section, every module (and your application) have **their own** separate `DbContext` classes. Each `DbContext` class only defines the entity to table mappings related to its own module and each module (and your application) use the related `DbContext` class **on runtime**. |
|||
|
|||
As you know, EF Core Code First migration system relies on a `DbContext` class **to track and generate** the code first migrations. So, which `DbContext` we should use for the migrations? The answer is *none of them*. There is another `DbContext` defined in the `.EntityFrameworkCore.DbMigrations` project (which is the `BookStoreMigrationsDbContext` for this example solution). |
|||
|
|||
##### The MigrationsDbContext |
|||
|
|||
The `MigrationsDbContext` is only used to create and apply the database migrations. It is **not used on runtime**. It **merges** all the entity to table mappings of all the used modules plus the application's mappings. |
|||
|
|||
In this way, you create and maintain a **single database migration path**. However, there are some difficulties of this approach and the next sections explains how ABP Framework overcomes these difficulties. But first, see the `BookStoreMigrationsDbContext` class as an example: |
|||
|
|||
````csharp |
|||
/* This DbContext is only used for database migrations. |
|||
* It is not used on runtime. See BookStoreDbContext for the runtime DbContext. |
|||
* It is a unified model that includes configuration for |
|||
* all used modules and your application. |
|||
*/ |
|||
public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbContext> |
|||
{ |
|||
public BookStoreMigrationsDbContext( |
|||
DbContextOptions<BookStoreMigrationsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
builder.ConfigurePermissionManagement(); |
|||
builder.ConfigureSettingManagement(); |
|||
builder.ConfigureBackgroundJobs(); |
|||
builder.ConfigureAuditLogging(); |
|||
builder.ConfigureIdentity(); |
|||
builder.ConfigureIdentityServer(); |
|||
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(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
##### Sharing the Mapping Code |
|||
|
|||
First problem is that: A module uses its own `DbContext` which needs to the database mappings. The `MigrationsDbContext` also needs to the same mapping in order to create the database tables for this module. We definitely don't want to duplicate the mapping code. |
|||
|
|||
The solution is to define an extension method (on the `ModelBuilder`) that can be called by both `DbContext` classes. So, every module defines such an extension method. |
|||
|
|||
For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that: |
|||
|
|||
````csharp |
|||
public static class BackgroundJobsDbContextModelCreatingExtensions |
|||
{ |
|||
public static void ConfigureBackgroundJobs( |
|||
this ModelBuilder builder, |
|||
Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null) |
|||
{ |
|||
var options = new BackgroundJobsModelBuilderConfigurationOptions( |
|||
BackgroundJobsDbProperties.DbTablePrefix, |
|||
BackgroundJobsDbProperties.DbSchema |
|||
); |
|||
|
|||
optionsAction?.Invoke(options); |
|||
|
|||
builder.Entity<BackgroundJobRecord>(b => |
|||
{ |
|||
b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); |
|||
|
|||
b.ConfigureCreationTime(); |
|||
b.ConfigureExtraProperties(); |
|||
|
|||
b.Property(x => x.JobName) |
|||
.IsRequired() |
|||
.HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); |
|||
|
|||
//... |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This extension method also gets options to change the database table prefix and schema for this module, but it is not important here. |
|||
|
|||
The final application calls the extension methods inside the `MigrationsDbContext` class, so it can decide which modules are included to the database maintained by this `MigrationsDbContext`. If you want to create a second database and move some module tables to the second database, then you need to have a second `MigrationsDbContext` class which only calls the extension methods of the related modules. This topic will be detailed in the next sections. |
|||
|
|||
The same `ConfigureBackgroundJobs` method is also called the `DbContext` of the Background Jobs module: |
|||
|
|||
````csharp |
|||
[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] |
|||
public class BackgroundJobsDbContext |
|||
: AbpDbContext<BackgroundJobsDbContext>, IBackgroundJobsDbContext |
|||
{ |
|||
public DbSet<BackgroundJobRecord> BackgroundJobs { get; set; } |
|||
|
|||
public BackgroundJobsDbContext(DbContextOptions<BackgroundJobsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
//Reuse the same extension method! |
|||
builder.ConfigureBackgroundJobs(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
In this way, the mapping configuration of a module can be shared between `DbContext` classes. |
|||
|
|||
##### Reusing a Table of a Module |
|||
|
|||
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. |
|||
2. You can create a new entity mapping to the same database table. |
|||
|
|||
###### Use the Entity Defined by a Module |
|||
|
|||
Using an entity defined a module is pretty easy and standard. For example, Identity module defines the `IdentityUser` entity. You can inject the [repository](Repositories.md) for the `IdentityUser` and perform the standard repository operations for this entity. Example: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IRepository<IdentityUser, Guid> _identityUserRepository; |
|||
|
|||
public MyService(IRepository<IdentityUser, Guid> identityUserRepository) |
|||
{ |
|||
_identityUserRepository = identityUserRepository; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
//Get all users |
|||
var users = await _identityUserRepository.GetListAsync(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This example injects the `IRepository<IdentityUser, Guid>` (default repository) which defines the standard repository methods and implements the `IQueryable` interface. |
|||
|
|||
> In addition, Identity module defines the `IIdentityUserRepository` (custom repository) that can also be injected and used by your application. `IIdentityUserRepository` provides additional custom methods for the `IdentityUser` entity while it does not implement the `IQueryable` interface. |
|||
|
|||
###### Create a New Entity |
|||
|
|||
Working with an entity of a module is easy if you want to use the entity as is. However, you may want to define your own entity class and map to the same database table in the following cases; |
|||
|
|||
* You want to add a new field to the table and map it to a property in the entity. You can't use the module's entity since it doesn't have the related property. |
|||
* You want to use a subset of the table fields. You don't want to access to all properties of the entity and hide the unrelated properties (from a security perspective or just by design). |
|||
* You don't want to directly depend on a module entity class. |
|||
|
|||
In any case, the progress is same. Assume that you want to create an entity, named `AppRole`, mapped to the same table of the `IdentityRole` entity of the [Identity module](Modules/Identity.md). |
|||
|
|||
Here, we will show the implementation, then **will discuss the limitations** (and reasons of the limitations) of this approach. |
|||
|
|||
First, create a new `AppRole` class in your `.Domain` project: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.MultiTenancy; |
|||
|
|||
namespace Acme.BookStore.Roles |
|||
{ |
|||
public class AppRole : AggregateRoot<Guid>, IMultiTenant |
|||
{ |
|||
// Properties shared with the IdentityRole class |
|||
|
|||
public Guid? TenantId { get; private set; } |
|||
public virtual string Name { get; protected internal set; } |
|||
|
|||
//Additional properties |
|||
|
|||
public string Title { get; set; } |
|||
|
|||
private AppRole() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* It's inherited from [the `AggregateRoot<Guid>` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does the same. |
|||
* You can add any properties defined by the `IdentityRole` entity. This examples add only the `TenantId` and `Name` properties since we only need them here. |
|||
* You can add custom (additional) properties. This example adds the `Title` property. |
|||
* The constructor is provide, so it is not allowed to directly create a new `AppRole` entity. Creating a role is a responsibility of the Identity module. You can query roles, set/update your custom properties, but you should not create or delete a role in your code, as a best practice (while there is nothing restricts you). |
|||
|
|||
Now, it is time to define the EF Core mappings. Open the `DbContext` of your application (`BookStoreDbContext` in this sample) and add the following property: |
|||
|
|||
````csharp |
|||
public DbSet<AppRole> Roles { get; set; } |
|||
```` |
|||
|
|||
Then configure the mapping inside the `OnModelCreating` method (after calling the `base.OnModelCreating(builder)`): |
|||
|
|||
````csharp |
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Configure the shared tables (with included modules) here */ |
|||
|
|||
//CONFIGURE THE AppRole ENTITY |
|||
builder.Entity<AppRole>(b => |
|||
{ |
|||
b.ToTable("AbpRoles"); |
|||
|
|||
b.ConfigureByConvention(); |
|||
|
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
|
|||
... |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
|
|||
builder.ConfigureBookStore(); |
|||
} |
|||
```` |
|||
|
|||
We added the following lines: |
|||
|
|||
````csharp |
|||
builder.Entity<AppRole>(b => |
|||
{ |
|||
b.ToTable("AbpRoles"); |
|||
|
|||
b.ConfigureByConvention(); |
|||
|
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
```` |
|||
|
|||
* 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. |
|||
|
|||
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: |
|||
|
|||
````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 */ |
|||
|
|||
//CONFIGURE THE CUSTOM ROLE PROPERTIES |
|||
builder.Entity<IdentityRole>(b => |
|||
{ |
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
|
|||
... |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
|
|||
builder.ConfigureBookStore(); |
|||
} |
|||
```` |
|||
|
|||
Only added the following lines: |
|||
|
|||
````csharp |
|||
builder.Entity<IdentityRole>(b => |
|||
{ |
|||
b.ConfigureCustomRoleProperties(); |
|||
}); |
|||
```` |
|||
|
|||
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. |
|||
|
|||
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): |
|||
|
|||
 |
|||
|
|||
This command will create a new code first migration class as shown below: |
|||
|
|||
````csharp |
|||
public partial class Added_Title_To_Roles : Migration |
|||
{ |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AddColumn<string>( |
|||
name: "Title", |
|||
table: "AbpRoles", |
|||
maxLength: 128, |
|||
nullable: true); |
|||
} |
|||
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "Title", |
|||
table: "AbpRoles"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
All done! Just run the `Update-Database` command in the PMC or run the `.DbMigrator` project in your solution to apply changes to database. |
|||
|
|||
##### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path |
|||
|
|||
TODO |
|||
@ -0,0 +1,4 @@ |
|||
# Identity Management Module |
|||
|
|||
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon... |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
# Tenant Management Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,6 @@ |
|||
# Tutorials |
|||
|
|||
## Application Development |
|||
|
|||
* [With ASP.NET Core MVC / Razor Pages UI](AspNetCore-Mvc/Part-I.md) |
|||
* [With Angular UI](Angular/Part-I.md) |
|||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,320 @@ |
|||
|
|||
# EF核心高级数据库迁移 |
|||
|
|||
本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论您可能希望为自己的应用程序实现的**各种场景**. |
|||
|
|||
> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). |
|||
|
|||
## 关于EF Core 代码优先迁移 |
|||
|
|||
Entity Framework Core 提供了一种简单强大[数据库迁移系统](https://docs.microsoft.com/zh-cn/ef/core/managing-schemas/migrations/). ABP框架[启动模板](Startup-Templates/Index.md)使用这个系统,让你以标准的方式开发你的应用程序. |
|||
|
|||
但是EF Core迁移系统在[模块化环境中不是很好],在模块化环境中,每个模块都维护**自己的数据库架构**,而实际上两个或多个模块可以**共享一个数据库**. |
|||
|
|||
由于ABP框架在所有方面都关心模块化,所以它为这个问题提供了**解决方案**. 如果你需要**自定义数据库结构**,那么应当了解这个解决方案. |
|||
|
|||
> 参阅[EF Core文档](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)充分了解EF Core Code First迁移,以及为什么需要这样的系统. |
|||
|
|||
## 默认解决方案与数据库配置 |
|||
|
|||
当你[创建一个新的Web应用程序](https://abp.io/get-started)(使用EF Core,它是默认的数据库提供程序),你的解决方案结构类似下图: |
|||
|
|||
 |
|||
|
|||
> 实际的解决方案结构可能会根据你的偏好有所不同,但是数据库部分是相同的. |
|||
|
|||
### 数据库架构 |
|||
|
|||
启动模板已预安装了一些[应用程序模块](Modules/Index.md). 解决方案的每一层都有相应的模块包引用. 所以 `.EntityFrameworkCore` 项目含有使用 `EntityFrameworkCore` 模块的Nuget的引用: |
|||
|
|||
 |
|||
|
|||
通过这种方式,你可以看到所有的`.EntityFrameworkCore`项目下的EF Core的依赖. |
|||
|
|||
> 除了模块引用之外,它还引用了 `Volo.Abp.EntityFrameworkCore.SqlServer` 包,因为启动模板预配置的是Sql Server. 参阅文档了解如何[切换到其它DBMS](Entity-Framework-Core-Other-DBMS.md). |
|||
|
|||
虽然每个模块在设计上有自己的`DbContext`类,并且可以使用其自己的**物理数据库**,但解决方案的配置是使用**单个共享数据库**如下图所示: |
|||
|
|||
 |
|||
|
|||
这是**最简单的配置**,适用于大部分的应用程序. `appsettings.json` 文件有名为`Default`**单个连接字符串**: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "..." |
|||
} |
|||
```` |
|||
|
|||
所以你有一个**单一的数据库模式**,其中包含**共享**此数据库的模块的所有表. |
|||
|
|||
ABP框架的[连接字符串](Connection-Strings.md)系统允许你轻松为所需的模块**设置不同的连接字符串**: |
|||
|
|||
````json |
|||
"ConnectionStrings": { |
|||
"Default": "...", |
|||
"AbpAuditLogging": "..." |
|||
} |
|||
```` |
|||
|
|||
示例配置告诉ABP框架[审计日志模块](Modules/Audit-Logging.md)应使用第二个连接字符串. |
|||
|
|||
然而这仅仅只是开始. 你还需要创建第二个数据库以及里面审计日志表并使用code frist的方法维护数据库表. 本文档的主要目的之一就是指导你了解这样的数据库分离场景. |
|||
|
|||
#### 模块表 |
|||
|
|||
每个模块都使用自己的数据库表. 例如[身份模块](Modules/Identity.md)有一些表来管理系统中的用户和角色. |
|||
|
|||
#### 表前缀 |
|||
|
|||
由于所有模块都允许共享一个数据库(这是默认配置),所以模块通常使用前缀来对自己的表进行分组. |
|||
|
|||
基础模块(如[身份](Modules/Identity.md), [租户管理](Modules/Tenant-Management.md) 和 [审计日志](Modules/Audit-Logging.md))使用 `Abp` 前缀, 其他的模块使用自己的前缀. 如[Identity Server](Modules/IdentityServer.md) 模块使用前缀 `IdentityServer`. |
|||
|
|||
如果你愿意,你可以为你的应用程序的模块更改数据库表前缀. |
|||
例: |
|||
|
|||
````csharp |
|||
Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids"; |
|||
```` |
|||
|
|||
这段代码更改了[Identity Server](Modules/IdentityServer.md)的前缀. 在应用程序的最开始编写这段代码. |
|||
|
|||
> 每个模块还定义了 `DbSchema` 属性,你可以在支持schema的数据库中使用它. |
|||
|
|||
### 项目 |
|||
|
|||
从数据库的角度来看.有三个重要的项目将在下一节中解释. |
|||
|
|||
#### .EntityFrameworkCore 项目 |
|||
|
|||
这个项目有应用程序的 `DbContext`类(本例中的 `BookStoreDbContex` ). |
|||
|
|||
每个模块都使用自己的 `DbContext` 类来访问数据库。同样你的应用程序有它自己的 `DbContext`. 通常在应用程序中使用这个 `DbContet`(如果你遵循最佳实践,应该在自定义[仓储](Repositories.md)中使用). 它几乎是一个空的 `DbContext`,因为你的应用程序在一开始没有任何实体,除了预定义的 `AppUser` 实体: |
|||
|
|||
````csharp |
|||
[ConnectionStringName("Default")] |
|||
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> |
|||
{ |
|||
public DbSet<AppUser> Users { get; set; } |
|||
|
|||
/* Add DbSet properties for your Aggregate Roots / Entities here. */ |
|||
|
|||
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Configure the shared tables (with included modules) here */ |
|||
|
|||
builder.Entity<AppUser>(b => |
|||
{ |
|||
//Sharing the same table "AbpUsers" with the IdentityUser |
|||
b.ToTable("AbpUsers"); |
|||
|
|||
//Configure base properties |
|||
b.ConfigureByConvention(); |
|||
b.ConfigureAbpUser(); |
|||
|
|||
//Moved customization of the "AbpUsers" table to an extension method |
|||
b.ConfigureCustomUserProperties(); |
|||
}); |
|||
|
|||
/* Configure your own tables/entities inside the ConfigureBookStore method */ |
|||
builder.ConfigureBookStore(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
这个简单的 `DbContext` 类仍然需要一些解释: |
|||
|
|||
* 它定义了一个 `[connectionStringName]` Attribute,它告诉ABP始终为此 `Dbcontext` 使用 `Default` 连接字符串. |
|||
* 它从 `AbpDbContext<T>` 而不是标准的 `DbContext` 类继承. 你可以参阅[EF Core集成](Entity-Framework-Core.md)文档了解更多. 现在你需要知道 `AbpDbContext<T>` 基类实现ABP框架的一些约定,为你自动化一些常见的任务. |
|||
* 它为 `AppUser` 实体定义了 `DbSet` 属性. `AppUser` 与[身份模块]的 `IdentityUser` 实体共享同一个表(默认名为 `AbpUsers`). 启动模板在应用程序中提供这个实体,因为我们认为用户实体一般需要应用程序中进行定制. |
|||
* 构造函数接受一个 `DbContextOptions<T>` 实例. |
|||
* 它覆盖了 `OnModelCreating` 方法定义EF Core 映射. |
|||
* 首先调用 `base.OnModelCreating` 方法让ABP框架为我们实现基础映射. |
|||
* 然后它配置了 `AppUser` 实体的映射. 这个实体有一个特殊的情况(它与Identity模块共享一个表),在下一节中进行解释. |
|||
* 最后它调用 `builder.ConfigureBookStore()` 扩展方法来配置应用程序的其他实体. |
|||
|
|||
在介绍其他数据库相关项目之后,将更详细地说明这个设计. |
|||
|
|||
#### .EntityFrameworkCore.DbMigrations 项目 |
|||
|
|||
正如前面所提到的,每个模块(和你的应用程序)有**它们自己**独立的 `DbContext` 类. 每个 `DbContext` 类只定义了自身模块的实体到表的映射,每个模块(包括你的应用程序)在**运行时**都使用相关的 `DbContext` 类. |
|||
|
|||
如你所知,EF Core Code First迁移系统依赖于 `DbContext` 类来跟踪和生成Code First迁移. 那么我们应该使用哪个 `DbContext` 进行迁移? 答案是它们都不是. `.EntityFrameworkCore.DbMigrations` 项目中定义了另一个 `DbContext` (示例解决方案中的 `BookStoreMigrationsDbContext`). |
|||
|
|||
##### MigrationsDbContext |
|||
|
|||
`MigrationsDbContext` 仅用于创建和应用数据库迁移. **不在运行时使用**. 它将所有使用的模块的所有实体到表的映射以及应用程序的映射**合并**. |
|||
|
|||
通过这种方式你可以创建和维护**单个数据库迁移路径**. 然而这种方法有一些困难,接下来的章节将解释ABP框架如何克服这些困难. 首先以 `BookStoreMigrationsDbContext` 类为例: |
|||
|
|||
````csharp |
|||
/* This DbContext is only used for database migrations. |
|||
* It is not used on runtime. See BookStoreDbContext for the runtime DbContext. |
|||
* It is a unified model that includes configuration for |
|||
* all used modules and your application. |
|||
*/ |
|||
public class BookStoreMigrationsDbContext : AbpDbContext<BookStoreMigrationsDbContext> |
|||
{ |
|||
public BookStoreMigrationsDbContext( |
|||
DbContextOptions<BookStoreMigrationsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
/* Include modules to your migration db context */ |
|||
builder.ConfigurePermissionManagement(); |
|||
builder.ConfigureSettingManagement(); |
|||
builder.ConfigureBackgroundJobs(); |
|||
builder.ConfigureAuditLogging(); |
|||
builder.ConfigureIdentity(); |
|||
builder.ConfigureIdentityServer(); |
|||
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(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
##### 共享映射代码 |
|||
|
|||
第一个问题是: 一个模块使用自己的 `DbContext` 这就需要到数据库的映射. 该 `MigrationsDbContext` 也需要相同的映射创建此模块的数据库表. 我们绝对不希望复制的映射代码. |
|||
|
|||
解决方案是定义一个扩展方法(在`ModelBuilder`)由两个 `DbContext` 类调用. 所以每个模块都定义了这样的扩展方法. |
|||
|
|||
For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that: |
|||
|
|||
例如,`builder.ConfigureBackgroundJobs()` 方法调用[后台作业模块]配置数据库表. 扩展方法的定义如下: |
|||
|
|||
````csharp |
|||
public static class BackgroundJobsDbContextModelCreatingExtensions |
|||
{ |
|||
public static void ConfigureBackgroundJobs( |
|||
this ModelBuilder builder, |
|||
Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null) |
|||
{ |
|||
var options = new BackgroundJobsModelBuilderConfigurationOptions( |
|||
BackgroundJobsDbProperties.DbTablePrefix, |
|||
BackgroundJobsDbProperties.DbSchema |
|||
); |
|||
|
|||
optionsAction?.Invoke(options); |
|||
|
|||
builder.Entity<BackgroundJobRecord>(b => |
|||
{ |
|||
b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema); |
|||
|
|||
b.ConfigureCreationTime(); |
|||
b.ConfigureExtraProperties(); |
|||
|
|||
b.Property(x => x.JobName) |
|||
.IsRequired() |
|||
.HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength); |
|||
|
|||
//... |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
此扩展方法还获取选项用于更改此模块的数据库表前缀和模式,但在这里并不重要. |
|||
|
|||
最终的应用程序在 `MigrationsDbContext` 类中调用扩展方法, 因此它可以确定此 `MigrationsDbContext` 维护的数据库中包含哪些模块. 如果要创建第二个数据库并将某些模块表移动到第二个数据库,则需要有第二个`MigrationsDbContext` 类,该类仅调用相关模块的扩展方法. 下一部分将详细介绍该主题. |
|||
|
|||
同样 `ConfigureBackgroundJobs` 方法也被后台作业模块的 `DbContext` 调用: |
|||
|
|||
````csharp |
|||
[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)] |
|||
public class BackgroundJobsDbContext |
|||
: AbpDbContext<BackgroundJobsDbContext>, IBackgroundJobsDbContext |
|||
{ |
|||
public DbSet<BackgroundJobRecord> BackgroundJobs { get; set; } |
|||
|
|||
public BackgroundJobsDbContext(DbContextOptions<BackgroundJobsDbContext> options) |
|||
: base(options) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
//Reuse the same extension method! |
|||
builder.ConfigureBackgroundJobs(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
以这种方式,可以在 `DbContext` 类之间共享模块的映射配置. |
|||
|
|||
##### 重用模块的表 |
|||
|
|||
您可能想在应用程序中重用依赖模块的表. 在这种情况下你有两个选择: |
|||
|
|||
1. 你可以直接使用模块定义的实体. |
|||
2. 你可以创建一个新的实体映射到同一个数据库表。 |
|||
|
|||
###### 使用由模块定义的实体 |
|||
|
|||
使用实体定义的模块有标准用法非常简单. 例如身份模块定义了 `IdentityUser` 实体. 你可以为注入 `IdentityUser` 仓储,为此实体执行标准仓储操作. |
|||
例: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Identity; |
|||
|
|||
namespace Acme.BookStore |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IRepository<IdentityUser, Guid> _identityUserRepository; |
|||
|
|||
public MyService(IRepository<IdentityUser, Guid> identityUserRepository) |
|||
{ |
|||
_identityUserRepository = identityUserRepository; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
//Get all users |
|||
var users = await _identityUserRepository.GetListAsync(); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
示例注入了 `IRepository<IdentityUser,Guid>`(默认仓储). 它定义了标准的存储库方法并实现了 `IQueryable` 接口. |
|||
|
|||
另外,身份模块定义了 `IIdentityUserRepository`(自定义仓储), 你的应用程序也可以注入和使用它. `IIdentityUserRepository` 为 `IdentityUser` 实体提供了额外的定制方法,但它没有实现 `IQueryable`. |
|||
|
|||
###### 创建一个新的实体 |
|||
|
|||
TODO |
|||
|
|||
##### 讨论另一种场景:每个模块管理自己的迁移路径 |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# 身份管理模块 |
|||
|
|||
参阅 [源码](https://github.com/abpframework/abp/tree/dev/modules/identity). 文档很快会被完善. |
|||
@ -0,0 +1,3 @@ |
|||
# 租户管理模块 |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
## Angular 教程 - 第一章 |
|||
|
|||
TODO... |
|||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,6 @@ |
|||
# 教程 |
|||
|
|||
## 应用开发 |
|||
|
|||
* [使用ASP.NET Core MVC/ Razor Pages UI](AspNetCore-Mvc/Part-I.md) |
|||
* [使用Angular UI](Angular/Part-I.md) |
|||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,19 @@ |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcUiThemeSharedModule) |
|||
)] |
|||
public class AbpAspNetCoreMvcUiThemeSharedDemoModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpAspNetCoreMvcUiThemeSharedDemoModule>("Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo"); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,16 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo |
|||
{ |
|||
[Widget] |
|||
public class ButtonsDemoViewComponent : AbpViewComponent |
|||
{ |
|||
public const string ViewPath = "/Views/Components/Themes/Shared/Demos/ButtonsDemo/Default.cshtml"; |
|||
|
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View(ViewPath); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo |
|||
|
|||
<abp-component-demo-section title="Basics" view-path="@ButtonsDemoViewComponent.ViewPath"> |
|||
<abp-button text="Default" /> |
|||
<abp-button button-type="Primary" text="Primary" /> |
|||
<abp-button button-type="Secondary">Secondary</abp-button> |
|||
<abp-button button-type="Success">Success</abp-button> |
|||
<abp-button button-type="Danger">Danger</abp-button> |
|||
<abp-button button-type="Warning">Warning</abp-button> |
|||
<abp-button button-type="Info">Info</abp-button> |
|||
<abp-button button-type="Light">Light</abp-button> |
|||
<abp-button button-type="Dark">Dark</abp-button> |
|||
<abp-button button-type="Link">Link</abp-button> |
|||
</abp-component-demo-section> |
|||
|
|||
<abp-component-demo-section title="Outline" view-path="@ButtonsDemoViewComponent.ViewPath"> |
|||
<abp-button button-type="Outline_Primary">Primary</abp-button> |
|||
<abp-button button-type="Outline_Secondary">Secondary</abp-button> |
|||
<abp-button button-type="Outline_Success">Success</abp-button> |
|||
<abp-button button-type="Outline_Danger">Danger</abp-button> |
|||
<abp-button button-type="Outline_Warning">Warning</abp-button> |
|||
<abp-button button-type="Outline_Info">Info</abp-button> |
|||
<abp-button button-type="Outline_Light">Light</abp-button> |
|||
<abp-button button-type="Outline_Dark">Dark</abp-button> |
|||
</abp-component-demo-section> |
|||
|
|||
<abp-component-demo-section title="Icons" view-path="@ButtonsDemoViewComponent.ViewPath"> |
|||
<abp-button button-type="Warning" icon="pencil" text="Edit" /> |
|||
<abp-button button-type="Info" icon-type="FontAwesome" icon="info" text="Information" /> |
|||
</abp-component-demo-section> |
|||
@ -0,0 +1,105 @@ |
|||
using System; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Razor.TagHelpers; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.TagHelpers |
|||
{ |
|||
public class AbpComponentDemoSectionTagHelper : AbpTagHelper |
|||
{ |
|||
private const string DemoSectionOpeningTag = "<abp-component-demo-section"; |
|||
private const string DemoSectionClosingTag = "</abp-component-demo-section"; |
|||
|
|||
public string ViewPath { get; set; } |
|||
public string Title { get; set; } |
|||
|
|||
private readonly IVirtualFileProvider _virtualFileProvider; |
|||
|
|||
public AbpComponentDemoSectionTagHelper(IVirtualFileProvider virtualFileProvider) |
|||
{ |
|||
_virtualFileProvider = virtualFileProvider; |
|||
} |
|||
|
|||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) |
|||
{ |
|||
output.TagName = null; |
|||
|
|||
var content = await output.GetChildContentAsync(); |
|||
|
|||
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section\">"); |
|||
output.PreContent.AppendHtml($"<h2>{Title}</h2>"); |
|||
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section-body\">"); |
|||
/* component rendering here */ |
|||
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section-body
|
|||
AppendRawSource(output); |
|||
AppendBootstrapSource(output, content); |
|||
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section
|
|||
} |
|||
|
|||
private static void AppendBootstrapSource(TagHelperOutput output, TagHelperContent content) |
|||
{ |
|||
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-bs-source\">"); |
|||
output.PostContent.AppendHtml("<h3>Bootstrap</h3>"); |
|||
output.PostContent.AppendHtml("<pre>"); |
|||
output.PostContent.Append(content.GetContent()); |
|||
output.PostContent.AppendHtml("</pre>"); |
|||
output.PostContent.AppendHtml("</div>"); |
|||
} |
|||
|
|||
private void AppendRawSource(TagHelperOutput output) |
|||
{ |
|||
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-raw-source\">"); |
|||
output.PostContent.AppendHtml("<h3>ABP Tag Helpers</h3>"); |
|||
output.PostContent.AppendHtml("<pre>"); |
|||
output.PostContent.Append(GetRawDemoSource()); |
|||
output.PostContent.AppendHtml("</pre>"); |
|||
output.PostContent.AppendHtml("</div>"); |
|||
} |
|||
|
|||
private string GetRawDemoSource() |
|||
{ |
|||
StringBuilder sourceBuilder = null; |
|||
|
|||
var lines = GetFileContent().SplitToLines(); |
|||
|
|||
foreach (var line in lines) |
|||
{ |
|||
if (line.Contains(DemoSectionOpeningTag) && GetName(line) == Title) |
|||
{ |
|||
sourceBuilder = new StringBuilder(); |
|||
} |
|||
else if (line.Contains(DemoSectionClosingTag, StringComparison.InvariantCultureIgnoreCase)) |
|||
{ |
|||
if (sourceBuilder == null) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
return sourceBuilder.ToString(); |
|||
} |
|||
else if (sourceBuilder != null) |
|||
{ |
|||
sourceBuilder.AppendLine(line); |
|||
} |
|||
} |
|||
|
|||
throw new AbpException($"Could not find {Title} demo section inside {ViewPath}"); |
|||
} |
|||
|
|||
private string GetFileContent() |
|||
{ |
|||
var viewFileInfo = _virtualFileProvider.GetFileInfo(ViewPath); |
|||
return viewFileInfo.ReadAsString(); |
|||
} |
|||
|
|||
private string GetName(string line) |
|||
{ |
|||
var str = line.Substring(line.IndexOf("title=\"", StringComparison.OrdinalIgnoreCase) + "title=\"".Length); |
|||
str = str.Left(str.IndexOf("\"", StringComparison.OrdinalIgnoreCase)); |
|||
return str; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
@using System.Globalization |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo |
|||
@ -0,0 +1,34 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Razor"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc> |
|||
<AssemblyName>Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo</AssemblyName> |
|||
<PackageId>Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<IsPackable>true</IsPackable> |
|||
<OutputType>Library</OutputType> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="Views\**\*.cshtml" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Content Remove="Views\**\*.cshtml" /> |
|||
<Content Remove="compilerconfig.json" /> |
|||
<Content Remove="Properties\launchSettings.json" /> |
|||
<None Include="Properties\launchSettings.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Authorization |
|||
{ |
|||
public class AlwaysAllowMethodInvocationAuthorizationService : IMethodInvocationAuthorizationService |
|||
{ |
|||
public Task CheckAsync(MethodInvocationAuthorizationContext context) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Volo.Abp.Cli.Licensing; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands.Services |
|||
{ |
|||
public class AbpNuGetIndexUrlService : ITransientDependency |
|||
{ |
|||
private readonly IApiKeyService _apiKeyService; |
|||
public ILogger<AbpNuGetIndexUrlService> Logger { get; set; } |
|||
|
|||
public AbpNuGetIndexUrlService(IApiKeyService apiKeyService) |
|||
{ |
|||
_apiKeyService = apiKeyService; |
|||
Logger = NullLogger<AbpNuGetIndexUrlService>.Instance; |
|||
} |
|||
|
|||
public async Task<string> GetAsync() |
|||
{ |
|||
var apiKeyResult = await _apiKeyService.GetApiKeyOrNullAsync(); |
|||
|
|||
if (apiKeyResult == null) |
|||
{ |
|||
Logger.LogWarning("You are not signed in! Use the CLI command \"abp login <username>\" to sign in, then try again."); |
|||
return null; |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(apiKeyResult.ErrorMessage)) |
|||
{ |
|||
Logger.LogWarning(apiKeyResult.ErrorMessage); |
|||
return null; |
|||
} |
|||
|
|||
if (string.IsNullOrEmpty(apiKeyResult.ApiKey)) |
|||
{ |
|||
Logger.LogError("Couldn't retrieve your NuGet API key! You can re-sign in with the CLI command \"abp login <username>\"."); |
|||
return null; |
|||
} |
|||
|
|||
return CliUrls.GetNuGetServiceIndexUrl(apiKeyResult.ApiKey); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System.IO; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo; |
|||
using Volo.Abp.Autofac; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.UI; |
|||
using Volo.Abp.UI.Navigation; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpAspNetCoreMvcUiBasicThemeModule), |
|||
typeof(AbpAspNetCoreMvcUiThemeSharedDemoModule), |
|||
typeof(AbpAutofacModule) |
|||
)] |
|||
public class AbpAspNetCoreMvcUiThemeBasicDemoModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var env = context.Services.GetHostingEnvironment(); |
|||
|
|||
if (env.IsDevelopment()) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.ReplaceEmbeddedByPhysical<AbpAspNetCoreMvcUiThemeSharedDemoModule>(Path.Combine(env.ContentRootPath, string.Format("..{0}..{0}src{0}Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo", Path.DirectorySeparatorChar))); |
|||
}); |
|||
} |
|||
|
|||
Configure<AbpBundlingOptions>(options => |
|||
{ |
|||
options.StyleBundles |
|||
.Get(StandardBundles.Styles.Global) |
|||
.AddFiles("/demo/styles/main.css"); |
|||
}); |
|||
|
|||
Configure<AbpNavigationOptions>(options => |
|||
{ |
|||
options.MenuContributors.Add(new BasicThemeDemoMenuContributor()); |
|||
}); |
|||
} |
|||
|
|||
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
|||
{ |
|||
var app = context.GetApplicationBuilder(); |
|||
var env = context.GetEnvironment(); |
|||
|
|||
if (env.IsDevelopment()) |
|||
{ |
|||
app.UseDeveloperExceptionPage(); |
|||
} |
|||
|
|||
app.UseVirtualFiles(); |
|||
app.UseRouting(); |
|||
app.UseMvcWithDefaultRouteAndArea(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.UI.Navigation; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo |
|||
{ |
|||
public class BasicThemeDemoMenuContributor : IMenuContributor |
|||
{ |
|||
public Task ConfigureMenuAsync(MenuConfigurationContext context) |
|||
{ |
|||
if(context.Menu.Name == StandardMenus.Main) |
|||
{ |
|||
AddMainMenuItems(context); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
private void AddMainMenuItems(MenuConfigurationContext context) |
|||
{ |
|||
context.Menu.AddItem( |
|||
new ApplicationMenuItem("BasicThemeDemo.Components", "Components") |
|||
.AddItem( |
|||
new ApplicationMenuItem("BasicThemeDemo.Components.Buttons", "Buttons", url: "/Components/Buttons") |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
@page |
|||
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.Demos.ButtonsDemo |
|||
@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Buttons.IndexModel |
|||
<h1>Buttons</h1> |
|||
|
|||
@await Component.InvokeAsync(typeof(ButtonsDemoViewComponent)) |
|||
@ -0,0 +1,12 @@ |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Buttons |
|||
{ |
|||
public class IndexModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
@page |
|||
@model Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.IndexModel |
|||
<h1>Basic Theme Demo</h1> |
|||
@ -0,0 +1,12 @@ |
|||
using Microsoft.AspNetCore.Mvc.RazorPages; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages |
|||
{ |
|||
public class IndexModel : PageModel |
|||
{ |
|||
public void OnGet() |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
@using System.Globalization |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Hosting; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Serilog; |
|||
using Serilog.Events; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo |
|||
{ |
|||
public class Program |
|||
{ |
|||
public static int Main(string[] args) |
|||
{ |
|||
Log.Logger = new LoggerConfiguration() |
|||
.MinimumLevel.Debug() |
|||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) |
|||
.Enrich.FromLogContext() |
|||
.WriteTo.File("Logs/logs.txt") |
|||
.CreateLogger(); |
|||
|
|||
try |
|||
{ |
|||
Log.Information("Starting web host."); |
|||
CreateHostBuilder(args).Build().Run(); |
|||
return 0; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Log.Fatal(ex, "Host terminated unexpectedly!"); |
|||
return 1; |
|||
} |
|||
finally |
|||
{ |
|||
Log.CloseAndFlush(); |
|||
} |
|||
} |
|||
|
|||
|
|||
internal static IHostBuilder CreateHostBuilder(string[] args) => |
|||
Host.CreateDefaultBuilder(args) |
|||
.ConfigureWebHostDefaults(webBuilder => |
|||
{ |
|||
webBuilder.UseStartup<Startup>(); |
|||
}) |
|||
.UseAutofac() |
|||
.UseSerilog(); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
{ |
|||
"iisSettings": { |
|||
"windowsAuthentication": false, |
|||
"anonymousAuthentication": true, |
|||
"iisExpress": { |
|||
"applicationUrl": "http://localhost:61659", |
|||
"sslPort": 0 |
|||
} |
|||
}, |
|||
"profiles": { |
|||
"IIS Express": { |
|||
"commandName": "IISExpress", |
|||
"launchBrowser": true, |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
}, |
|||
"Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo": { |
|||
"commandName": "Project", |
|||
"launchBrowser": true, |
|||
"applicationUrl": "http://localhost:5000", |
|||
"environmentVariables": { |
|||
"ASPNETCORE_ENVIRONMENT": "Development" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo |
|||
{ |
|||
public class Startup |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddApplication<AbpAspNetCoreMvcUiThemeBasicDemoModule>(); |
|||
} |
|||
|
|||
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) |
|||
{ |
|||
app.InitializeApplication(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk.Web"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<PreserveCompilationReferences>true</PreserveCompilationReferences> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<Compile Remove="Logs\**" /> |
|||
<Content Remove="Logs\**" /> |
|||
<EmbeddedResource Remove="Logs\**" /> |
|||
<None Remove="Logs\**" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.1" /> |
|||
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> |
|||
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic\Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo\Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"Logging": { |
|||
"LogLevel": { |
|||
"Default": "Information", |
|||
"Microsoft": "Warning", |
|||
"Microsoft.Hosting.Lifetime": "Information" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"Logging": { |
|||
"LogLevel": { |
|||
"Default": "Information", |
|||
"Microsoft": "Warning", |
|||
"Microsoft.Hosting.Lifetime": "Information" |
|||
} |
|||
}, |
|||
"AllowedHosts": "*" |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
"use strict"; |
|||
|
|||
var gulp = require("gulp"), |
|||
path = require('path'), |
|||
copyResources = require('./node_modules/@abp/aspnetcore.mvc.ui/gulp/copy-resources.js'); |
|||
|
|||
exports.default = function(){ |
|||
return copyResources(path.resolve('./')); |
|||
}; |
|||
@ -0,0 +1,9 @@ |
|||
{ |
|||
"version": "1.0.0", |
|||
"name": "asp.net", |
|||
"private": true, |
|||
"dependencies": { |
|||
"@abp/aspnetcore.mvc.ui.theme.basic": "^2.1.0" |
|||
}, |
|||
"devDependencies": {} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
.abp-component-demo-section { |
|||
border: 1px solid #999; |
|||
padding: 10px; |
|||
} |
|||
|
|||
.abp-component-demo-section-body { |
|||
padding-bottom: 10px; |
|||
} |
|||
|
|||
.abp-component-demo-section-raw-source { |
|||
background-color: #eee; |
|||
padding: 5px; |
|||
} |
|||
|
|||
.abp-component-demo-section-raw-source pre { |
|||
border: 1px solid #999; |
|||
margin: 5px; |
|||
} |
|||
|
|||
|
|||
.abp-component-demo-section-bs-source { |
|||
background-color: #ddd; |
|||
padding: 5px; |
|||
} |
|||
|
|||
.abp-component-demo-section-bs-source pre { |
|||
border: 1px solid #999; |
|||
margin: 5px; |
|||
} |
|||
|
After Width: | Height: | Size: 699 KiB |