@ -0,0 +1,3 @@ |
|||
# Blog Storing |
|||
|
|||
TODO |
|||
@ -0,0 +1,296 @@ |
|||
# ABP Framework v2.9 Has Been Released |
|||
|
|||
The **ABP Framework** & and the **ABP Commercial** version 2.9 have been released, which are the last versions before v3.0! This post will cover **what's new** with these this release. |
|||
|
|||
## What's New with the ABP Framework 2.9? |
|||
|
|||
You can see all the changes on the [GitHub release notes](https://github.com/abpframework/abp/releases/tag/2.9.0). This post will only cover the important features/changes. |
|||
|
|||
### Pre-Compiling Razor Pages |
|||
|
|||
Pre-built pages (for [the application modules](https://docs.abp.io/en/abp/latest/Modules/Index)) and view components were compiling on runtime until this version. Now, they are pre-compiled and we've measured that the application startup time (especially for the MVC UI) has been reduced more than 50%. In other words, it is **two-times faster** than the previous version. The speed change also effects when you visit a page for the first time. |
|||
|
|||
Here, a test result for the startup application template with v2.8 and v.2.9: |
|||
|
|||
```` |
|||
### v2.8 |
|||
|
|||
2020-06-04 22:59:04.891 +08:00 [INF] Starting web host. |
|||
2020-06-04 22:59:07.662 +08:00 [INF] Now listening on: https://localhost:44391 |
|||
2020-06-04 22:59:17.315 +08:00 [INF] Request finished in 7756.6218ms 200 text/html; |
|||
|
|||
Total: 12.42s |
|||
|
|||
### v2.9 |
|||
|
|||
2020-06-04 22:59:13.720 +08:00 [INF] Starting web host. |
|||
2020-06-04 22:59:16.639 +08:00 [INF] Now listening on: https://localhost:44369 |
|||
2020-06-04 22:59:18.957 +08:00 [INF] Request finished in 1780.5461ms 200 text/html; |
|||
|
|||
Total: 5.24s |
|||
```` |
|||
|
|||
You do nothing to get the benefit of the new approach. [Overriding UI pages/components](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface) are also just working as before. We will be working on more performance improvements in the v3.0. |
|||
|
|||
### Organization Unit System |
|||
|
|||
[The Identity Module](https://docs.abp.io/en/abp/latest/Modules/Identity) now has the most requested feature: Organization Units! |
|||
|
|||
Organization unit system is used to create a hierarchical organization tree in your application. You can then use this organization tree to authorize data and functionality in your application. |
|||
|
|||
The documentation will come soon... |
|||
|
|||
### New Blob Storing Package |
|||
|
|||
We've created a new [Blob Storing package](https://www.nuget.org/packages/Volo.Abp.BlobStoring) to store arbitrary binary objects. It is generally used to store the content of the files in your application. This package provides an abstraction, so any application or [module](https://docs.abp.io/en/abp/latest/Module-Development-Basics) can save and retrieve files independent from the actual storing provider. |
|||
|
|||
There are two storage provider currently implemented: |
|||
|
|||
* [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) package stores objects/files in the local file system. |
|||
* [Volo.Abp.BlobStoring.Database](https://github.com/abpframework/abp/tree/dev/modules/blob-storing-database) module stores objects/files in a database. It currently supports [Entity Framework Core](https://docs.abp.io/en/abp/latest/Entity-Framework-Core) (so, you can use [any relational DBMS](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS)) and [MongoDB](https://docs.abp.io/en/abp/latest/MongoDB). |
|||
|
|||
[Azure BLOB provider](https://github.com/abpframework/abp/issues/4098) will be available with v3.0. You can request other cloud providers or contribute yourself on the [GitHub repository](https://github.com/abpframework/abp/issues/new). |
|||
|
|||
One of the benefits of the blob storing system is that it allows you to create multiple containers (each container is a blob storage) and use different storage providers for each container. |
|||
|
|||
**Example: Use the default container to save and get a byte array** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer _container; |
|||
|
|||
public MyService(IBlobContainer container) |
|||
{ |
|||
_container = container; |
|||
} |
|||
|
|||
public async Task FooAsync() |
|||
{ |
|||
//Save a BLOB |
|||
byte[] bytes = GetBytesFromSomeWhere(); |
|||
await _container.SaveAsync("my-unique-blob-name", bytes); |
|||
|
|||
//Retrieve a BLOB |
|||
bytes = await _container.GetAllBytesAsync("my-unique-blob-name"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
It can work with `byte[]` and `Stream` objects. |
|||
|
|||
**Example: Use a typed (named) container to save and get a stream** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer<TestContainer> _container; |
|||
|
|||
public MyService(IBlobContainer<TestContainer> container) |
|||
{ |
|||
_container = container; |
|||
} |
|||
|
|||
public async Task FooAsync() |
|||
{ |
|||
//Save a BLOB |
|||
Stream stream = GetStreamFromSomeWhere(); |
|||
await _container.SaveAsync("my-unique-blob-name", stream); |
|||
|
|||
//Retrieve a BLOB |
|||
stream = await _container.GetAsync("my-unique-blob-name"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`TestContainer` is an empty class that has no purpose than identifying the container: |
|||
|
|||
````csharp |
|||
[BlobContainerName("test")] //specifies the name of the container |
|||
public class TestContainer |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
A typed (named) container can be configured to use a different storing provider than the default one. It is a good practice to always use a typed container while developing re-usable modules, so the final application can configure provider for this container without effecting the other containers. |
|||
|
|||
**Example: Configure the File System provider for the `TestContainer`** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<TestContainer>(configuration => |
|||
{ |
|||
configuration.UseFileSystem(fileSystem => |
|||
{ |
|||
fileSystem.BasePath = "C:\\MyStorageFolder"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
See the [blob storing documentation](https://docs.abp.io/en/abp/latest/Blob-Storing) for more information. |
|||
|
|||
### Oracle Integration Package for Entity Framework Core |
|||
|
|||
We've created an [integration package for Oracle](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart), so you can easily switch to the Oracle for the EF Core. It is tested for the framework and pre-built modules. |
|||
|
|||
[See the documentation](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle) to start using the Oracle integration package. |
|||
|
|||
### Automatically Determining the Database Provider |
|||
|
|||
When you develop a **reusable application module** with EF Core integration, you generally want to develop your module **DBMS independent**. However, there are minor (sometimes major) differences between different DBMSs. If you perform a custom mapping based on the DBMS, you can now use `ModelBuilder.IsUsingXXX()` extension methods: |
|||
|
|||
````csharp |
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
modelBuilder.Entity<Phone>(b => |
|||
{ |
|||
//... |
|||
if (modelBuilder.IsUsingPostgreSql()) //Check if using PostgreSQL! |
|||
{ |
|||
b.Property(x => x.Number).HasMaxLength(20); |
|||
} |
|||
else |
|||
{ |
|||
b.Property(x => x.Number).HasMaxLength(32); |
|||
} |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
Beside the stupid example above, you can configure your mapping however you need! |
|||
|
|||
### ABP CLI: Translate Command |
|||
|
|||
`abp translate` is a new command that simplifies to translate [localization](https://docs.abp.io/en/abp/latest/Localization) files when you have multiple JSON localization files in a source control repository. |
|||
|
|||
The main purpose of this command is to **translate the ABP Framework** localization files (since the [abp repository](https://github.com/abpframework/abp) has tens of localization files to be translated in different folders). |
|||
|
|||
It is appreciated if you use this command to translate the framework resources **for your mother language**. |
|||
|
|||
See [the documentation](https://docs.abp.io/en/abp/latest/CLI#translate) to learn how to use it. Also see [the contribution guide](https://docs.abp.io/en/abp/latest/Contribution/Index). |
|||
|
|||
### The New Virtual File System Explorer Module |
|||
|
|||
Thanks to [@liangshiw](https://github.com/liangshiw) created and contributed a new module to explore files in the [Virtual File System](https://docs.abp.io/en/abp/latest/Virtual-File-System). It works for MVC UI and shows all the virtual files in the application. Example screenshots: |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
[See the documentation](https://docs.abp.io/en/abp/latest/Modules/Virtual-File-Explorer) to learn how to use it. |
|||
|
|||
### Sample Application: SignalR with Tiered Architecture |
|||
|
|||
Implementing SignalR in a distributed/tiered architecture can be challenging. We've created a sample application that demonstrate how to implement it using the [SignalR integration](https://docs.abp.io/en/abp/latest/SignalR-Integration) and the [distributed event bus](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus) system easily. |
|||
|
|||
See [the source code](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo) of the sample solution. |
|||
|
|||
**An article is on the road** that will deeply explain the solution. Follow the [@abpframework](https://twitter.com/abpframework) Twitter account. |
|||
|
|||
 |
|||
|
|||
*A picture from the article that shows the communication diagram of the solution* |
|||
|
|||
### About gRPC |
|||
|
|||
We've created a sample application to show how to create and consume gRPC endpoints in your ABP based applications. |
|||
|
|||
See [the source code](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) on GitHub. |
|||
|
|||
We were planning to create gRPC endpoints for all the pre-built application modules, but we see that ASP.NET Core gRPC integration is not mature enough and doesn't support some common deployment scenarios yet. So, deferring this to the next versions ([see this comment](https://github.com/abpframework/abp/issues/2882#issuecomment-633080242) for more). However, it is pretty standard if you want to use gRPC in your applications. ABP Framework has no issue with gRPC. Just check the [sample application](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo). |
|||
|
|||
### Others |
|||
|
|||
* [Time zone system](https://github.com/abpframework/abp/pull/3933) to support different time zones for an application. |
|||
* Support for [virtual path deployment](https://github.com/abpframework/abp/issues/4089) on IIS. |
|||
* RTL support for the Angular UI. |
|||
|
|||
See the [GitHub release notes](https://github.com/abpframework/abp/releases/tag/2.9.0) for others updates. |
|||
|
|||
## What's New with the ABP Commercial 2.9 |
|||
|
|||
In addition to all the features coming with the ABP Framework, the ABP Commercial has additional features with this release, as always. This section covers the [ABP Commercial](https://commercial.abp.io/) highlights in the version 2.9. |
|||
|
|||
### Organization Unit Management UI |
|||
|
|||
We've created the UI for manage organization units, their members and roles for the ABP Commercial [Identity Module](https://commercial.abp.io/modules/Volo.Identity.Pro): |
|||
|
|||
 |
|||
|
|||
OU management is available for both of the MVC (Razor Pages) and the Angular user interfaces. |
|||
|
|||
> See [this entry](https://support.abp.io/QA/Questions/222/Bugs--Problems-v290#answer-3cf5eba3-0bf1-2aa1-cc5e-39f5a0750329) if you're upgrading your solution from an earlier version. |
|||
|
|||
### Chat Module Angular UI |
|||
|
|||
We had introduced a new [chat module](https://commercial.abp.io/modules/Volo.Chat) in the previous version, which was only supporting the ASP.NET Core MVC / Razor Pages UI. Now, it has also an Angular UI option. |
|||
|
|||
 |
|||
|
|||
*A screenshot from the chat module - two users are sending messages to each other* |
|||
|
|||
### Easy CRM Angular UI |
|||
|
|||
Easy CRM is a sample application that is built on the ABP Commercial to provide a relatively complex application to the ABP Commercial customers. In the version 2.7, we have lunched it with MVC / Razor Pages UI. With the 2.9 version, we are releasing the Angular UI for the Easy CRM application. |
|||
|
|||
 |
|||
|
|||
*A screenshot from the "Order Details" page of the Easy CRM application.* |
|||
|
|||
See the [Easy CRM document](https://docs.abp.io/en/commercial/latest/samples/easy-crm) to learn how to download and run it. |
|||
|
|||
### Module Code Generation for the ABP Suite |
|||
|
|||
[ABP Suite](https://commercial.abp.io/tools/suite) is a tool that's main feature is to [generate code](https://docs.abp.io/en/commercial/latest/abp-suite/generating-crud-page) for complete CRUD functionality for an entity, from database to the UI layer. |
|||
|
|||
 |
|||
|
|||
*A screenshot from the ABP Suite: Define the properties of a new entity and let it to create the application code for you!* |
|||
|
|||
It was working only for [the application template](https://docs.abp.io/en/commercial/latest/startup-templates/application/index) until this release. Now, it supports to generate code for the [module projects](https://docs.abp.io/en/commercial/latest/startup-templates/module/index) too. That's a great way to create reusable application modules by taking the power of the code generation. |
|||
|
|||
In addition to this main feature, we added many minor enhancements on the ABP Suite in this release. |
|||
|
|||
> Notice: Generating code for the module template is currently in beta. Please inform us if you find any bug. |
|||
|
|||
### Lepton Theme |
|||
|
|||
[Lepton Theme](https://commercial.abp.io/themes) is the commercial theme we've developed for the ABP Commercial; |
|||
|
|||
* It is 100% bootstrap compatible - so you don't write theme specific HTML! |
|||
* Provides different kind of styles - you see the material style in the picture below. |
|||
* Provides different kind of layouts (side/top menu, fluid/boxed layout...). |
|||
* It is lightweight, responsive and modern. |
|||
* And... it is upgradeable with no cost! You just update a NuGet/NPM package to get the new features. |
|||
|
|||
We've create its own web site: [http://leptontheme.com/](http://leptontheme.com/) |
|||
|
|||
You can view all the components together, independent from an application: |
|||
|
|||
 |
|||
|
|||
This web site is currently in a very early stage. We will be documenting and improving this web site to be a reference for your development and explore the features of the theme. |
|||
|
|||
### Coming Soon: The File management Module |
|||
|
|||
Based on the new blob storing system (introduced above), we've started to build a file management module that is used to manage (navigate/upload/download) a hierarchical file system on your application and share the files between your users and with your customers. |
|||
|
|||
We plan to release the initial version with the ABP Commercial v3.0 and continue to improve it with the subsequent releases. |
|||
|
|||
## About the Next Version: 3.0 |
|||
|
|||
We have added many new features with the [v2.8](https://blog.abp.io/abp/ABP-v2.8.0-Releases-%26-Road-Map) and v2.9. In the next version, we will completely focus on the **documentation, performance improvements** and and other enhancements as well as bug fixes. |
|||
|
|||
For a long time, we were releasing a new feature version in every 2 weeks. We will continue to this approach after v3.0. But, as an exception to the v3.0, the development cycle will be ~4 weeks. **The planned release date for the v3.0 is the July 1, 2020**. |
|||
|
|||
## Bonus: Articles! |
|||
|
|||
Beside developing our products, our team are constantly writing articles/tutorials on various topics. You may want to check the latest articles: |
|||
|
|||
* [ASP.NET Core 3.1 WebHook Implementation Using Pub/Sub](https://volosoft.com/blog/ASP.NET-CORE-3.1-Webhook-Implementation-Using-Pub-Sub) |
|||
* [Using Azure Key Vault with ASP.NET Core](https://volosoft.com/blog/Using-Azure-Key-Vault-with-ASP.NET-Core) |
|||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 169 KiB |
@ -1,3 +1,167 @@ |
|||
# Current User |
|||
|
|||
TODO! |
|||
It is common to obtain information about the current user in an application. Current user is generally the user related to the current request in a web application. |
|||
|
|||
## ICurrentUser |
|||
|
|||
`ICurrentUser` is the main service to get info about the current user. |
|||
|
|||
Example: [Injecting](Dependency-Injection.md) the `ICurrentUser` into a service: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Users; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentUser _currentUser; |
|||
|
|||
public MyService(ICurrentUser currentUser) |
|||
{ |
|||
_currentUser = currentUser; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
Guid? userId = _currentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Common base classes have already injected this service as a base property. For example, you can directly use the `CurrentUser` property in an [application service](Application-Services.md): |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
public void Foo() |
|||
{ |
|||
Guid? userId = CurrentUser.Id; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Properties |
|||
|
|||
Here, the fundamental properties of the `ICurrentUser` interface: |
|||
|
|||
* **IsAuthenticated** (bool): Return `true` if current user has logged in (authenticated). `Id` and `UserName` returns `null` if the user has not logged in. |
|||
* **Id** (Guid?): Id of the current user or `null` if the current user has not logged in. |
|||
* **UserName** (string): User name of the current user or `null` if the current user has not logged in. |
|||
* **TenantId** (Guid?): Tenant Id of the current user, which can be useful for a [multi-tenant](Multi-Tenancy.md) application. |
|||
* **Email** (string): Email address of the current user or `null` if the current user has not logged in or not set an email address. |
|||
* **EmailVerified** (bool): True, if the phone number of the current user has been verified. |
|||
* **PhoneNumber** (string): Phone number of the current user or `null` if the current user has not logged in or not set a phone number. |
|||
* **PhoneNumberVerified** (bool): True, if the phone number of the current user has been verified. |
|||
* **Roles** (string[]): A string array of the role names of the current user. |
|||
|
|||
### Methods |
|||
|
|||
`ICurrentUser` is implemented on the `ICurrentPrincipalAccessor` (see the section below) and works with the claims. So, all of the above properties are actually retrieved from claims of the currently authenticated user. |
|||
|
|||
`ICurrentUser` has some methods to directly work with the claims if you have custom claims or get other non-common claim types. |
|||
|
|||
* **FindClaim**: Gets a claim with the given name or returns `null` if not found. |
|||
* **FindClaims**: Gets all the claims with the given name (it is allowed to have multiple claim values with the same name). |
|||
* **GetAllClaims**: Gets all the claims. |
|||
* **IsInRole**: A shortcut method to check if the current user has a role. |
|||
|
|||
Beside these standard methods, there are extension methods as shortcuts: |
|||
|
|||
* **FindClaimValue**: Gets the value of the claim with the given name, or `null` if not found. It has a generic overload that also casts the value to a specific type. |
|||
* **GetId**: Returns `Id` of the current user but throws an exception (instead of returning `null`) if the current user has not logged in. Use this only if you are sure that the user has already authenticated in your code context. |
|||
|
|||
### Authentication & Authorization |
|||
|
|||
`ICurrentUser` works independent of how user is authenticated or authorized. It silently works with any authentication system that works with the current principal (see the section below). |
|||
|
|||
## ICurrentPrincipalAccessor |
|||
|
|||
`ICurrentPrincipalAccessor` is the service that should be used (by the ABP Framework and your application code) whenever the current principle of the current user is needed. |
|||
|
|||
For a web application, it gets the `User` property of the current `HttpContext`. For a non-web application, it returns the `Thread.CurrentPrincipal`. |
|||
|
|||
> You generally don't need to this low level `ICurrentPrincipalAccessor` service and directly work with the `ICurrentUser` explained above. |
|||
|
|||
### Basic Usage |
|||
|
|||
You can inject `ICurrentPrincipalAccessor` and use the `Principal` property to the the current principal: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var allClaims = _currentPrincipalAccessor.Principal.Claims.ToList(); |
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### Changing the Current Principle |
|||
|
|||
Current principle is not something you want to set/change, except some advanced scenarios. If you need it, use the `Change` method of the `ICurrentPrincipalAccessor`. It takes a `ClaimsPrinciple` object and makes it "current" for a scope. |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
public class MyAppService : ApplicationService |
|||
{ |
|||
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor; |
|||
|
|||
public MyAppService(ICurrentPrincipalAccessor currentPrincipalAccessor) |
|||
{ |
|||
_currentPrincipalAccessor = currentPrincipalAccessor; |
|||
} |
|||
|
|||
public void Foo() |
|||
{ |
|||
var newPrinciple = new ClaimsPrincipal( |
|||
new ClaimsIdentity( |
|||
new Claim[] |
|||
{ |
|||
new Claim(AbpClaimTypes.UserId, Guid.NewGuid().ToString()), |
|||
new Claim(AbpClaimTypes.UserName, "john"), |
|||
new Claim("MyCustomCliam", "42") |
|||
} |
|||
) |
|||
); |
|||
|
|||
using (_currentPrincipalAccessor.Change(newPrinciple)) |
|||
{ |
|||
var userName = CurrentUser.UserName; //returns "john" |
|||
//... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Use the `Change` method always in a `using` statement, so it is restored to the previous value after the `using` scope ends. |
|||
|
|||
This can be a way to simulate a user login for a scope of the application code, however use it by care. |
|||
|
|||
## AbpClaimTypes |
|||
|
|||
`AbpClaimTypes` is a static class that defines the names of the standard claims and used by the ABP Framework. |
|||
|
|||
* Default values for the `UserName`, `UserId`, `Role` and `Email` properties are set from the [System.Security.Claims.ClaimTypes](https://docs.microsoft.com/en-us/dotnet/api/system.security.claims.claimtypes) class, but you can change them. |
|||
* Other properties, like `EmailVerified`, `PhoneNumber`, `TenantId`... are defined by the ABP Framework by following the standard names wherever possible. |
|||
|
|||
It is suggested to use properties of this class instead of magic strings for claim names. |
|||
|
|||
|
|||
@ -0,0 +1,21 @@ |
|||
# Console Application Startup Template |
|||
|
|||
This template is used to create a minimalist console application project. |
|||
|
|||
## How to Start With? |
|||
|
|||
First, install the [ABP CLI](../CLI.md) if you haven't installed before: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
Then use the `abp new` command in an empty folder to create a new solution: |
|||
|
|||
````bash |
|||
abp new Acme.MyConsoleApp -t console |
|||
```` |
|||
|
|||
`Acme.MyConsoleApp` is the solution name, like *YourCompany.YourProduct*. You can use single level, two-levels or three-levels naming. |
|||
|
|||
### |
|||
@ -0,0 +1,500 @@ |
|||
# How to Replace PermissionManagementComponent |
|||
|
|||
 |
|||
|
|||
Run the following command in `angular` folder to create a new component called `PermissionManagementComponent`. |
|||
|
|||
```bash |
|||
yarn ng generate component permission-management --entryComponent --inlineStyle |
|||
|
|||
# You don't need the --entryComponent option in Angular 9 |
|||
``` |
|||
|
|||
Open the generated `permission-management.component.ts` in `src/app/permission-management` folder and replace the content with the following: |
|||
|
|||
```js |
|||
import { |
|||
Component, |
|||
EventEmitter, |
|||
Input, |
|||
Output, |
|||
Renderer2, |
|||
TrackByFunction, |
|||
Inject, |
|||
Optional, |
|||
} from '@angular/core'; |
|||
import { ReplaceableComponents } from '@abp/ng.core'; |
|||
import { Select, Store } from '@ngxs/store'; |
|||
import { Observable } from 'rxjs'; |
|||
import { finalize, map, pluck, take, tap } from 'rxjs/operators'; |
|||
import { |
|||
GetPermissions, |
|||
UpdatePermissions, |
|||
PermissionManagement, |
|||
PermissionManagementState, |
|||
} from '@abp/ng.permission-management'; |
|||
|
|||
type PermissionWithMargin = PermissionManagement.Permission & { |
|||
margin: number; |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'app-permission-management', |
|||
templateUrl: './permission-management.component.html', |
|||
styles: [ |
|||
` |
|||
.overflow-scroll { |
|||
max-height: 70vh; |
|||
overflow-y: scroll; |
|||
} |
|||
`, |
|||
], |
|||
}) |
|||
export class PermissionManagementComponent |
|||
implements |
|||
PermissionManagement.PermissionManagementComponentInputs, |
|||
PermissionManagement.PermissionManagementComponentOutputs { |
|||
protected _providerName: string; |
|||
@Input() |
|||
get providerName(): string { |
|||
if (this.replaceableData) return this.replaceableData.inputs.providerName; |
|||
|
|||
return this._providerName; |
|||
} |
|||
|
|||
set providerName(value: string) { |
|||
this._providerName = value; |
|||
} |
|||
|
|||
protected _providerKey: string; |
|||
@Input() |
|||
get providerKey(): string { |
|||
if (this.replaceableData) return this.replaceableData.inputs.providerKey; |
|||
|
|||
return this._providerKey; |
|||
} |
|||
|
|||
set providerKey(value: string) { |
|||
this._providerKey = value; |
|||
} |
|||
|
|||
protected _hideBadges = false; |
|||
@Input() |
|||
get hideBadges(): boolean { |
|||
if (this.replaceableData) return this.replaceableData.inputs.hideBadges; |
|||
|
|||
return this._hideBadges; |
|||
} |
|||
|
|||
set hideBadges(value: boolean) { |
|||
this._hideBadges = value; |
|||
} |
|||
|
|||
protected _visible = false; |
|||
@Input() |
|||
get visible(): boolean { |
|||
return this._visible; |
|||
} |
|||
|
|||
set visible(value: boolean) { |
|||
if (value === this._visible) return; |
|||
|
|||
if (value) { |
|||
this.openModal().subscribe(() => { |
|||
this._visible = true; |
|||
this.visibleChange.emit(true); |
|||
if (this.replaceableData) this.replaceableData.outputs.visibleChange(true); |
|||
}); |
|||
} else { |
|||
this.selectedGroup = null; |
|||
this._visible = false; |
|||
this.visibleChange.emit(false); |
|||
if (this.replaceableData) this.replaceableData.outputs.visibleChange(false); |
|||
} |
|||
} |
|||
|
|||
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
@Select(PermissionManagementState.getPermissionGroups) |
|||
groups$: Observable<PermissionManagement.Group[]>; |
|||
|
|||
@Select(PermissionManagementState.getEntityDisplayName) |
|||
entityName$: Observable<string>; |
|||
|
|||
selectedGroup: PermissionManagement.Group; |
|||
|
|||
permissions: PermissionManagement.Permission[] = []; |
|||
|
|||
selectThisTab = false; |
|||
|
|||
selectAllTab = false; |
|||
|
|||
modalBusy = false; |
|||
|
|||
trackByFn: TrackByFunction<PermissionManagement.Group> = (_, item) => item.name; |
|||
|
|||
get selectedGroupPermissions$(): Observable<PermissionWithMargin[]> { |
|||
return this.groups$.pipe( |
|||
map((groups) => |
|||
this.selectedGroup |
|||
? groups.find((group) => group.name === this.selectedGroup.name).permissions |
|||
: [] |
|||
), |
|||
map<PermissionManagement.Permission[], PermissionWithMargin[]>((permissions) => |
|||
permissions.map( |
|||
(permission) => |
|||
(({ |
|||
...permission, |
|||
margin: findMargin(permissions, permission), |
|||
isGranted: this.permissions.find((per) => per.name === permission.name).isGranted, |
|||
} as any) as PermissionWithMargin) |
|||
) |
|||
) |
|||
); |
|||
} |
|||
|
|||
get isVisible(): boolean { |
|||
if (!this.replaceableData) return this.visible; |
|||
|
|||
return this.replaceableData.inputs.visible; |
|||
} |
|||
|
|||
constructor( |
|||
@Optional() |
|||
@Inject('REPLACEABLE_DATA') |
|||
public replaceableData: ReplaceableComponents.ReplaceableTemplateData< |
|||
PermissionManagement.PermissionManagementComponentInputs, |
|||
PermissionManagement.PermissionManagementComponentOutputs |
|||
>, |
|||
private store: Store |
|||
) {} |
|||
|
|||
getChecked(name: string) { |
|||
return (this.permissions.find((per) => per.name === name) || { isGranted: false }).isGranted; |
|||
} |
|||
|
|||
isGrantedByOtherProviderName(grantedProviders: PermissionManagement.GrantedProvider[]): boolean { |
|||
if (grantedProviders.length) { |
|||
return grantedProviders.findIndex((p) => p.providerName !== this.providerName) > -1; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
onClickCheckbox(clickedPermission: PermissionManagement.Permission, value) { |
|||
if ( |
|||
clickedPermission.isGranted && |
|||
this.isGrantedByOtherProviderName(clickedPermission.grantedProviders) |
|||
) |
|||
return; |
|||
|
|||
setTimeout(() => { |
|||
this.permissions = this.permissions.map((per) => { |
|||
if (clickedPermission.name === per.name) { |
|||
return { ...per, isGranted: !per.isGranted }; |
|||
} else if (clickedPermission.name === per.parentName && clickedPermission.isGranted) { |
|||
return { ...per, isGranted: false }; |
|||
} else if (clickedPermission.parentName === per.name && !clickedPermission.isGranted) { |
|||
return { ...per, isGranted: true }; |
|||
} |
|||
|
|||
return per; |
|||
}); |
|||
|
|||
this.setTabCheckboxState(); |
|||
this.setGrantCheckboxState(); |
|||
}, 0); |
|||
} |
|||
|
|||
setTabCheckboxState() { |
|||
this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { |
|||
const selectedPermissions = permissions.filter((per) => per.isGranted); |
|||
const element = document.querySelector('#select-all-in-this-tabs') as any; |
|||
|
|||
if (selectedPermissions.length === permissions.length) { |
|||
element.indeterminate = false; |
|||
this.selectThisTab = true; |
|||
} else if (selectedPermissions.length === 0) { |
|||
element.indeterminate = false; |
|||
this.selectThisTab = false; |
|||
} else { |
|||
element.indeterminate = true; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
setGrantCheckboxState() { |
|||
const selectedAllPermissions = this.permissions.filter((per) => per.isGranted); |
|||
const checkboxElement = document.querySelector('#select-all-in-all-tabs') as any; |
|||
|
|||
if (selectedAllPermissions.length === this.permissions.length) { |
|||
checkboxElement.indeterminate = false; |
|||
this.selectAllTab = true; |
|||
} else if (selectedAllPermissions.length === 0) { |
|||
checkboxElement.indeterminate = false; |
|||
this.selectAllTab = false; |
|||
} else { |
|||
checkboxElement.indeterminate = true; |
|||
} |
|||
} |
|||
|
|||
onClickSelectThisTab() { |
|||
this.selectedGroupPermissions$.pipe(take(1)).subscribe((permissions) => { |
|||
permissions.forEach((permission) => { |
|||
if (permission.isGranted && this.isGrantedByOtherProviderName(permission.grantedProviders)) |
|||
return; |
|||
|
|||
const index = this.permissions.findIndex((per) => per.name === permission.name); |
|||
|
|||
this.permissions = [ |
|||
...this.permissions.slice(0, index), |
|||
{ ...this.permissions[index], isGranted: !this.selectThisTab }, |
|||
...this.permissions.slice(index + 1), |
|||
]; |
|||
}); |
|||
}); |
|||
|
|||
this.setGrantCheckboxState(); |
|||
} |
|||
|
|||
onClickSelectAll() { |
|||
this.permissions = this.permissions.map((permission) => ({ |
|||
...permission, |
|||
isGranted: |
|||
this.isGrantedByOtherProviderName(permission.grantedProviders) || !this.selectAllTab, |
|||
})); |
|||
|
|||
this.selectThisTab = !this.selectAllTab; |
|||
} |
|||
|
|||
onChangeGroup(group: PermissionManagement.Group) { |
|||
this.selectedGroup = group; |
|||
this.setTabCheckboxState(); |
|||
} |
|||
|
|||
submit() { |
|||
this.modalBusy = true; |
|||
const unchangedPermissions = getPermissions( |
|||
this.store.selectSnapshot(PermissionManagementState.getPermissionGroups) |
|||
); |
|||
|
|||
const changedPermissions: PermissionManagement.MinimumPermission[] = this.permissions |
|||
.filter((per) => |
|||
unchangedPermissions.find((unchanged) => unchanged.name === per.name).isGranted === |
|||
per.isGranted |
|||
? false |
|||
: true |
|||
) |
|||
.map(({ name, isGranted }) => ({ name, isGranted })); |
|||
|
|||
if (changedPermissions.length) { |
|||
this.store |
|||
.dispatch( |
|||
new UpdatePermissions({ |
|||
providerKey: this.providerKey, |
|||
providerName: this.providerName, |
|||
permissions: changedPermissions, |
|||
}) |
|||
) |
|||
.pipe(finalize(() => (this.modalBusy = false))) |
|||
.subscribe(() => { |
|||
this.visible = false; |
|||
}); |
|||
} else { |
|||
this.modalBusy = false; |
|||
this.visible = false; |
|||
} |
|||
} |
|||
|
|||
openModal() { |
|||
if (!this.providerKey || !this.providerName) { |
|||
throw new Error('Provider Key and Provider Name are required.'); |
|||
} |
|||
|
|||
return this.store |
|||
.dispatch( |
|||
new GetPermissions({ |
|||
providerKey: this.providerKey, |
|||
providerName: this.providerName, |
|||
}) |
|||
) |
|||
.pipe( |
|||
pluck('PermissionManagementState', 'permissionRes'), |
|||
tap((permissionRes: PermissionManagement.Response) => { |
|||
this.selectedGroup = permissionRes.groups[0]; |
|||
this.permissions = getPermissions(permissionRes.groups); |
|||
}) |
|||
); |
|||
} |
|||
|
|||
initModal() { |
|||
this.setTabCheckboxState(); |
|||
this.setGrantCheckboxState(); |
|||
} |
|||
|
|||
onVisibleChange(visible: boolean) { |
|||
this.visible = visible; |
|||
|
|||
if (this.replaceableData) { |
|||
this.replaceableData.inputs.visible = visible; |
|||
this.replaceableData.outputs.visibleChange(visible); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function findMargin( |
|||
permissions: PermissionManagement.Permission[], |
|||
permission: PermissionManagement.Permission |
|||
) { |
|||
const parentPermission = permissions.find((per) => per.name === permission.parentName); |
|||
|
|||
if (parentPermission && parentPermission.parentName) { |
|||
let margin = 20; |
|||
return (margin += findMargin(permissions, parentPermission)); |
|||
} |
|||
|
|||
return parentPermission ? 20 : 0; |
|||
} |
|||
|
|||
function getPermissions(groups: PermissionManagement.Group[]): PermissionManagement.Permission[] { |
|||
return groups.reduce((acc, val) => [...acc, ...val.permissions], []); |
|||
} |
|||
``` |
|||
|
|||
Open the generated `permission-management.component.html` in `src/app/permission-management` folder and replace the content with the below: |
|||
|
|||
```html |
|||
<abp-modal |
|||
[visible]="isVisible" |
|||
(visibleChange)="onVisibleChange($event)" |
|||
(init)="initModal()" |
|||
[busy]="modalBusy" |
|||
> |
|||
<ng-container *ngIf="{ entityName: entityName$ | async } as data"> |
|||
<ng-template #abpHeader> |
|||
<h4> |
|||
{%{{{ 'AbpPermissionManagement::Permissions' | abpLocalization }}}%} - {%{{{ data.entityName }}}%} |
|||
</h4> |
|||
</ng-template> |
|||
<ng-template #abpBody> |
|||
<div class="custom-checkbox custom-control mb-2"> |
|||
<input |
|||
type="checkbox" |
|||
id="select-all-in-all-tabs" |
|||
name="select-all-in-all-tabs" |
|||
class="custom-control-input" |
|||
[(ngModel)]="selectAllTab" |
|||
(click)="onClickSelectAll()" |
|||
/> |
|||
<label class="custom-control-label" for="select-all-in-all-tabs">{%{{{ |
|||
'AbpPermissionManagement::SelectAllInAllTabs' | abpLocalization |
|||
}}}%}</label> |
|||
</div> |
|||
|
|||
<hr class="mt-2 mb-2" /> |
|||
<div class="row"> |
|||
<div class="overflow-scroll col-md-4"> |
|||
<ul class="nav nav-pills flex-column"> |
|||
<li *ngFor="let group of groups$ | async; trackBy: trackByFn" class="nav-item"> |
|||
<a |
|||
class="nav-link pointer" |
|||
[class.active]="selectedGroup?.name === group?.name" |
|||
(click)="onChangeGroup(group)" |
|||
>{%{{{ group?.displayName }}}%}</a |
|||
> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="col-md-8 overflow-scroll"> |
|||
<h4>{%{{{ selectedGroup?.displayName }}}%}</h4> |
|||
<hr class="mt-2 mb-3" /> |
|||
<div class="pl-1 pt-1"> |
|||
<div class="custom-checkbox custom-control mb-2"> |
|||
<input |
|||
type="checkbox" |
|||
id="select-all-in-this-tabs" |
|||
name="select-all-in-this-tabs" |
|||
class="custom-control-input" |
|||
[(ngModel)]="selectThisTab" |
|||
(click)="onClickSelectThisTab()" |
|||
/> |
|||
<label class="custom-control-label" for="select-all-in-this-tabs">{%{{{ |
|||
'AbpPermissionManagement::SelectAllInThisTab' | abpLocalization |
|||
}}}%}</label> |
|||
</div> |
|||
<hr class="mb-3" /> |
|||
<div |
|||
*ngFor=" |
|||
let permission of selectedGroupPermissions$ | async; |
|||
let i = index; |
|||
trackBy: trackByFn |
|||
" |
|||
[style.margin-left]="permission.margin + 'px'" |
|||
class="custom-checkbox custom-control mb-2" |
|||
> |
|||
<input |
|||
#permissionCheckbox |
|||
type="checkbox" |
|||
[checked]="getChecked(permission.name)" |
|||
[value]="getChecked(permission.name)" |
|||
[attr.id]="permission.name" |
|||
class="custom-control-input" |
|||
[disabled]="isGrantedByOtherProviderName(permission.grantedProviders)" |
|||
/> |
|||
<label |
|||
class="custom-control-label" |
|||
[attr.for]="permission.name" |
|||
(click)="onClickCheckbox(permission, permissionCheckbox.value)" |
|||
>{%{{{ permission.displayName }}}%} |
|||
<ng-container *ngIf="!hideBadges"> |
|||
<span |
|||
*ngFor="let provider of permission.grantedProviders" |
|||
class="badge badge-light" |
|||
>{%{{{ provider.providerName }}}%}: {%{{{ provider.providerKey }}}%}</span |
|||
> |
|||
</ng-container> |
|||
</label> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
<ng-template #abpFooter> |
|||
<button type="button" class="btn btn-secondary" #abpClose> |
|||
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" (click)="submit()">{%{{{ |
|||
'AbpIdentity::Save' | abpLocalization |
|||
}}}%}</abp-button> |
|||
</ng-template> |
|||
</ng-container> |
|||
</abp-modal> |
|||
``` |
|||
|
|||
Open `app.component.ts` in `src/app` folder and modify it as shown below: |
|||
|
|||
```js |
|||
import { AddReplaceableComponent } from '@abp/ng.core'; |
|||
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { PermissionManagementComponent } from './permission-management/permission-management.component'; |
|||
|
|||
//... |
|||
export class AppComponent implements OnInit { |
|||
constructor(private store: Store) {} // injected store |
|||
|
|||
ngOnInit() { |
|||
// added dispatching the AddReplaceableComponent action |
|||
this.store.dispatch( |
|||
new AddReplaceableComponent({ |
|||
component: PermissionManagementComponent, |
|||
key: ePermissionManagementComponents.PermissionManagement, |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## See Also |
|||
|
|||
- [Component Replacement](./Component-Replacement.md) |
|||
|
After Width: | Height: | Size: 252 KiB |
@ -0,0 +1,294 @@ |
|||
# ABP框架v2.9已经发布 |
|||
|
|||
**ABP框架**和**ABP商业版**2.9已经发布,这是3.0之前的最后一个版本!这篇文章将涵盖本次发布中的**新增内容**. |
|||
|
|||
## ABP框架2.9有哪些新增内容? |
|||
|
|||
你可以中[GitHub的发行说明](https://github.com/abpframework/abp/releases/tag/2.9.0)中看到所有的变更.这篇文章将只包括重要特征/变更. |
|||
|
|||
### 预编译Razor Pages |
|||
|
|||
在之前的版本, 预构建的页面(为[应用模块](https://docs.abp.io/en/abp/latest/Modules/Index))和视图组件是在运行时编译. 现在,它们使用了预编译. 我们测量的应用程序启动时间(尤其是MVC UI)已经减少了50%以上.换句话说,它比之前的版本快**两倍**.速度变化也影响你第一次访问某一个页面时. |
|||
|
|||
这是一个v2.8和v2.9启动应用程序模板的对比结果: |
|||
|
|||
```` |
|||
### v2.8 |
|||
|
|||
2020-06-04 22:59:04.891 +08:00 [INF] Starting web host. |
|||
2020-06-04 22:59:07.662 +08:00 [INF] Now listening on: https://localhost:44391 |
|||
2020-06-04 22:59:17.315 +08:00 [INF] Request finished in 7756.6218ms 200 text/html; |
|||
|
|||
Total: 12.42s |
|||
|
|||
### v2.9 |
|||
|
|||
2020-06-04 22:59:13.720 +08:00 [INF] Starting web host. |
|||
2020-06-04 22:59:16.639 +08:00 [INF] Now listening on: https://localhost:44369 |
|||
2020-06-04 22:59:18.957 +08:00 [INF] Request finished in 1780.5461ms 200 text/html; |
|||
|
|||
Total: 5.24s |
|||
```` |
|||
|
|||
你不用做任何改动就能获得新方法带来的益处.[重写UI页/组件](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Customization-User-Interface)和之前一样也能正常工作.我们将在v3.0中继续致力于性能上的提升. |
|||
|
|||
### 组织单元系统 |
|||
|
|||
[Identity模块](https://docs.abp.io/en/abp/latest/Modules/Identity)现在有了呼声最高的功能: 组织单元! |
|||
|
|||
组织单元系统用来在应用程序中创建分层组织树.这样你可以使用该组织树来授权应用程序中的数据和功能. |
|||
|
|||
文档将很快到来...... |
|||
|
|||
### 新的Blob存储包 |
|||
|
|||
我们创建了一个新的[Blob存储包](https://www.nuget.org/packages/Volo.Abp.BlobStoring)用来存储任意二进制对象.它一般用于在应用程序中存储文件.这个包提供了一个抽象,因此任何应用程序或[模块](https://docs.abp.io/en/abp/latest/Module-Development-Basics)都能以存储提供器无关的方式来保存和获取文件. |
|||
|
|||
目前实现了两个存储提供器: |
|||
|
|||
* [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem)包, 在本地文件系统中存储对象/文件. |
|||
* [Volo.Abp.BlobStoring.Database](https://github.com/abpframework/abp/tree/dev/modules/blob-storing-database)模块, 在数据库中存储对象/文件.目前支持[Entity Framework Core](https://docs.abp.io/en/abp/latest/Entity-Framework-Core)(因此,你可以使用[任何关系数据库](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Other-DBMS)和[MongoDB](https://docs.abp.io/en/abp/latest/MongoDB)). |
|||
|
|||
[Azure BLOB提供器](https://github.com/abpframework/abp/issues/4098)将会在3.0中可用. 你可请求其他的云提供器或在[GitHub库](https://github.com/abpframework/abp/issues/new)上提交你自己的贡献. |
|||
|
|||
Blob存储系统的一个好处是,它允许你创建多个容器(每个容器是一个Blob存储),并为每个容器使用不同的存储提供器. |
|||
|
|||
**示例:使用默认的容器保存和取得一个字节数组** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer _container; |
|||
|
|||
public MyService(IBlobContainer container) |
|||
{ |
|||
_container = container; |
|||
} |
|||
|
|||
public async Task FooAsync() |
|||
{ |
|||
//保存一个BLOB |
|||
byte[] bytes = GetBytesFromSomeWhere(); |
|||
await _container.SaveAsync("my-unique-blob-name", bytes); |
|||
|
|||
//获取一个BLOB |
|||
bytes = await _container.GetAllBytesAsync("my-unique-blob-name"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
它可以使用`byte[]`和`Stream`对象. |
|||
|
|||
**示例:使用类型化(命名)容器来保存和获取stream** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer<TestContainer> _container; |
|||
|
|||
public MyService(IBlobContainer<TestContainer> container) |
|||
{ |
|||
_container = container; |
|||
} |
|||
|
|||
public async Task FooAsync() |
|||
{ |
|||
//保存一个BLOB |
|||
Stream stream = GetStreamFromSomeWhere(); |
|||
await _container.SaveAsync("my-unique-blob-name", stream); |
|||
|
|||
//获取一个BLOB |
|||
stream = await _container.GetAsync("my-unique-blob-name"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`TestContainer`只是一个用来标识容器的空类: |
|||
|
|||
````csharp |
|||
[BlobContainerName("test")] //指定容器的名字 |
|||
public class TestContainer |
|||
{ |
|||
|
|||
} |
|||
```` |
|||
|
|||
类型化(命名)容器可被配置为使用不同的存储提供器而不是默认的.在开发可复用的模块时, 始终使用类型化的容器是一个很好的做法,这样最终应用程序可以为这个容器配置提供器,而不影响其他容器. |
|||
|
|||
**示例:为`TestContainer`配置文件系统提供器** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<TestContainer>(configuration => |
|||
{ |
|||
configuration.UseFileSystem(fileSystem => |
|||
{ |
|||
fileSystem.BasePath = "C:\\MyStorageFolder"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
查看[blob存储文档](https://docs.abp.io/en/abp/latest/Blob-Storing)以获取更多的信息. |
|||
|
|||
### Entity Framework Core的Oracle集成包 |
|||
|
|||
我们创建了一个[Oralce集成包](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.Oracle.Devart),这样你就可以为EF Core轻松地切换到Oracle.它已经为框架和预构建的模块进行了测试. |
|||
|
|||
[查看文档](https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Oracle)开始使用Oracle集成包. |
|||
|
|||
### 自动判断数据库提供器 |
|||
|
|||
当你用EF Core开发一个**可复用的应用程序模块**时,你通常要将你的模块开发为**DBMS无关**的.但是,不同的DBMS有一些微小的(有时是很大的)区别.现在如何你执行基于DBMS的自定义映射,可以使用`ModelBuilder.IsUsingXXX()`扩展方法: |
|||
|
|||
````csharp |
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
modelBuilder.Entity<Phone>(b => |
|||
{ |
|||
//... |
|||
if (modelBuilder.IsUsingPostgreSql()) //检查是否在使用PostgreSQL! |
|||
{ |
|||
b.Property(x => x.Number).HasMaxLength(20); |
|||
} |
|||
else |
|||
{ |
|||
b.Property(x => x.Number).HasMaxLength(32); |
|||
} |
|||
}); |
|||
} |
|||
```` |
|||
|
|||
除了上面这种的傻傻的例子,你可以任意配置你的映射! |
|||
|
|||
### ABP CLI:翻译命令 |
|||
|
|||
`abp translate`是一个新的命令,当你的源代码库中包含多个JSON本地化文件时, 它可用来简化翻译[本地化](https://docs.abp.io/en/abp/latest/Localization)文件, |
|||
|
|||
该命令的主要目的是**翻译ABP框架**的本地化文件(因为[abp库](https://github.com/abpframework/abp)在不同的文件中含有成千上万个本地化文件需要翻译). |
|||
|
|||
非常感谢如果你使用这个命令将框架资源翻译**为你的母语**. |
|||
|
|||
查看[文档](https://docs.abp.io/en/abp/latest/CLI#translate)来学习如何使用它.也可查看[贡献指南](https://docs.abp.io/en/abp/latest/Contribution/Index). |
|||
|
|||
### 新的虚拟文件系统浏览器模块 |
|||
|
|||
感谢[@liangshiw](https://github.com/liangshiw)创建并贡献了一个新的模块用来浏览[虚拟文件系统](https://docs.abp.io/en/abp/latest/Virtual-File-System)中的文件.它适用于MVC UI并显示所有应用程序中的虚拟文件.示例截图: |
|||
|
|||
 |
|||
|
|||
 |
|||
|
|||
[查看文档](https://docs.abp.io/en/abp/latest/Modules/Virtual-File-Explorer)学习如何使用它. |
|||
|
|||
### 示例应用程序:SignalR与分层架构 |
|||
|
|||
在分布式/分层架构中实施SignalR是具有挑战性的.我们创建了一个示例应用程序演示如何轻松地使用[SignalR集成](https://docs.abp.io/en/abp/latest/SignalR-Integration)和[分布式事件总线](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus)系统 |
|||
|
|||
查看示例解决方案的[源代码](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo). |
|||
|
|||
**一篇正在路上的文章**将深入地解释该解决方案.关注[@abpframework](https://twitter.com/abpframework)的Twitter帐号. |
|||
|
|||
 |
|||
|
|||
*一张文章中的图片,显示了该解决方案的通信图* |
|||
|
|||
### 关于gRPC |
|||
|
|||
我们创建了一个示例应用程序来说明如何在基于ABP的应用程序中创建和使用gRPC端点. |
|||
|
|||
查看GitHub上的[源码](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo). |
|||
|
|||
我们本来计划为所有预构建的应用程序模块创建gRPC端点,但我们发现ASP.NET Core gRPC集成还不够成熟,不支持一些常见的部署场景.所以推迟到了下一个版本(更多内容[查看此评论](https://github.com/abpframework/abp/issues/2882#issuecomment-633080242)).但是,在你的应用程序中使用gRPC是非常标准的. ABP框架与gRPC没有问题.看一下[示例应用程序](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo). |
|||
|
|||
### 其它 |
|||
|
|||
* [时区系统](https://github.com/abpframework/abp/pull/3933)为应用程序支持不同的时区. |
|||
* 在IIS上支持[虚拟路径部署](https://github.com/abpframework/abp/issues/4089). |
|||
* 为Angular UI支持RTL. |
|||
|
|||
其它更新请查看[GitHub发行说明](https://github.com/abpframework/abp/releases/tag/2.9.0). |
|||
|
|||
## ABP商业版2.9有哪些新增内容 |
|||
|
|||
与往常一样, 除了ABP框架所有这些功能以外,ABP商业版在本次发布还有一些额外的功能.本节介绍[ABP商业版](https://commercial.abp.io/)在2.9版本中的亮点. |
|||
|
|||
### 组织单元管理UI |
|||
|
|||
我们为组织单元创建了UI,管理ABP商业版[Identity模块](https://commercial.abp.io/modules/Volo.Identity.Pro)的成员和角色: |
|||
|
|||
 |
|||
|
|||
OU管理适用于MVC(Razor Pages)和Angular用户界面. |
|||
|
|||
### 聊天模块Angular UI |
|||
|
|||
我们在前一个版本介绍了新的[聊天模块](https://commercial.abp.io/modules/Volo.Chat), 当时它只有ASP.NET Core MVC / Razor Pages UI. 现在它也包含了一个Angular UI选项. |
|||
|
|||
 |
|||
|
|||
*聊天模块的截图 - 两个用户互相发消息* |
|||
|
|||
### Easy CRM Angular UI |
|||
|
|||
Easy CRM是建立在ABP商业版上的一个示例应用程序, 用来为ABP商业版客户提供一个相对复杂的应用程序.在2.7版本中,我们已经发布了MVC / Razor Pages UI. 这次2.9版中, 我们为Easy CRM应用程序发布了Angular UI. |
|||
|
|||
 |
|||
|
|||
*Easy CRM应用程序中"订单详细"的截图.* |
|||
|
|||
查看[Easy CRM文档](https://docs.abp.io/en/commercial/latest/samples/easy-crm)学习如何下载并运行它. |
|||
|
|||
### ABP Suite模块代码生成 |
|||
|
|||
[ABP Suite](https://commercial.abp.io/tools/suite)是一个工具,主要功能是用来为一个实体[生成代码](https://docs.abp.io/en/commercial/latest/abp-suite/generating-crud-page), 从数据库到UI层具有完整的CRUD功能. |
|||
|
|||
 |
|||
|
|||
*ABP Suite的截图: 定义新实体的属性并且为你生成应用程序代码!* |
|||
|
|||
在本次发布之前它只工作于[应用程序模板](https://docs.abp.io/en/commercial/latest/startup-templates/application/index).现在,它支持为[模块项目](https://docs.abp.io/en/commercial/latest/startup-templates/module/index)生成代码.利用代码生成的威力来创建可复用应用程序模块是很棒的一个做法. |
|||
|
|||
除了这个主要功能,我们在这个版本中向ABP Suite添加了许多细微的改进. |
|||
|
|||
>注意:模块模板代码生成目前处于测试阶段.如果你发现任何bug,请告知我们. |
|||
|
|||
### Lepton主题 |
|||
|
|||
[Lepton主题](https://commercial.abp.io/themes)是我们为ABP商业版开发的一个商业主题. |
|||
|
|||
* 与Bootstrap 100%兼容 - 让你不写主题特定的HTML! |
|||
* 提供不同类型的风格 - 看一下下图中的Material风格. |
|||
* 提供不同类型的布局(侧/顶部菜单,流式/盒式布局...). |
|||
* 轻量化,响应式和现代化. |
|||
* 还有...它是可升级的,没有成本!你只需更新NuGet / NPM包来获得新的功能. |
|||
|
|||
我们创建了它的专属网站:[http://leptontheme.com/](http://leptontheme.com/) |
|||
|
|||
在这里你可以查看所有的组件, 无需单独的应用程序. |
|||
|
|||
 |
|||
|
|||
这个网站目前正处于一个非常早期的阶段.我们将创建文档和和改进网站, 来为你的开发提供参考和探索主题的功能. |
|||
|
|||
### 即将推出:文件管理模块 |
|||
|
|||
基于新的blob存储系统(上面介绍的),我们已经开始构建一个文件管理模块用来管理(浏览/上传/下载)你应用程序中分层文件系统并在用户与客户之间分享文件. |
|||
|
|||
我们计划在ABP商业版v3.0中发行最初版本,并继续进行后续版本的改进. |
|||
|
|||
## 关于下一个版本:3.0 |
|||
|
|||
我们在[v2.8](https://blog.abp.io/abp/ABP-v2.8.0-Releases-%26-Road-Map)和v2.9中增加了许多新的功能.在下一个版本中,我们将完全专注于**文档,性能优化**和其它改进,如bug修复. |
|||
|
|||
长期以来,我们每2周发布一个新功能版本.我们在v3.0以后继续这种方式.但是,v3.0是一个例外,开发周期大概为4周.**v3.0的计划发布日期是2020年7月1日**. |
|||
|
|||
## 彩蛋:文章! |
|||
|
|||
除了开发我们的产品,我们的团队都在不断地撰写各种主题的文章/教程.你可以看一下最新的文章: |
|||
|
|||
* [ASP.NET Core 3.1使用Pub/Sub实现WebHook](https://volosoft.com/blog/ASP.NET-CORE-3.1-Webhook-Implementation-Using-Pub-Sub) |
|||
* [ASP.NET Core使用Azure Key Vault](https://volosoft.com/blog/Using-Azure-Key-Vault-with-ASP.NET-Core) |
|||
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 169 KiB |
@ -0,0 +1,13 @@ |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageSearchBox |
|||
{ |
|||
public class AbpPageSearchBoxViewComponent : AbpViewComponent |
|||
{ |
|||
public IViewComponentResult Invoke() |
|||
{ |
|||
return View("~/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
@using Localization.Resources.AbpUi |
|||
@using Microsoft.Extensions.Localization |
|||
@inject IStringLocalizer<AbpUiResource> L |
|||
<form method="post" class="page-search-form"> |
|||
<div class="input-group"> |
|||
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value"> |
|||
<div class="input-group-append"> |
|||
<abp-button button-type="Primary" type="submit"> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
@ -1,2 +0,0 @@ |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@ -0,0 +1,4 @@ |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@ -1,13 +0,0 @@ |
|||
using Microsoft.AspNetCore.Mvc.Localization; |
|||
using Microsoft.AspNetCore.Mvc.Razor.Internal; |
|||
using Volo.Abp.Account.Localization; |
|||
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; |
|||
|
|||
namespace Volo.Abp.Account.Web.Pages.Account |
|||
{ |
|||
public abstract class AccountPage : AbpPage |
|||
{ |
|||
[RazorInject] |
|||
public IHtmlLocalizer<AccountResource> L { get; set; } |
|||
} |
|||
} |
|||
@ -1,3 +1,2 @@ |
|||
@page "/Account/Logout" |
|||
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage |
|||
@model Volo.Abp.Account.Web.Pages.Account.LogoutModel |
|||
@model Volo.Abp.Account.Web.Pages.Account.LogoutModel |
|||
|
|||