@ -0,0 +1,3 @@ |
|||
# BLOB Storing Azure Provider |
|||
|
|||
This feature will be available with v3.0! |
|||
@ -0,0 +1,177 @@ |
|||
# BLOB Storing: Creating a Custom Provider |
|||
|
|||
This document explains how you can create a new storage provider for the BLOB storing system with an example. |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to create a new storage provider. |
|||
|
|||
## Example Implementation |
|||
|
|||
The first step is to create a class implements the `IBlobProvider` interface or inherit from the `BlobProviderBase` abstract class. |
|||
|
|||
````csharp |
|||
using System.IO; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency |
|||
{ |
|||
public override Task SaveAsync(BlobProviderSaveArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<bool> DeleteAsync(BlobProviderDeleteArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<bool> ExistsAsync(BlobProviderExistsArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
|
|||
public override Task<Stream> GetOrNullAsync(BlobProviderGetArgs args) |
|||
{ |
|||
//TODO... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* `MyCustomBlobProvider` inherits from the `BlobProviderBase` and overrides the `abstract` methods. The actual implementation is up to you. |
|||
* Implementing `ITransientDependency` registers this class to the [Dependency Injection](Dependency-Injection.md) system as a transient service. |
|||
|
|||
> **Notice: Naming conventions are important**. If your class name doesn't end with `BlobProvider`, you must manually register/expose your service for the `IBlobProvider`. |
|||
|
|||
That's all. Now, you can configure containers (inside the `ConfigureServices` method of your [module](Module-Development-Basics.md)) to use the `MyCustomBlobProvider` class: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.ProviderType = typeof(MyCustomBlobProvider); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) if you want to configure a specific container. |
|||
|
|||
### BlobContainerConfiguration Extension Method |
|||
|
|||
If you want to provide a simpler configuration, create an extension method for the `BlobContainerConfiguration` class: |
|||
|
|||
```` |
|||
public static class MyBlobContainerConfigurationExtensions |
|||
{ |
|||
public static BlobContainerConfiguration UseMyCustomBlobProvider( |
|||
this BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); |
|||
return containerConfiguration; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can configure containers easier using the extension method: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMyCustomBlobProvider(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### Extra Configuration Options |
|||
|
|||
`BlobContainerConfiguration` allows to add/remove provider specific configuration objects. If your provider needs to additional configuration, you can create a wrapper class to the `BlobContainerConfiguration` for a type-safe configuration option: |
|||
|
|||
````csharp |
|||
public class MyCustomBlobProviderConfiguration |
|||
{ |
|||
public string MyOption1 |
|||
{ |
|||
get => _containerConfiguration |
|||
.GetConfiguration<string>("MyCustomBlobProvider.MyOption1"); |
|||
set => _containerConfiguration |
|||
.SetConfiguration("MyCustomBlobProvider.MyOption1", value); |
|||
} |
|||
|
|||
private readonly BlobContainerConfiguration _containerConfiguration; |
|||
|
|||
public MyCustomBlobProviderConfiguration( |
|||
BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
_containerConfiguration = containerConfiguration; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Then you can change the `MyBlobContainerConfigurationExtensions` class like that: |
|||
|
|||
````csharp |
|||
public static class MyBlobContainerConfigurationExtensions |
|||
{ |
|||
public static BlobContainerConfiguration UseMyCustomBlobProvider( |
|||
this BlobContainerConfiguration containerConfiguration, |
|||
Action<MyCustomBlobProviderConfiguration> configureAction) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(MyCustomBlobProvider); |
|||
|
|||
configureAction.Invoke( |
|||
new MyCustomBlobProviderConfiguration(containerConfiguration) |
|||
); |
|||
|
|||
return containerConfiguration; |
|||
} |
|||
|
|||
public static MyCustomBlobProviderConfiguration GetMyCustomBlobProviderConfiguration( |
|||
this BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
return new MyCustomBlobProviderConfiguration(containerConfiguration); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* Added an action parameter to the `UseMyCustomBlobProvider` method to allow developers to set the additional options. |
|||
* Added a new `GetMyCustomBlobProviderConfiguration` method to be used inside `MyCustomBlobProvider` class to obtain the configured values. |
|||
|
|||
Then anyone can set the `MyOption1` as shown below: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseMyCustomBlobProvider(provider => |
|||
{ |
|||
provider.MyOption1 = "my value"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
Finally, you can access to the extra options using the `GetMyCustomBlobProviderConfiguration` method: |
|||
|
|||
````csharp |
|||
public class MyCustomBlobProvider : BlobProviderBase, ITransientDependency |
|||
{ |
|||
public override Task SaveAsync(BlobProviderSaveArgs args) |
|||
{ |
|||
var config = args.Configuration.GetMyCustomBlobProviderConfiguration(); |
|||
var value = config.MyOption1; |
|||
|
|||
//... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## Contribute? |
|||
|
|||
If you create a new provider and you think it can be useful for other developers, please consider to [contribute](Contribution/Index.md) to the ABP Framework on GitHub. |
|||
@ -0,0 +1,96 @@ |
|||
# BLOB Storing Database Provider |
|||
|
|||
BLOB Storing Database Storage Provider can store BLOBs in a relational or non-relational database. |
|||
|
|||
There are two database providers implemented; |
|||
|
|||
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) package implements for [EF Core](Entity-Framework-Core.md), so it can store BLOBs in [any DBMS supported](https://docs.microsoft.com/en-us/ef/core/providers/) by the EF Core. |
|||
* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) package implements for [MongoDB](MongoDB.md). |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a database as the storage provider. |
|||
|
|||
## Installation |
|||
|
|||
### Automatic Installation |
|||
|
|||
If you've created your solution based on the [application startup template](Startup-Templates/Application.md), you can use the `abp add-module` [CLI](CLI.md) command to automatically add related packages to your solution. |
|||
|
|||
Open a command prompt (terminal) in the folder containing your solution (`.sln`) file and run the following command: |
|||
|
|||
````bash |
|||
abp add-module Volo.Abp.BlobStoring.Database |
|||
```` |
|||
|
|||
This command adds all the NuGet packages to corresponding layers of your solution. If you are using EF Core, it adds necessary configuration, adds a new database migration and updates the database. |
|||
|
|||
### Manual Installation |
|||
|
|||
Here, all the NuGet packages defined by this provider; |
|||
|
|||
* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Domain.Shared) |
|||
* [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) |
|||
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) |
|||
* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) |
|||
|
|||
You can only install Volo.Abp.BlobStoring.Database.EntityFrameworkCore or Volo.Abp.BlobStoring.Database.MongoDB (based on your preference) since they depends on the other packages. |
|||
|
|||
After installation, add `DepenedsOn` attribute to your related [module](Module-Development-Basics.md). Here, the list of module classes defined by the related NuGet packages listed above: |
|||
|
|||
* `BlobStoringDatabaseDomainModule` |
|||
* `BlobStoringDatabaseDomainSharedModule` |
|||
* `BlobStoringDatabaseEntityFrameworkCoreModule` |
|||
* `BlobStoringDatabaseMongoDbModule` |
|||
|
|||
Whenever you add a NuGet package to a project, also add the module class dependency. |
|||
|
|||
If you are using EF Core, you also need to configure your **Migration DbContext** to add BLOB storage tables to your database schema. Call `builder.ConfigureBlobStoring()` extension method inside the `OnModelCreating` method to include mappings to your DbContext. Then you can use the standard `Add-Migration` and `Update-Database` [commands](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create necessary tables in your database. |
|||
|
|||
## Configuration |
|||
|
|||
### Connection String |
|||
|
|||
If you will use your `Default` connection string, you don't need to any additional configuration. |
|||
|
|||
If you want to use a separate database for BLOB storage, use the `AbpBlobStoring` as the [connection string](Connection-Strings.md) name in your configuration file (`appsettings.json`). In this case, also read the [EF Core Migrations](Entity-Framework-Core-Migrations.md) document to learn how to create and use a different database for a desired module. |
|||
|
|||
### Configuring the Containers |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the database storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseDatabase(); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
## Additional Information |
|||
|
|||
It is expected to use the [BLOB Storing services](Blob-Storing.md) to use the BLOB storing system. However, if you want to work on the database tables/entities, you can use the following information. |
|||
|
|||
### Entities |
|||
|
|||
Entities defined for this module: |
|||
|
|||
* `DatabaseBlobContainer` (aggregate root) represents a container stored in the database. |
|||
* `DatabaseBlob` (aggregate root) represents a BLOB in the database. |
|||
|
|||
See the [entities document](Entities.md) to learn what is an entity and aggregate root. |
|||
|
|||
### Repositories |
|||
|
|||
* `IDatabaseBlobContainerRepository` |
|||
* `IDatabaseBlobRepository` |
|||
|
|||
You can also use `IRepository<DatabaseBlobContainer, Guid>` and `IRepository<DatabaseBlob, Guid>` to take the power of IQueryable. See the [repository document](Repositories.md) for more. |
|||
|
|||
### Other Services |
|||
|
|||
* `DatabaseBlobProvider` is the main service that implements the database BLOB storage provider, if you want to override/replace it via [dependency injection](Dependency-Injection.md) (don't replace `IBlobProvider` interface, but replace `DatabaseBlobProvider` class). |
|||
@ -0,0 +1,59 @@ |
|||
# BLOB Storing File System Provider |
|||
|
|||
File System Storage Provider is used to store BLOBs in the local file system as standard files inside a folder. |
|||
|
|||
> Read the [BLOB Storing document](Blob-Storing.md) to understand how to use the BLOB storing system. This document only covers how to configure containers to use the file system. |
|||
|
|||
## Installation |
|||
|
|||
Use the ABP CLI to add [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring.FileSystem` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring.FileSystem` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring.FileSystem](https://www.nuget.org/packages/Volo.Abp.BlobStoring.FileSystem) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringFileSystemModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## Configuration |
|||
|
|||
Configuration is done in the `ConfigureServices` method of your [module](Module-Development-Basics.md) class, as explained in the [BLOB Storing document](Blob-Storing.md). |
|||
|
|||
**Example: Configure to use the File System storage provider by default** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
container.UseFileSystem(fileSystem => |
|||
{ |
|||
fileSystem.BasePath = "C:\\my-files"; |
|||
}); |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
`UseFileSystem` extension method is used to set the File System Provider for a container and configure the file system options. |
|||
|
|||
> See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. |
|||
|
|||
### Options |
|||
|
|||
* **BasePath** (string): The base folder path to store BLOBs. It is required to set this option. |
|||
* **AppendContainerNameToBasePath** (bool; default: `true`): Indicates whether to create a folder with the container name inside the base folder. If you store multiple containers in the same `BaseFolder`, leave this as `true`. Otherwise, you can set it to `false` if you don't like an unnecessarily deeper folder hierarchy. |
|||
|
|||
## File Path Calculation |
|||
|
|||
File System Provider organizes BLOB files inside folders and implements some conventions. The full path of a BLOB file is determined by the following rules by default: |
|||
|
|||
* It starts with the `BasePath` configured as shown above. |
|||
* Appends `host` folder if [current tenant](Multi-Tenancy.md) is `null` (or multi-tenancy is disabled for the container - see the [BLOB Storing document](Blob-Storing.md) to learn how to disable multi-tenancy for a container). |
|||
* Appends `tenants/<tenant-id>` folder if current tenant is not `null`. |
|||
* Appends the container's name if `AppendContainerNameToBasePath` is `true`. If container name contains `/`, this will result with nested folders. |
|||
* Appends the BLOB name. If the BLOB name contains `/` it creates folders. If the BLOB name contains `.` it will have a file extension. |
|||
|
|||
## Extending the File System BLOB Provider |
|||
|
|||
* `FileSystemBlobProvider` is the main service that implements the File System storage. You can inherit from this class and [override](Customizing-Application-Modules-Overriding-Services.md) methods to customize it. |
|||
|
|||
* The `IBlobFilePathCalculator` service is used to calculate the file paths. Default implementation is the `DefaultBlobFilePathCalculator`. You can replace/override it if you want to customize the file path calculation. |
|||
@ -0,0 +1,305 @@ |
|||
# BLOB Storing |
|||
|
|||
It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. |
|||
|
|||
A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. |
|||
|
|||
The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; |
|||
|
|||
* You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. |
|||
* You can then **easily change** your BLOB storage without changing your application code. |
|||
* If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. |
|||
|
|||
ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](Multi-Tenancy.md). |
|||
|
|||
## BLOB Storage Providers |
|||
|
|||
The ABP Framework has already the following storage provider implementations; |
|||
|
|||
* [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files. |
|||
* [Database](Blob-Storing-Database.md): Stores BLOBs in a database. |
|||
* [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/). |
|||
|
|||
More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework. |
|||
|
|||
Multiple providers **can be used together** by the help of the **container system**, where each container can uses a different provider. |
|||
|
|||
> BLOB storing system can not work unless you **configure a storage provider**. Refer to the linked documents for the storage provider configurations. |
|||
|
|||
## Installation |
|||
|
|||
[Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) is the main package that defines the BLOB storing services. You can use this package to use the BLOB Storing system without depending a specific storage provider. |
|||
|
|||
Use the ABP CLI to add this package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI), if you haven't installed it. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.BlobStoring` package. |
|||
* Run `abp add-package Volo.Abp.BlobStoring` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.BlobStoring](https://www.nuget.org/packages/Volo.Abp.BlobStoring) NuGet package to your project and add `[DependsOn(typeof(AbpBlobStoringModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project. |
|||
|
|||
## The IBlobContainer |
|||
|
|||
`IBlobContainer` is the main interface to store and read BLOBs. Your application may have multiple containers and each container can be separately configured. But, there is a **default container** that can be simply used by [injecting](Dependency-Injection.md) the `IBlobContainer`. |
|||
|
|||
**Example: Simply save and read bytes of a named BLOB** |
|||
|
|||
````csharp |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.BlobStoring; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IBlobContainer _blobContainer; |
|||
|
|||
public MyService(IBlobContainer blobContainer) |
|||
{ |
|||
_blobContainer = blobContainer; |
|||
} |
|||
|
|||
public async Task SaveBytesAsync(byte[] bytes) |
|||
{ |
|||
await _blobContainer.SaveAsync("my-blob-1", bytes); |
|||
} |
|||
|
|||
public async Task<byte[]> GetBytesAsync() |
|||
{ |
|||
return await _blobContainer.GetAllBytesOrNullAsync("my-blob-1"); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This service saves the given bytes with the `my-blob-1` name and then gets the previously saved bytes with the same name. |
|||
|
|||
> A BLOB is a named object and **each BLOB should have a unique name**, which is an arbitrary string. |
|||
|
|||
`IBlobContainer` can work with `Stream` and `byte[]` objects, which will be detailed in the next sections. |
|||
|
|||
### Saving BLOBs |
|||
|
|||
`SaveAsync` method is used to save a new BLOB or replace an existing BLOB. It can save a `Stream` by default, but there is a shortcut extension method to save byte arrays. |
|||
|
|||
`SaveAsync` gets the following parameters: |
|||
|
|||
* **name** (string): Unique name of the BLOB. |
|||
* **stream** (Stream) or **bytes** (byte[]): The stream to read the BLOB content or a byte array. |
|||
* **overrideExisting** (bool): Set `true` to replace the BLOB content if it does already exists. Default value is `false` and throws `BlobAlreadyExistsException` if there is already a BLOB in the container with the same name. |
|||
|
|||
### Reading/Getting BLOBs |
|||
|
|||
* `GetAsync`: Only gets a BLOB name and returns a `Stream` object that can be used to read the BLOB content. Always **dispose the stream** after using it. This method throws exception, if it can not find the BLOB with the given name. |
|||
* `GetOrNullAsync`: In opposite to the `GetAsync` method, this one returns `null` if there is no BLOB found with the given name. |
|||
* `GetAllBytesAsync`: Returns a `byte[]` instead of a `Stream`. Still throws exception if can not find the BLOB with the given name. |
|||
* `GetAllBytesOrNullAsync`: In opposite to the `GetAllBytesAsync` method, this one returns `null` if there is no BLOB found with the given name. |
|||
|
|||
### Deleting BLOBs |
|||
|
|||
`DeleteAsync` method gets a BLOB name and deletes the BLOB data. It doesn't throw any exception if given BLOB was not found. Instead, it returns a `bool` indicating that the BLOB was actually deleted or not, if you care about it. |
|||
|
|||
### Other Methods |
|||
|
|||
* `ExistsAsync` method simply checks if there is a BLOB in the container with the given name. |
|||
|
|||
### About Naming the BLOBs |
|||
|
|||
There is not a rule for naming the BLOBs. A BLOB name is just a string that is unique per container (and per tenant - see the "*Multi-Tenancy*" section). However, different storage providers may conventionally implement some practices. For example, the [File System Provider](Blob-Storing-File-System.md) use directory separators (`/`) and file extensions in your BLOB name (if your BLOB name is `images/common/x.png` then it is saved as `x.png` in the `images/common` folder inside the root container folder). |
|||
|
|||
## Typed IBlobContainer |
|||
|
|||
Typed BLOB container system is a way of creating and managing **multiple containers** in an application; |
|||
|
|||
* **Each container is separately stored**. That means the BLOB names should be unique in a container and two BLOBs with the same name can live in different containers without effecting each other. |
|||
* **Each container can be separately configured**, so each container can use a different storage provider based on your configuration. |
|||
|
|||
To create a typed container, you need to create a simple class decorated with the `BlobContainerName` attribute: |
|||
|
|||
````csharp |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace AbpDemo |
|||
{ |
|||
[BlobContainerName("profile-pictures")] |
|||
public class ProfilePictureContainer |
|||
{ |
|||
|
|||
} |
|||
} |
|||
```` |
|||
|
|||
> If you don't use the `BlobContainerName` attribute, ABP Framework uses the full name of the class (with namespace), but it is always recommended to use a container name which is stable and does not change even if you rename the class. |
|||
|
|||
Once you create the container class, you can inject `IBlobContainer<T>` for your container type. |
|||
|
|||
**Example: An [application service](Application-Services.md) to save and read profile picture of the [current user](CurrentUser.md)** |
|||
|
|||
````csharp |
|||
[Authorize] |
|||
public class ProfileAppService : ApplicationService |
|||
{ |
|||
private readonly IBlobContainer<ProfilePictureContainer> _blobContainer; |
|||
|
|||
public ProfileAppService(IBlobContainer<ProfilePictureContainer> blobContainer) |
|||
{ |
|||
_blobContainer = blobContainer; |
|||
} |
|||
|
|||
public async Task SaveProfilePictureAsync(byte[] bytes) |
|||
{ |
|||
var blobName = CurrentUser.GetId().ToString(); |
|||
await _blobContainer.SaveAsync(blobName, bytes); |
|||
} |
|||
|
|||
public async Task<byte[]> GetProfilePictureAsync() |
|||
{ |
|||
var blobName = CurrentUser.GetId().ToString(); |
|||
return await _blobContainer.GetAllBytesOrNullAsync(blobName); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`IBlobContainer<T>` has the same methods with the `IBlobContainer`. |
|||
|
|||
> It is a good practice to **always use a typed container while developing re-usable modules**, so the final application can configure the provider for your container without effecting the other containers. |
|||
|
|||
### The Default Container |
|||
|
|||
If you don't use the generic argument and directly inject the `IBlobContainer` (as explained before), you get the default container. Another way of injecting the default container is using `IBlobContainer<DefaultContainer>`, which returns exactly the same container. |
|||
|
|||
The name of the default container is `Default`. |
|||
|
|||
### Named Containers |
|||
|
|||
Typed containers are just shortcuts for named containers. You can inject and use the `IBlobContainerFactory` to get a BLOB container by its name: |
|||
|
|||
````csharp |
|||
public class ProfileAppService : ApplicationService |
|||
{ |
|||
private readonly IBlobContainer _blobContainer; |
|||
|
|||
public ProfileAppService(IBlobContainerFactory blobContainerFactory) |
|||
{ |
|||
_blobContainer = blobContainerFactory.Create("profile-pictures"); |
|||
} |
|||
|
|||
//... |
|||
} |
|||
```` |
|||
|
|||
## IBlobContainerFactory |
|||
|
|||
`IBlobContainerFactory` is the service that is used to create the BLOB containers. One example was shown above. |
|||
|
|||
**Example: Create a container by name** |
|||
|
|||
````csharp |
|||
var blobContainer = blobContainerFactory.Create("profile-pictures"); |
|||
```` |
|||
|
|||
**Example: Create a container by type** |
|||
|
|||
````csharp |
|||
var blobContainer = blobContainerFactory.Create<ProfilePictureContainer>(); |
|||
```` |
|||
|
|||
> You generally don't need to use the `IBlobContainerFactory` since it is used internally, when you inject a `IBlobContainer` or `IBlobContainer<T>`. |
|||
|
|||
## Configuring the Containers |
|||
|
|||
Containers should be configured before using them. The most fundamental configuration is to **select a BLOB storage provider** (see the "*BLOB Storage Providers*" section above). |
|||
|
|||
`AbpBlobStoringOptions` is the [options class](Options.md) to configure the containers. You can configure the options inside the `ConfigureServices` method of your [module](Module-Development-Basics.md). |
|||
|
|||
### Configure a Single Container |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<ProfilePictureContainer>(container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
This example configures the `ProfilePictureContainer`. You can also configure by the container name: |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure("profile-pictures", container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
### Configure the Default Container |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureDefault(container => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> There is a special case about the default container; If you don't specify a configuration for a container, it **fallbacks to the default container configuration**. This is a good way to configure defaults for all containers and specialize configuration for a specific container when needed. |
|||
|
|||
### Configure All Containers |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.ConfigureAll((containerName, containerConfiguration) => |
|||
{ |
|||
//TODO... |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
This is a way to configure all the containers. |
|||
|
|||
> The main difference from configuring the default container is that `ConfigureAll` overrides the configuration even if it was specialized for a specific container. |
|||
|
|||
## Multi-Tenancy |
|||
|
|||
If your application is set as multi-tenant, the BLOB Storage system **works seamlessly with the [multi-tenancy](Multi-Tenancy.md)**. All the providers implement multi-tenancy as a standard feature. They **isolate BLOBs** of different tenants from each other, so they can only access to their own BLOBs. It means you can use the **same BLOB name for different tenants**. |
|||
|
|||
If your application is multi-tenant, you may want to control **multi-tenancy behavior** of the containers individually. For example, you may want to **disable multi-tenancy** for a specific container, so the BLOBs inside it will be **available to all the tenants**. This is a way to share BLOBs among all tenants. |
|||
|
|||
**Example: Disable multi-tenancy for a specific container** |
|||
|
|||
````csharp |
|||
Configure<AbpBlobStoringOptions>(options => |
|||
{ |
|||
options.Containers.Configure<ProfilePictureContainer>(container => |
|||
{ |
|||
container.IsMultiTenant = false; |
|||
}); |
|||
}); |
|||
```` |
|||
|
|||
> If your application is not multi-tenant, no worry, it works as expected. You don't need to configure the `IsMultiTenant` option. |
|||
|
|||
## Extending the BLOB Storing System |
|||
|
|||
Most of the times, you won't need to customize the BLOB storage system except [creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md). However, you can replace any service (injected via [dependency injection](Dependency-Injection.md)), if you need. Here, some other services not mentioned above, but you may want to know: |
|||
|
|||
* `IBlobProviderSelector` is used to get a `IBlobProvider` instance by a container name. Default implementation (`DefaultBlobProviderSelector`) selects the provider using the configuration. |
|||
* `IBlobContainerConfigurationProvider` is used to get the `BlobContainerConfiguration` for a given container name. Default implementation (`DefaultBlobContainerConfigurationProvider`) gets the configuration from the `AbpBlobStoringOptions` explained above. |
|||
|
|||
## BLOB Storing vs File Management System |
|||
|
|||
Notice that BLOB storing is not a file management system. It is a low level system that is used to save, get and delete named BLOBs. It doesn't provide a hierarchical structure like directories, you may expect from a typical file system. |
|||
|
|||
If you want to create folders and move files between folders, assign permissions to files and share files between users then you need to implement your own application on top of the BLOB Storage system. |
|||
|
|||
## See Also |
|||
|
|||
* [Creating a custom BLOB storage provider](Blob-Storing-Custom-Provider.md) |
|||
@ -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 very common to retrieve the information about the logged in user in a web application. The current user is the active user related to the current request in a web application. |
|||
|
|||
## ICurrentUser |
|||
|
|||
`ICurrentUser` is the main service to get info about the current active 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 are the fundamental properties of the `ICurrentUser` interface: |
|||
|
|||
* **IsAuthenticated** (bool): Returns `true` if the current user has logged in (authenticated). If the user has not logged in then `Id` and `UserName` returns `null`. |
|||
* **Id** (Guid?): Id of the current user. Returns `null`, if the current user has not logged in. |
|||
* **UserName** (string): User name of the current user. Returns `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. Returns `null`, if the current user is not assigned to a tenant. |
|||
* **Email** (string): Email address of the current user.Returns `null`, if the current user has not logged in or not set an email address. |
|||
* **EmailVerified** (bool): Returns `true`, if the phone number of the current user has been verified. |
|||
* **PhoneNumber** (string): Phone number of the current user. Returns `null`, if the current user has not logged in or not set a phone number. |
|||
* **PhoneNumberVerified** (bool): Returns `true`, if the phone number of the current user has been verified. |
|||
* **Roles** (string[]): Roles of the current user. Returns 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 the claims of the current 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. 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 is in the specified role. |
|||
|
|||
Beside these standard methods, there are some extension methods: |
|||
|
|||
* **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. If the current user has not logged in, it throws an exception (instead of returning `null`) . Use this only if you are sure that the user has already authenticated in your code context. |
|||
|
|||
### Authentication & Authorization |
|||
|
|||
`ICurrentUser` works independently of how the user is authenticated or authorized. It seamlessly 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 or change, except at 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 will be restored to the original value after the `using` scope ends. |
|||
|
|||
This can be a way to simulate a user login for a scope of the application code, however try to use it carefully. |
|||
|
|||
## 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,40 @@ |
|||
# Badges |
|||
|
|||
## Introduction |
|||
|
|||
`abp-badge` and `abp-badge-pill` are abp tags for badges. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<span abp-badge="Primary">Primary</span> |
|||
<a abp-badge="Info" href="#">Info</a> |
|||
<a abp-badge-pill="Danger" href="#">Danger</a> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [badges demo page](https://bootstrap-taghelpers.abp.io/Components/Badges) to see it in action. |
|||
|
|||
### Values |
|||
|
|||
* Indicates the type of the badge. Should be one of the following values: |
|||
|
|||
* `_` (default value) |
|||
* `Default` (default value) |
|||
* `Primary` |
|||
* `Secondary` |
|||
* `Success` |
|||
* `Danger` |
|||
* `Warning` |
|||
* `Info` |
|||
* `Light` |
|||
* `Dark` |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
<span abp-badge-pill="Danger">Danger</span> |
|||
```` |
|||
@ -0,0 +1,126 @@ |
|||
# Borders |
|||
|
|||
## Introduction |
|||
|
|||
`abp-border` is a main element for border styling. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<span abp-border="Default"></span> |
|||
<span abp-border="Top"></span> |
|||
<span abp-border="Right"></span> |
|||
<span abp-border="Bottom"></span> |
|||
<span abp-border="Left"></span> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [borders demo page](https://bootstrap-taghelpers.abp.io/Components/Borders) to see it in action. |
|||
|
|||
## Values |
|||
|
|||
A value indicates type, position and the color of the border. Should be one of the following values: |
|||
|
|||
* `Default` |
|||
* `_0` |
|||
* `Primary` |
|||
* `Secondary` |
|||
* `Success` |
|||
* `Danger` |
|||
* `Warning` |
|||
* `Info` |
|||
* `Light` |
|||
* `Dark` |
|||
* `White` |
|||
* `Primary_0` |
|||
* `Secondary_0` |
|||
* `Success_0` |
|||
* `Danger_0` |
|||
* `Warning_0` |
|||
* `Info_0` |
|||
* `Light_0` |
|||
* `Dark_0` |
|||
* `White_0` |
|||
* `Top` |
|||
* `Top_0` |
|||
* `Top_Primary` |
|||
* `Top_Secondary` |
|||
* `Top_Success` |
|||
* `Top_Danger` |
|||
* `Top_Warning` |
|||
* `Top_Info` |
|||
* `Top_Light` |
|||
* `Top_Dark` |
|||
* `Top_White` |
|||
* `Top_Primary_0` |
|||
* `Top_Secondary_0` |
|||
* `Top_Success_0` |
|||
* `Top_Danger_0` |
|||
* `Top_Warning_0` |
|||
* `Top_Info_0` |
|||
* `Top_Light_0` |
|||
* `Top_Dark_0` |
|||
* `Top_White_0` |
|||
* `Right` |
|||
* `Right_0` |
|||
* `Right_Primary` |
|||
* `Right_Secondary` |
|||
* `Right_Success` |
|||
* `Right_Danger` |
|||
* `Right_Warning` |
|||
* `Right_Info` |
|||
* `Right_Light` |
|||
* `Right_Dark` |
|||
* `Right_White` |
|||
* `Right_Primary_0` |
|||
* `Right_Secondary_0` |
|||
* `Right_Success_0` |
|||
* `Right_Danger_0` |
|||
* `Right_Warning_0` |
|||
* `Right_Info_0` |
|||
* `Right_Light_0` |
|||
* `Right_Dark_0` |
|||
* `Right_White_0` |
|||
* `Left` |
|||
* `Left_0` |
|||
* `Left_Primary` |
|||
* `Left_Secondary` |
|||
* `Left_Success` |
|||
* `Left_Danger` |
|||
* `Left_Warning` |
|||
* `Left_Info` |
|||
* `Left_Light` |
|||
* `Left_Dark` |
|||
* `Left_White` |
|||
* `Left_Primary_0` |
|||
* `Left_Secondary_0` |
|||
* `Left_Success_0` |
|||
* `Left_Danger_0` |
|||
* `Left_Warning_0` |
|||
* `Left_Info_0` |
|||
* `Left_Light_0` |
|||
* `Left_Dark_0` |
|||
* `Left_White_0` |
|||
* `Bottom` |
|||
* `Bottom_0` |
|||
* `Bottom_Primary` |
|||
* `Bottom_Secondary` |
|||
* `Bottom_Success` |
|||
* `Bottom_Danger` |
|||
* `Bottom_Warning` |
|||
* `Bottom_Info` |
|||
* `Bottom_Light` |
|||
* `Bottom_Dark` |
|||
* `Bottom_White` |
|||
* `Bottom_Primary_0` |
|||
* `Bottom_Secondary_0` |
|||
* `Bottom_Success_0` |
|||
* `Bottom_Danger_0` |
|||
* `Bottom_Warning_0` |
|||
* `Bottom_Info_0` |
|||
* `Bottom_Light_0` |
|||
* `Bottom_Dark_0` |
|||
* `Bottom_White_0` |
|||
@ -0,0 +1,25 @@ |
|||
# Breadcrumbs |
|||
|
|||
## Introduction |
|||
|
|||
`abp-breadcrumb` is the main container for breadcrumb items. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-breadcrumb> |
|||
<abp-breadcrumb-item href="#" title="Home" /> |
|||
<abp-breadcrumb-item href="#" title="Library"/> |
|||
<abp-breadcrumb-item title="Page"/> |
|||
</abp-breadcrumb> |
|||
```` |
|||
|
|||
## Demo |
|||
|
|||
See the [breadcrumbs demo page](https://bootstrap-taghelpers.abp.io/Components/Breadcrumbs) to see it in action. |
|||
|
|||
## abp-breadcrumb-item Attributes |
|||
|
|||
- **title**: Sets the text of the breadcrumb item. |
|||
- **active**: Sets the active breadcrumb item. Last item is active by default, if no other item is active. |
|||
- **href**: A value indicates if an `abp-breadcrumb-item` has a link. Should be a string link value. |
|||
@ -0,0 +1,114 @@ |
|||
# Navs |
|||
|
|||
## Introduction |
|||
|
|||
`abp-nav` is the basic tag helper component derived from bootstrap nav element. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-nav nav-style="Pill" align="Center"> |
|||
<abp-nav-item> |
|||
<a abp-nav-link active="true" href="#">Active</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">Longer nav link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link disabled="true" href="#">disabled</a> |
|||
</abp-nav-item> |
|||
</abp-nav> |
|||
```` |
|||
|
|||
## Demo |
|||
|
|||
See the [navs demo page](https://bootstrap-taghelpers.abp.io/Components/Navs) to see it in action. |
|||
|
|||
## abp-nav Attributes |
|||
|
|||
- **nav-style**: The value indicates the positioning and style of the containing items. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Vertical` |
|||
* `Pill` |
|||
* `PillVertical` |
|||
- **align:** The value indicates the alignment of the containing items: |
|||
* `Default` (default value) |
|||
* `Start` |
|||
* `Center` |
|||
* `End` |
|||
|
|||
### abp-nav-bar Attributes |
|||
|
|||
- **nav-style**: The value indicates the color layout of the base navigation bar. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Dark` |
|||
* `Light` |
|||
* `Dark_Primary` |
|||
* `Dark_Secondary` |
|||
* `Dark_Success` |
|||
* `Dark_Danger` |
|||
* `Dark_Warning` |
|||
* `Dark_Info` |
|||
* `Dark_Dark` |
|||
* `Dark_Link` |
|||
* `Light_Primary` |
|||
* `Light_Secondary` |
|||
* `Light_Success` |
|||
* `Light_Danger` |
|||
* `Light_Warning` |
|||
* `Light_Info` |
|||
* `Light_Dark` |
|||
* `Light_Link` |
|||
- **size:** The value indicates size of the base navigation bar. Should be one of the following values: |
|||
* `Default` (default value) |
|||
* `Sm` |
|||
* `Md` |
|||
* `Lg` |
|||
* `Xl` |
|||
|
|||
### abp-nav-item Attributes |
|||
|
|||
**dropdown**: A value that sets the navigation item to be a dropdown menu if provided. Can be one of the following values: |
|||
|
|||
* `false` (default value) |
|||
* `true` |
|||
|
|||
Example: |
|||
|
|||
````csharp |
|||
<abp-nav-bar size="Lg" navbar-style="Dark_Warning"> |
|||
<a abp-navbar-brand href="#">Navbar</a> |
|||
<abp-navbar-toggle> |
|||
<abp-navbar-nav> |
|||
<abp-nav-item active="true"> |
|||
<a abp-nav-link href="#">Home <span class="sr-only">(current)</span></a> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link href="#">Link</a> |
|||
</abp-nav-item> |
|||
<abp-nav-item dropdown="true"> |
|||
<abp-dropdown> |
|||
<abp-dropdown-button nav-link="true" text="Dropdown" /> |
|||
<abp-dropdown-menu> |
|||
<abp-dropdown-header>Dropdown header</abp-dropdown-header> |
|||
<abp-dropdown-item href="#" active="true">Action</abp-dropdown-item> |
|||
<abp-dropdown-item href="#" disabled="true">Another disabled action</abp-dropdown-item> |
|||
<abp-dropdown-item href="#">Something else here</abp-dropdown-item> |
|||
<abp-dropdown-divider /> |
|||
<abp-dropdown-item href="#">Separated link</abp-dropdown-item> |
|||
</abp-dropdown-menu> |
|||
</abp-dropdown> |
|||
</abp-nav-item> |
|||
<abp-nav-item> |
|||
<a abp-nav-link disabled="true" href="#">Disabled</a> |
|||
</abp-nav-item> |
|||
</abp-navbar-nav> |
|||
<span abp-navbar-text> |
|||
Sample Text |
|||
</span> |
|||
</abp-navbar-toggle> |
|||
</abp-nav-bar> |
|||
```` |
|||
@ -0,0 +1,61 @@ |
|||
# Tables |
|||
|
|||
## Introduction |
|||
|
|||
`abp-table` is the basic tag component for tables in abp. |
|||
|
|||
Basic usage: |
|||
|
|||
````csharp |
|||
<abp-table hoverable-rows="true" responsive-sm="true"> |
|||
<thead> |
|||
<tr> |
|||
<th scope="Column">#</th> |
|||
<th scope="Column">First</th> |
|||
<th scope="Column">Last</th> |
|||
<th scope="Column">Handle</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr> |
|||
<th scope="Row">1</th> |
|||
<td>Mark</td> |
|||
<td>Otto</td> |
|||
<td table-style="Danger">mdo</td> |
|||
</tr> |
|||
<tr table-style="Warning"> |
|||
<th scope="Row">2</th> |
|||
<td>Jacob</td> |
|||
<td>Thornton</td> |
|||
<td>fat</td> |
|||
</tr> |
|||
<tr> |
|||
<th scope="Row">3</th> |
|||
<td table-style="Success">Larry</td> |
|||
<td>the Bird</td> |
|||
<td>twitter</td> |
|||
</tr> |
|||
</tbody> |
|||
</abp-table> |
|||
```` |
|||
|
|||
|
|||
|
|||
## Demo |
|||
|
|||
See the [tables demo page](https://bootstrap-taghelpers.abp.io/Components/Tables) to see it in action. |
|||
|
|||
## abp-table Attributes |
|||
|
|||
- **responsive**: Used to create responsive tables up to a particular breakpoint. see [breakpoint specific](https://getbootstrap.com/docs/4.1/content/tables/#breakpoint-specific) for more information. |
|||
- **responsive-sm**: If not set to false, sets the table responsiveness for small screen devices. |
|||
- **responsive-md**: If not set to false, sets the table responsiveness for medium screen devices. |
|||
- **responsive-lg**: If not set to false, sets the table responsiveness for large screen devices. |
|||
- **responsive-xl**: If not set to false, sets the table responsiveness for extra large screen devices. |
|||
- **dark-theme**: If set to true, sets the table color theme to dark. |
|||
- **striped-rows**: If set to true, adds zebra-striping to table rows. |
|||
- **hoverable-rows**: If set to true, adds hover state to table rows. |
|||
- **border-style**: Sets the border style of the table. Should be one of the following values: |
|||
- `Default` (default) |
|||
- `Bordered` |
|||
- `Borderless` |
|||
@ -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 |
@ -1,8 +1,8 @@ |
|||
namespace Volo.Abp.BlobStoring |
|||
{ |
|||
[BlobContainerName("Default")] |
|||
[BlobContainerName(Name)] |
|||
public class DefaultContainer |
|||
{ |
|||
|
|||
public const string Name = "Default"; |
|||
} |
|||
} |
|||
@ -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 |
|||
|
|||
@ -1,6 +1,8 @@ |
|||
@page |
|||
@using Microsoft.AspNetCore.Mvc.Localization |
|||
@using Volo.Abp.Account.Localization |
|||
@using Volo.Abp.Account.Web.Pages.Account |
|||
@inherits AccountPage |
|||
@model SendSecurityCodeModel |
|||
@inject IHtmlLocalizer<AccountResource> L |
|||
<h2>Send security code!</h2> |
|||
<p>TODO: This page is under construction.</p> |
|||
<p>TODO: This page is under construction.</p> |
|||
|
|||