Merge branch 'dev' into bs5

pull/10355/head
maliming 4 years ago
commit 56b262dd8e
No known key found for this signature in database
GPG Key ID: 096224957E51C89E

@ -346,9 +346,12 @@
"Volo.AbpIo.Commercial:030002": "Once activated, trial license cannot be set to requested!",
"Volo.AbpIo.Commercial:030003": "There is no such status!",
"Volo.AbpIo.Commercial:030004": "Status could not be changed due to an unexpected error!",
"Volo.AbpIo.Commercial:030005": "Start date and end date cannot be given when the trial license is in the requested state!",
"Volo.AbpIo.Commercial:030005": "Start and end date can be updated when the trial license is in the -activated- status!",
"Volo.AbpIo.Commercial:030006": "End date must always be greater than start date!",
"Volo.AbpIo.Commercial:030007": "This trial license has already been activated once!",
"Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!",
"Volo.AbpIo.Commercial:030009": "User not found!",
"Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!",
"Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!"
}
}

@ -230,7 +230,7 @@
"HowCanIRefundVatExplanation2": "Log in into your <a href=\"https://secure.2checkout.com/cpanel/login.php\" target=\"_blank\">2Checkout</a> account",
"HowCanIRefundVatExplanation3": "Find the appropriate order and press \"Refund Belated VAT\" (enter your VAT ID)",
"HowCanIGetMyInvoice": "How can I get my invoice?",
"HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: PayU and 2Checkout. If you purchase your license through 2Checkout gateway, it sends the PDF invoice to your email address, see <a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work\">2Checkout invoicing.</a> If you purchase through the PayU gateway or via bank wire transfer, we will prepare and send your invoice. You can request your invoice from the <a href=\"{0}\">organization management page.</a>",
"HowCanIGetMyInvoiceExplanation": "There are 2 payment gateways for purchasing a license: Iyzico and 2Checkout. If you purchase your license through the 2Checkout gateway, it sends the PDF invoice to your email address, see <a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work\">2Checkout invoicing.</a> If you purchase through the Iyzico gateway, with custom purchase link or via bank wire transfer, we will prepare and send your invoice. You can request or download your invoice from the <a href=\"{0}\">organization management page</a>. Before contacting us for the invoice, check your organization management page!",
"Forum": "Forum",
"SupportExplanation": "ABP Commercial licenses provides a premium forum support by a team consists of the ABP Framework experts.",
"PrivateTicket": "Private Ticket",

@ -230,7 +230,7 @@
"HowCanIRefundVatExplanation2": "登录到你的<a href=\"https://secure.2checkout.com/cpanel/login.php\" target=\"_blank\">2Checkout</a>账户",
"HowCanIRefundVatExplanation3": "找到适当的订单,然后按\"退还已延期的增值税\"输入你的增值税ID",
"HowCanIGetMyInvoice": "我如何获得发票?",
"HowCanIGetMyInvoiceExplanation": "有两个用于购买许可的支付网关:PayU和2Checkout. 如果你通过2Checkout网关购买许可,则它将PDF发票发送到你的电子邮件地址,请参阅<a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work>2Checkout发票.</a>如果你是通过PayU网关或通过银行电汇购买的,我们将准备并发送你的发票. 你可以从<a href=\"{0}\">组织管理页面</a>索取发票.",
"HowCanIGetMyInvoiceExplanation": "有两个用于购买许可的支付网关:Iyzico和2Checkout. 如果你通过2Checkout网关购买许可,则它将PDF发票发送到你的电子邮件地址,请参阅<a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work>2Checkout发票.</a>如果你是通过Iyzico网关或通过银行电汇购买的,我们将准备并发送你的发票. 你可以从<a href=\"{0}\">组织管理页面</a>索取发票.",
"Forum": "论坛",
"SupportExplanation": "ABP商业版许可包含由ABP框架专家组成的团队提供的高级论坛支持.",
"PrivateTicket": "私有票",

@ -228,7 +228,7 @@
"HowCanIRefundVatExplanation2": "登錄到你的<a href=\"https://secure.2checkout.com/cpanel/login.php\" target=\"_blank\">2Checkout</a>賬戶",
"HowCanIRefundVatExplanation3": "找到適當的訂單,然後按\"退還已延期的增值稅\"輸入你的增值稅ID",
"HowCanIGetMyInvoice": "我如何獲得發票?",
"HowCanIGetMyInvoiceExplanation": "有兩個用於購買許可的支付網關:PayU和2Checkout. 如果你通過2Checkout網關購買許可,則它將PDF發票發送到你的電子郵件地址,請參閱<a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work>2Checkout發票.</a>如果你是通過PayU網關或通過銀行電匯購買的,我們將準備並發送你的發票. 你可以從<a href=\"{0}\">組織管理頁面</a>索取發票.",
"HowCanIGetMyInvoiceExplanation": "有兩個用於購買許可的支付網關:Iyzico和2Checkout. 如果你通過2Checkout網關購買許可,則它將PDF發票發送到你的電子郵件地址,請參閱<a href=\"https://knowledgecenter.2checkout.com/Documentation/03Billing-and-payments/Payment-operations/How-do-invoices-work>2Checkout發票.</a>如果你是通過Iyzico網關或通過銀行電匯購買的,我們將準備並發送你的發票. 你可以從<a href=\"{0}\">組織管理頁面</a>索取發票.",
"Forum": "論壇",
"SupportExplanation": "ABP商業版許可包含由ABP框架專家組成的團隊提供的高級論壇支持.",
"PrivateTicket": "私有票",
@ -319,4 +319,4 @@
"MongoDB": "MongoDB"
}
}
}

@ -32,6 +32,14 @@ See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all t
> You can also use the *Direct Download* tab on the [Get Started](https://abp.io/get-started) page by selecting the **Preview checkbox**.
### Visual Studio Upgrade
As .NET 6 is a preview version you need to make changes on your current Visual Studio. If you want to run a .NET 6 project on Visual Studio 2019, you need to enable the `Use Previews of the .NET SDK` option from Visual Studio 2019 options. See the screenshot:
![Enable Using Previews of the .NET SDK ](use-preview-visual-studio.png)
Alternatively you can download the latest Visual Studio 2022 preview to run/create the .NET 6 projects. We have tested the ABP solution with the Visual Studio 2022 `17.0.0 Preview 4.1`. Check out https://visualstudio.microsoft.com/vs/preview/ for more information.
### Migration Notes & Breaking Changes
This is a major version and there are some breaking changes and upgrade steps. Here, a list of important breaking changes in this version:
@ -111,7 +119,7 @@ Enabling the inbox and outbox patterns requires a few manual steps for your appl
First of all, you need to have EF Core or MongoDB installed into your solution. It should be already installed if you'd created a solution from the ABP startup template.
#### Install the package
#### Install the packages
Install the new [Volo.Abp.EventBus.Boxes](https://www.nuget.org/packages/Volo.Abp.EventBus.Boxes) NuGet package to your database layer (to `EntityFrameworkCore` or `MongoDB` project) or to the host application. Open a command-line terminal at the root directory of your database (or host) project and execute the following command:
@ -119,7 +127,27 @@ Install the new [Volo.Abp.EventBus.Boxes](https://www.nuget.org/packages/Volo.Ab
abp add-package Volo.Abp.EventBus.Boxes
````
This will install the package and setup the ABP module dependency.
This will install the package and setup the ABP module dependency. This package depends on [DistributedLock.Core](https://www.nuget.org/packages/DistributedLock.Core) library which provides a distributed locking system for concurrency control in a distributed environment. There are [many distributed lock providers](https://github.com/madelson/DistributedLock#implementations), including Redis, SqlServer and ZooKeeper. You can use the one you like. Here, I will show the Redis provider.
Add [DistributedLock.Redis](https://www.nuget.org/packages/DistributedLock.Redis) NuGet package to your project, then add the following code into the ConfigureService method of your ABP module class:
````csharp
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
return new RedisDistributedSynchronizationProvider(connection.GetDatabase());
});
````
We are getting the Redis configuration from the `appsettings.json` file, so add the following lines to your `appsettings.json` file:
````json
"Redis": {
"Configuration": "127.0.0.1"
}
````
You can change the IP or customize the configuration based on your environment.
#### Configure the DbContext

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@ -0,0 +1,133 @@
# Distributed Event Bus Azure Integration
> This document explains **how to configure the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system
## Installation
Use the ABP CLI to add [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) 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.EventBus.Azure` package.
* Run `abp add-package Volo.Abp.EventBus.Azure` command.
If you want to do it manually, install the [Volo.Abp.EventBus.Azure](https://www.nuget.org/packages/Volo.Abp.EventBus.Azure) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusAzureModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
## Configuration
You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes.
### `appsettings.json` file configuration
This is the simplest way to configure the Azure Service Bus settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/).
**Example: The minimal configuration to connect to Azure Service Bus Namespace with default configurations**
````json
{
"Azure": {
"ServiceBus": {
"Connections": {
"Default": {
"ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={};EntityPath=marketing-consent"
}
}
},
"EventBus": {
"ConnectionName": "Default",
"SubscriberName": "MySubscriberName",
"TopicName": "MyTopicName"
}
}
}
````
* `MySubscriberName` is the name of this subscription, which is used as the **Subscriber** on the Azure Service Bus.
* `MyTopicName` is the **topic name**.
See [the Azure Service Bus document](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-queues-topics-subscriptions) to understand these options better.
#### Connections
If you need to connect to another Azure Service Bus Namespace the Default, you need to configure the connection properties.
**Example: Declare two connections and use one of them for the event bus**
````json
{
"Azure": {
"ServiceBus": {
"Connections": {
"Default": {
"ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={{SharedAccessKey}}"
},
"SecondConnection": {
"ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={{SharedAccessKey}}"
}
}
},
"EventBus": {
"ConnectionName": "SecondConnection",
"SubscriberName": "MySubscriberName",
"TopicName": "MyTopicName"
}
}
}
````
This allows you to use multiple Azure Service Bus namespaces in your application, but select one of them for the event bus.
You can use any of the [ServiceBusAdministrationClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.administration.servicebusadministrationclientoptions?view=azure-dotnet), [ServiceBusClientOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusclientoptions?view=azure-dotnet), [ServiceBusProcessorOptions](https://docs.microsoft.com/en-us/dotnet/api/azure.messaging.servicebus.servicebusprocessoroptions?view=azure-dotnet) properties for the connection.
**Example: Specify the Admin, Client and Processor options**
````json
{
"Azure": {
"ServiceBus": {
"Connections": {
"Default": {
"ConnectionString": "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={};EntityPath=marketing-consent",
"Admin": {
"Retry": {
"MaxRetries": 3
}
},
"Client": {
"RetryOptions": {
"MaxRetries": 1
}
},
"Processor": {
"AutoCompleteMessages": true,
"ReceiveMode": "ReceiveAndDelete"
}
}
}
},
"EventBus": {
"ConnectionName": "Default",
"SubscriberName": "MySubscriberName",
"TopicName": "MyTopicName"
}
}
}
````
### The Options Classes
`AbpAzureServiceBusOptions` and `AbpAzureEventBusOptions` classes can be used to configure the connection strings and event bus options for Azure Service Bus.
You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md).
**Example: Configure the connection**
````csharp
Configure<AbpAzureServiceBusOptions>(options =>
{
options.Connections.Default.ConnectionString = "Endpoint=sb://sb-my-app.servicebus.windows.net/;SharedAccessKeyName={{Policy Name}};SharedAccessKey={}";
options.Connections.Default.Admin.Retry.MaxRetries = 3;
options.Connections.Default.Client.RetryOptions.MaxRetries = 1;
});
````
Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file.

@ -7,6 +7,7 @@ Distributed Event bus system allows to **publish** and **subscribe** to events t
Distributed event bus system provides an **abstraction** that can be implemented by any vendor/provider. There are four providers implemented out of the box:
* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider.
* `AzureDistributedEventBus` implements the distributed event bus with the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/). See the [Azure Service Bus integration document](Distributed-Event-Bus-Azure-Integration.md) to learn how to configure it.
* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it.
* `KafkaDistributedEventBus` implements the distributed event bus with the [Kafka](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it.
* `RebusDistributedEventBus` implements the distributed event bus with the [Rebus](http://mookid.dk/category/rebus/). See the [Rebus integration document](Distributed-Event-Bus-Rebus-Integration.md) to learn how to configure it.

@ -11,13 +11,15 @@
constructor(
public content: string,
protected domStrategy?: DomStrategy,
protected contentSecurityStrategy?: ContentSecurityStrategy
protected contentSecurityStrategy?: ContentSecurityStrategy,
protected options?: ElementOptions = {},
)
```
- `content` is set to `<script>` and `<style>` elements as `textContent` property.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `contentSecurityStrategy` is the `ContentSecurityStrategy` that will be used on the created element before inserting it. (_default: None_)
- `options` can be used to pass any option to the element that will be created. e.g: `{ id: "some-id" }` (_default: empty object_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [ContentSecurityStrategy](./Content-Security-Strategy.md) documentation for their usage.
@ -57,7 +59,7 @@ Predefined content strategies are accessible via `CONTENT_STRATEGY` constant.
### AppendScriptToBody
```js
CONTENT_STRATEGY.AppendScriptToBody(content: string)
CONTENT_STRATEGY.AppendScriptToBody(content: string, options?: ElementOptions<HTMLScriptElement>)
```
Creates a `<script>` element with the given content and places it at the **end** of `<body>` tag in the document.
@ -66,7 +68,7 @@ Creates a `<script>` element with the given content and places it at the **end**
### AppendScriptToHead
```js
CONTENT_STRATEGY.AppendScriptToHead(content: string)
CONTENT_STRATEGY.AppendScriptToHead(content: string, options?: ElementOptions<HTMLScriptElement>)
```
Creates a `<script>` element with the given content and places it at the **end** of `<head>` tag in the document.
@ -75,7 +77,7 @@ Creates a `<script>` element with the given content and places it at the **end**
### AppendStyleToHead
```js
CONTENT_STRATEGY.AppendStyleToHead(content: string)
CONTENT_STRATEGY.AppendStyleToHead(content: string, options?: ElementOptions<HTMLStyleElement>)
```
Creates a `<style>` element with the given content and places it at the **end** of `<head>` tag in the document.
@ -84,7 +86,7 @@ Creates a `<style>` element with the given content and places it at the **end**
### PrependStyleToHead
```js
CONTENT_STRATEGY.PrependStyleToHead(content: string)
CONTENT_STRATEGY.PrependStyleToHead(content: string, options?: ElementOptions<HTMLStyleElement>)
```
Creates a `<style>` element with the given content and places it at the **beginning** of `<head>` tag in the document.

@ -328,6 +328,10 @@
"text": "Distributed Event Bus",
"path": "Distributed-Event-Bus.md",
"items": [
{
"text": "Azure Service Bus Integration",
"path": "Distributed-Event-Bus-Azure-Integration.md"
},
{
"text": "RabbitMQ Integration",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"

@ -369,6 +369,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Compone
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions", "src\Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions\Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions.csproj", "{E9CE58DB-0789-4D18-8B63-474F7D7B14B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AzureServiceBus", "src\Volo.Abp.AzureServiceBus\Volo.Abp.AzureServiceBus.csproj", "{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EventBus.Azure", "src\Volo.Abp.EventBus.Azure\Volo.Abp.EventBus.Azure.csproj", "{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Authorization.Abstractions", "src\Volo.Abp.Authorization.Abstractions\Volo.Abp.Authorization.Abstractions.csproj", "{87B0C2A8-FE95-4779-8B9C-2181AA52B3FA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.TextTemplating.Core", "src\Volo.Abp.TextTemplating.Core\Volo.Abp.TextTemplating.Core.csproj", "{184E859A-282D-44D7-B8E9-FEA874644013}"
@ -1124,6 +1127,14 @@ Global
{E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9CE58DB-0789-4D18-8B63-474F7D7B14B4}.Release|Any CPU.Build.0 = Release|Any CPU
{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{808EC18E-C8CC-4F5C-82B6-984EADBBF85D}.Release|Any CPU.Build.0 = Release|Any CPU
{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2}.Release|Any CPU.Build.0 = Release|Any CPU
{87B0C2A8-FE95-4779-8B9C-2181AA52B3FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87B0C2A8-FE95-4779-8B9C-2181AA52B3FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87B0C2A8-FE95-4779-8B9C-2181AA52B3FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -1362,6 +1373,8 @@ Global
{863C18F9-2407-49F9-9ADC-F6229AF3B385} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{B4B6B7DE-9798-4007-B1DF-7EE7929E392A} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{E9CE58DB-0789-4D18-8B63-474F7D7B14B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{808EC18E-C8CC-4F5C-82B6-984EADBBF85D} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{FB27F78E-F10E-4810-9B8E-BCD67DCFC8A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{87B0C2A8-FE95-4779-8B9C-2181AA52B3FA} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{184E859A-282D-44D7-B8E9-FEA874644013} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{228723E6-FA6D-406B-B8F8-C9BCC547AF8E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}

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

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

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Volo.Abp.AzureServiceBus</AssemblyName>
<PackageId>Volo.Abp.AzureServiceBus</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.1.1" />
<ProjectReference Include="..\Volo.Abp.Json\Volo.Abp.Json.csproj" />
<ProjectReference Include="..\Volo.Abp.Threading\Volo.Abp.Threading.csproj" />
</ItemGroup>
</Project>

@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
using Volo.Abp.Threading;
namespace Volo.Abp.AzureServiceBus
{
[DependsOn(
typeof(AbpJsonModule),
typeof(AbpThreadingModule)
)]
public class AbpAzureServiceBusModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpAzureServiceBusOptions>(configuration.GetSection("Azure:ServiceBus"));
}
}
}

@ -0,0 +1,12 @@
namespace Volo.Abp.AzureServiceBus
{
public class AbpAzureServiceBusOptions
{
public AzureServiceBusConnections Connections { get; }
public AbpAzureServiceBusOptions()
{
Connections = new AzureServiceBusConnections();
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Volo.Abp.AzureServiceBus
{
[Serializable]
public class AzureServiceBusConnections : Dictionary<string, ClientConfig>
{
public const string DefaultConnectionName = "Default";
[NotNull]
public ClientConfig Default
{
get => this[DefaultConnectionName];
set => this[DefaultConnectionName] = Check.NotNull(value, nameof(value));
}
public AzureServiceBusConnections()
{
Default = new ClientConfig();
}
public ClientConfig GetOrDefault(string connectionName)
{
return TryGetValue(connectionName, out var connectionFactory)
? connectionFactory
: Default;
}
}
}

@ -0,0 +1,104 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;
namespace Volo.Abp.AzureServiceBus
{
public class AzureServiceBusMessageConsumer : IAzureServiceBusMessageConsumer, ITransientDependency
{
public ILogger<AzureServiceBusMessageConsumer> Logger { get; set; }
private readonly IExceptionNotifier _exceptionNotifier;
private readonly IProcessorPool _processorPool;
private readonly ConcurrentBag<Func<ServiceBusReceivedMessage, Task>> _callbacks;
private string _connectionName;
private string _subscriptionName;
private string _topicName;
public AzureServiceBusMessageConsumer(
IExceptionNotifier exceptionNotifier,
IProcessorPool processorPool)
{
_exceptionNotifier = exceptionNotifier;
_processorPool = processorPool;
Logger = NullLogger<AzureServiceBusMessageConsumer>.Instance;
_callbacks = new ConcurrentBag<Func<ServiceBusReceivedMessage, Task>>();
}
public virtual void Initialize(
[NotNull] string topicName,
[NotNull] string subscriptionName,
string connectionName)
{
Check.NotNull(topicName, nameof(topicName));
Check.NotNull(subscriptionName, nameof(subscriptionName));
_topicName = topicName;
_connectionName = connectionName ?? AzureServiceBusConnections.DefaultConnectionName;
_subscriptionName = subscriptionName;
StartProcessing();
}
public void OnMessageReceived(Func<ServiceBusReceivedMessage, Task> callback)
{
_callbacks.Add(callback);
}
protected virtual void StartProcessing()
{
Task.Factory.StartNew(function: async () =>
{
var serviceBusProcessor = await _processorPool.GetAsync(_subscriptionName, _topicName, _connectionName);
serviceBusProcessor.ProcessErrorAsync += HandleIncomingError;
serviceBusProcessor.ProcessMessageAsync += HandleIncomingMessage;
if (!serviceBusProcessor.IsProcessing)
{
await serviceBusProcessor.StartProcessingAsync();
}
while (true)
{
Thread.Sleep(1000);
}
}, TaskCreationOptions.LongRunning);
}
protected virtual async Task HandleIncomingMessage(ProcessMessageEventArgs args)
{
try
{
foreach (var callback in _callbacks)
{
await callback(args.Message);
}
await args.CompleteMessageAsync(args.Message);
}
catch (Exception exception)
{
await HandleError(exception);
}
}
protected virtual async Task HandleIncomingError(ProcessErrorEventArgs args)
{
await HandleError(args.Exception);
}
protected virtual async Task HandleError(Exception exception)
{
Logger.LogException(exception);
await _exceptionNotifier.NotifyAsync(exception);
}
}
}

@ -0,0 +1,29 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AzureServiceBus
{
public class AzureServiceBusMessageConsumerFactory : IAzureServiceBusMessageConsumerFactory, ISingletonDependency, IDisposable
{
protected IServiceScope ServiceScope { get; }
public AzureServiceBusMessageConsumerFactory(IServiceScopeFactory serviceScopeFactory)
{
ServiceScope = serviceScopeFactory.CreateScope();
}
public IAzureServiceBusMessageConsumer CreateMessageConsumer(string topicName, string subscriptionName, string connectionName)
{
var processor = ServiceScope.ServiceProvider.GetRequiredService<AzureServiceBusMessageConsumer>();
processor.Initialize(topicName, subscriptionName, connectionName);
return processor;
}
public void Dispose()
{
ServiceScope?.Dispose();
}
}
}

@ -0,0 +1,16 @@
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
namespace Volo.Abp.AzureServiceBus
{
public class ClientConfig
{
public string ConnectionString { get; set; }
public ServiceBusAdministrationClientOptions Admin { get; set; } = new();
public ServiceBusClientOptions Client { get; set; } = new();
public ServiceBusProcessorOptions Processor { get; set; } = new();
}
}

@ -0,0 +1,88 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AzureServiceBus
{
public class ConnectionPool : IConnectionPool, ISingletonDependency
{
public ILogger<ConnectionPool> Logger { get; set; }
private bool _isDisposed;
private readonly AbpAzureServiceBusOptions _options;
private readonly ConcurrentDictionary<string, Lazy<ServiceBusClient>> _clients;
private readonly ConcurrentDictionary<string, Lazy<ServiceBusAdministrationClient>> _adminClients;
public ConnectionPool(IOptions<AbpAzureServiceBusOptions> options)
{
_options = options.Value;
_clients = new ConcurrentDictionary<string, Lazy<ServiceBusClient>>();
_adminClients = new ConcurrentDictionary<string, Lazy<ServiceBusAdministrationClient>>();
Logger = new NullLogger<ConnectionPool>();
}
public ServiceBusClient GetClient(string connectionName)
{
connectionName ??= AzureServiceBusConnections.DefaultConnectionName;
return _clients.GetOrAdd(
connectionName, new Lazy<ServiceBusClient>(() =>
{
var config = _options.Connections.GetOrDefault(connectionName);
return new ServiceBusClient(config.ConnectionString, config.Client);
})
).Value;
}
public ServiceBusAdministrationClient GetAdministrationClient(string connectionName)
{
connectionName ??= AzureServiceBusConnections.DefaultConnectionName;
return _adminClients.GetOrAdd(
connectionName, new Lazy<ServiceBusAdministrationClient>(() =>
{
var config = _options.Connections.GetOrDefault(connectionName);
return new ServiceBusAdministrationClient(config.ConnectionString);
})
).Value;
}
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!_clients.Any())
{
Logger.LogDebug($"Disposed connection pool with no connection in the pool.");
return;
}
Logger.LogInformation($"Disposing connection pool ({_clients.Count} connections).");
foreach (var connection in _clients.Values)
{
await connection.Value.DisposeAsync();
}
_clients.Clear();
if (!_adminClients.Any())
{
Logger.LogDebug($"Disposed admin connection pool with no admin connection in the pool.");
return;
}
Logger.LogInformation($"Disposing admin connection pool ({_adminClients.Count} admin connections).");
_adminClients.Clear();
}
}
}

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
namespace Volo.Abp.AzureServiceBus
{
public interface IAzureServiceBusMessageConsumer
{
void OnMessageReceived(Func<ServiceBusReceivedMessage, Task> callback);
}
}

@ -0,0 +1,21 @@
using System.Threading.Tasks;
namespace Volo.Abp.AzureServiceBus
{
public interface IAzureServiceBusMessageConsumerFactory
{
/// <summary>
/// Creates a new <see cref="IAzureServiceBusMessageConsumerFactory"/>.
/// Avoid to create too many consumers since they are
/// not disposed until end of the application.
/// </summary>
/// <param name="topicName"></param>
/// <param name="subscriptionName"></param>
/// <param name="connectionName"></param>
/// <returns></returns>
IAzureServiceBusMessageConsumer CreateMessageConsumer(
string topicName,
string subscriptionName,
string connectionName);
}
}

@ -0,0 +1,13 @@
using System;
namespace Volo.Abp.AzureServiceBus
{
public interface IAzureServiceBusSerializer
{
byte[] Serialize(object obj);
object Deserialize(byte[] value, Type type);
T Deserialize<T>(byte[] value);
}
}

@ -0,0 +1,13 @@
using System;
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
namespace Volo.Abp.AzureServiceBus
{
public interface IConnectionPool : IAsyncDisposable
{
ServiceBusClient GetClient(string connectionName);
ServiceBusAdministrationClient GetAdministrationClient(string connectionName);
}
}

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
namespace Volo.Abp.AzureServiceBus
{
public interface IProcessorPool : IAsyncDisposable
{
Task<ServiceBusProcessor> GetAsync(string subscriptionName, string topicName, string connectionName);
}
}

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
namespace Volo.Abp.AzureServiceBus
{
public interface IPublisherPool : IAsyncDisposable
{
Task<ServiceBusSender> GetAsync(string topicName, string connectionName);
}
}

@ -0,0 +1,82 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AzureServiceBus
{
public class ProcessorPool : IProcessorPool, ISingletonDependency
{
public ILogger<ProcessorPool> Logger { get; set; }
private bool _isDisposed;
private readonly AbpAzureServiceBusOptions _options;
private readonly IConnectionPool _connectionPool;
private readonly ConcurrentDictionary<string, Lazy<ServiceBusProcessor>> _processors;
public ProcessorPool(
IOptions<AbpAzureServiceBusOptions> options,
IConnectionPool connectionPool)
{
_options = options.Value;
_connectionPool = connectionPool;
_processors = new ConcurrentDictionary<string, Lazy<ServiceBusProcessor>>();
Logger = new NullLogger<ProcessorPool>();
}
public async Task<ServiceBusProcessor> GetAsync(string subscriptionName, string topicName, string connectionName)
{
var admin = _connectionPool.GetAdministrationClient(connectionName);
await admin.SetupSubscriptionAsync(topicName, subscriptionName);
return _processors.GetOrAdd(
$"{topicName}-{subscriptionName}", new Lazy<ServiceBusProcessor>(() =>
{
var config = _options.Connections.GetOrDefault(connectionName);
var client = _connectionPool.GetClient(connectionName);
return client.CreateProcessor(topicName, subscriptionName, config.Processor);
})
).Value;
}
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!_processors.Any())
{
Logger.LogDebug($"Disposed processor pool with no processors in the pool.");
return;
}
Logger.LogInformation($"Disposing processor pool ({_processors.Count} processors).");
foreach (var item in _processors.Values)
{
var processor = item.Value;
if (processor.IsProcessing)
{
await processor.StopProcessingAsync();
}
if (!processor.IsClosed)
{
await processor.CloseAsync();
}
await processor.DisposeAsync();
}
_processors.Clear();
}
}
}

@ -0,0 +1,66 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AzureServiceBus
{
public class PublisherPool : IPublisherPool, ISingletonDependency
{
public ILogger<PublisherPool> Logger { get; set; }
private bool _isDisposed;
private readonly IConnectionPool _connectionPool;
private readonly ConcurrentDictionary<string, Lazy<ServiceBusSender>> _publishers;
public PublisherPool(IConnectionPool connectionPool)
{
_connectionPool = connectionPool;
_publishers = new ConcurrentDictionary<string, Lazy<ServiceBusSender>>();
Logger = new NullLogger<PublisherPool>();
}
public async Task<ServiceBusSender> GetAsync(string topicName, string connectionName)
{
var admin = _connectionPool.GetAdministrationClient(connectionName);
await admin.SetupTopicAsync(topicName);
return _publishers.GetOrAdd(
topicName, new Lazy<ServiceBusSender>(() =>
{
var client = _connectionPool.GetClient(connectionName);
return client.CreateSender(topicName);
})
).Value;
}
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (!_publishers.Any())
{
Logger.LogDebug($"Disposed publisher pool with no publisher in the pool.");
return;
}
Logger.LogInformation($"Disposing publisher pool ({_publishers.Count} publishers).");
foreach (var publisher in _publishers.Values)
{
await publisher.Value.CloseAsync();
await publisher.Value.DisposeAsync();
}
_publishers.Clear();
}
}
}

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus.Administration;
namespace Volo.Abp.AzureServiceBus
{
public static class ServiceBusAdministrationClientExtensions
{
public static async Task SetupTopicAsync(this ServiceBusAdministrationClient client, string topicName)
{
if (!await client.TopicExistsAsync(topicName))
{
await client.CreateTopicAsync(topicName);
}
}
public static async Task SetupSubscriptionAsync(this ServiceBusAdministrationClient client, string topicName, string subscriptionName)
{
await client.SetupTopicAsync(topicName);
if (!await client.SubscriptionExistsAsync(topicName, subscriptionName))
{
await client.CreateSubscriptionAsync(topicName, subscriptionName);
}
}
}
}

@ -0,0 +1,32 @@
using System;
using System.Text;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.AzureServiceBus
{
public class Utf8JsonAzureServiceBusSerializer : IAzureServiceBusSerializer, ITransientDependency
{
private readonly IJsonSerializer _jsonSerializer;
public Utf8JsonAzureServiceBusSerializer(IJsonSerializer jsonSerializer)
{
_jsonSerializer = jsonSerializer;
}
public byte[] Serialize(object obj)
{
return Encoding.UTF8.GetBytes(_jsonSerializer.Serialize(obj));
}
public object Deserialize(byte[] value, Type type)
{
return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value));
}
public T Deserialize<T>(byte[] value)
{
return _jsonSerializer.Deserialize<T>(Encoding.UTF8.GetString(value));
}
}
}

@ -134,7 +134,7 @@ namespace Volo.Abp.Reflection
var currentType = objectType;
var objectPath = currentType.FullName;
var absolutePropertyPath = propertyPath;
if (objectPath != null && absolutePropertyPath.StartsWith(objectPath))
if (objectPath != null && absolutePropertyPath.StartsWith(objectPath))
{
absolutePropertyPath = absolutePropertyPath.Replace(objectPath + ".", "");
}
@ -144,7 +144,10 @@ namespace Volo.Abp.Reflection
var property = currentType.GetProperty(propertyName);
if (property != null)
{
value = property.GetValue(value, null);
if (value != null)
{
value = property.GetValue(value, null);
}
currentType = property.PropertyType;
}
else

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0-preview7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.0-rc.1" />
</ItemGroup>
</Project>

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

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

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

@ -0,0 +1,11 @@
namespace Volo.Abp.EventBus.Azure
{
public class AbpAzureEventBusOptions
{
public string ConnectionName { get; set; }
public string SubscriberName { get; set; }
public string TopicName { get; set; }
}
}

@ -0,0 +1,28 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AzureServiceBus;
using Volo.Abp.Modularity;
namespace Volo.Abp.EventBus.Azure
{
[DependsOn(
typeof(AbpEventBusModule),
typeof(AbpAzureServiceBusModule)
)]
public class AbpEventBusAzureModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpAzureEventBusOptions>(configuration.GetSection("Azure:EventBus"));
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
context
.ServiceProvider
.GetRequiredService<AzureDistributedEventBus>()
.Initialize();
}
}
}

@ -0,0 +1,234 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.AzureServiceBus;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.Azure
{
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IDistributedEventBus), typeof(AzureDistributedEventBus))]
public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDependency
{
private readonly AbpAzureEventBusOptions _options;
private readonly IAzureServiceBusMessageConsumerFactory _messageConsumerFactory;
private readonly IPublisherPool _publisherPool;
private readonly IAzureServiceBusSerializer _serializer;
private readonly ConcurrentDictionary<Type, List<IEventHandlerFactory>> _handlerFactories;
private readonly ConcurrentDictionary<string, Type> _eventTypes;
private IAzureServiceBusMessageConsumer _consumer;
public AzureDistributedEventBus(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
IGuidGenerator guidGenerator,
IClock clock,
IOptions<AbpAzureEventBusOptions> abpAzureEventBusOptions,
IAzureServiceBusSerializer serializer,
IAzureServiceBusMessageConsumerFactory messageConsumerFactory,
IPublisherPool publisherPool)
: base(serviceScopeFactory,
currentTenant,
unitOfWorkManager,
abpDistributedEventBusOptions,
guidGenerator,
clock)
{
_options = abpAzureEventBusOptions.Value;
_serializer = serializer;
_messageConsumerFactory = messageConsumerFactory;
_publisherPool = publisherPool;
_handlerFactories = new ConcurrentDictionary<Type, List<IEventHandlerFactory>>();
_eventTypes = new ConcurrentDictionary<string, Type>();
}
public void Initialize()
{
_consumer = _messageConsumerFactory.CreateMessageConsumer(
_options.TopicName,
_options.SubscriberName,
_options.ConnectionName);
_consumer.OnMessageReceived(ProcessEventAsync);
SubscribeHandlers(AbpDistributedEventBusOptions.Handlers);
}
private async Task ProcessEventAsync(ServiceBusReceivedMessage message)
{
var eventName = message.Subject;
var eventType = _eventTypes.GetOrDefault(eventName);
if (eventType == null)
{
return;
}
if (await AddToInboxAsync(message.MessageId, eventName, eventType, message.Body.ToArray()))
{
return;
}
var eventData = _serializer.Deserialize(message.Body.ToArray(), eventType);
await TriggerHandlersAsync(eventType, eventData);
}
public override async Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig)
{
await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData);
}
public override async Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig)
{
var eventType = _eventTypes.GetOrDefault(incomingEvent.EventName);
if (eventType == null)
{
return;
}
var eventData = _serializer.Deserialize(incomingEvent.EventData, eventType);
var exceptions = new List<Exception>();
await TriggerHandlersAsync(eventType, eventData, exceptions, inboxConfig);
if (exceptions.Any())
{
ThrowOriginalExceptions(eventType, exceptions);
}
}
protected override byte[] Serialize(object eventData)
{
return _serializer.Serialize(eventData);
}
public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory)
{
var handlerFactories = GetOrCreateHandlerFactories(eventType);
if (factory.IsInFactories(handlerFactories))
{
return NullDisposable.Instance;
}
handlerFactories.Add(factory);
return new EventHandlerFactoryUnregistrar(this, eventType, factory);
}
public override void Unsubscribe<TEvent>(Func<TEvent, Task> action)
{
Check.NotNull(action, nameof(action));
GetOrCreateHandlerFactories(typeof(TEvent))
.Locking(factories =>
{
factories.RemoveAll(
factory =>
{
var singleInstanceFactory = factory as SingleInstanceHandlerFactory;
if (singleInstanceFactory == null)
{
return false;
}
var actionHandler = singleInstanceFactory.HandlerInstance as ActionEventHandler<TEvent>;
if (actionHandler == null)
{
return false;
}
return actionHandler.Action == action;
});
});
}
public override void Unsubscribe(Type eventType, IEventHandler handler)
{
GetOrCreateHandlerFactories(eventType)
.Locking(factories =>
{
factories.RemoveAll(
factory =>
factory is SingleInstanceHandlerFactory handlerFactory &&
handlerFactory.HandlerInstance == handler
);
});
}
public override void Unsubscribe(Type eventType, IEventHandlerFactory factory)
{
GetOrCreateHandlerFactories(eventType)
.Locking(factories => factories.Remove(factory));
}
public override void UnsubscribeAll(Type eventType)
{
GetOrCreateHandlerFactories(eventType)
.Locking(factories => factories.Clear());
}
protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
await PublishAsync(eventType, eventData);
}
protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord)
{
unitOfWork.AddOrReplaceDistributedEvent(eventRecord);
}
protected virtual async Task PublishAsync(string eventName, object eventData)
{
var body = _serializer.Serialize(eventData);
var message = new ServiceBusMessage(body)
{
Subject = eventName
};
var publisher = await _publisherPool.GetAsync(
_options.TopicName,
_options.ConnectionName);
await publisher.SendMessageAsync(message);
}
protected override IEnumerable<EventTypeWithEventHandlerFactories> GetHandlerFactories(Type eventType)
{
return _handlerFactories
.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))
.Select(handlerFactory =>
new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value))
.ToArray();
}
private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType)
{
return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType);
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return _handlerFactories.GetOrAdd(
eventType,
type =>
{
var eventName = EventNameAttribute.GetNameOrDefault(type);
_eventTypes[eventName] = type;
return new List<IEventHandlerFactory>();
}
);
}
}
}

@ -1,11 +1,13 @@
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedLocking;
using Volo.Abp.Modularity;
namespace Volo.Abp.EventBus.Boxes
{
[DependsOn(
typeof(AbpEventBusModule),
typeof(AbpBackgroundWorkersModule)
typeof(AbpBackgroundWorkersModule),
typeof(AbpDistributedLockingModule)
)]
public class AbpEventBusBoxesModule : AbpModule
{

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Http.Client.ClientProxying
{
public class AbpHttpClientProxyingOptions
{
public Dictionary<Type, Type> QueryStringConverts { get; set; }
public Dictionary<Type, Type> FormDataConverts { get; set; }
public AbpHttpClientProxyingOptions()
{
QueryStringConverts = new Dictionary<Type, Type>();
FormDataConverts = new Dictionary<Type, Type>();
}
}
}

@ -28,15 +28,15 @@ namespace Volo.Abp.Http.Client.ClientProxying
protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService<IClientProxyApiDescriptionFinder>();
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetRequiredService<ICancellationTokenProvider>();
protected ICorrelationIdProvider CorrelationIdProvider => LazyServiceProvider.LazyGetRequiredService<ICorrelationIdProvider>();
protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
protected IOptions<AbpCorrelationIdOptions> AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpCorrelationIdOptions>>();
protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService<IProxyHttpClientFactory>();
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceConfigurationProvider>();
protected IOptions<AbpHttpClientOptions> ClientOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpHttpClientOptions>>();
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>();
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceHttpClientAuthenticator>();
protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyRequestPayloadBuilder>();
protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyUrlBuilder>();
protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
protected IOptions<AbpCorrelationIdOptions> AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpCorrelationIdOptions>>();
protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService<IProxyHttpClientFactory>();
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceConfigurationProvider>();
protected IOptions<AbpHttpClientOptions> ClientOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpHttpClientOptions>>();
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>();
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceHttpClientAuthenticator>();
protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyRequestPayloadBuilder>();
protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyUrlBuilder>();
protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue arguments = null)
{
@ -114,7 +114,7 @@ namespace Volo.Abp.Http.Client.ClientProxying
var requestMessage = new HttpRequestMessage(requestContext.Action.GetHttpMethod(), url)
{
Content = ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)
Content = await ClientProxyRequestPayloadBuilder.BuildContentAsync(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)
};
AddHeaders(requestContext.Arguments, requestContext.Action, requestMessage, apiVersion);
@ -156,14 +156,14 @@ namespace Volo.Abp.Http.Client.ClientProxying
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion);
}
protected virtual Task<string> GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
protected virtual async Task<string> GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
{
return Task.FromResult(ClientProxyUrlBuilder.GenerateUrlWithParameters(requestContext.Action, requestContext.Arguments, apiVersion));
return await ClientProxyUrlBuilder.GenerateUrlWithParametersAsync(requestContext.Action, requestContext.Arguments, apiVersion);
}
protected virtual Task<HttpContent> GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
protected virtual async Task<HttpContent> GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
{
return Task.FromResult(ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion));
return await ClientProxyRequestPayloadBuilder.BuildContentAsync(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion);
}
protected virtual async Task<string> FindBestApiVersionAsync(ClientProxyRequestContext requestContext)

@ -3,8 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Content;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Proxying;
@ -16,21 +20,40 @@ namespace Volo.Abp.Http.Client.ClientProxying
{
public class ClientProxyRequestPayloadBuilder : ITransientDependency
{
protected static MethodInfo CallObjectToFormDataAsyncMethod { get; }
static ClientProxyRequestPayloadBuilder()
{
CallObjectToFormDataAsyncMethod = typeof(ClientProxyRequestPayloadBuilder)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.First(m => m.Name == nameof(ObjectToFormDataAsync) && m.IsGenericMethodDefinition);
}
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected AbpHttpClientProxyingOptions HttpClientProxyingOptions { get; }
public ClientProxyRequestPayloadBuilder(IServiceScopeFactory serviceScopeFactory, IOptions<AbpHttpClientProxyingOptions> httpClientProxyingOptions)
{
ServiceScopeFactory = serviceScopeFactory;
HttpClientProxyingOptions = httpClientProxyingOptions.Value;
}
[CanBeNull]
public virtual HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion)
public virtual async Task<HttpContent> BuildContentAsync(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion)
{
var body = GenerateBody(action, methodArguments, jsonSerializer);
var body = await GenerateBodyAsync(action, methodArguments, jsonSerializer);
if (body != null)
{
return body;
}
body = GenerateFormPostData(action, methodArguments);
body = await GenerateFormPostDataAsync(action, methodArguments);
return body;
}
protected virtual HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer)
protected virtual Task<HttpContent> GenerateBodyAsync(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer)
{
var parameters = action
.Parameters
@ -39,7 +62,7 @@ namespace Volo.Abp.Http.Client.ClientProxying
if (parameters.Length <= 0)
{
return null;
return Task.FromResult<HttpContent>(null);
}
if (parameters.Length > 1)
@ -52,13 +75,13 @@ namespace Volo.Abp.Http.Client.ClientProxying
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameters[0]);
if (value == null)
{
return null;
return Task.FromResult<HttpContent>(null);
}
return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json);
return Task.FromResult<HttpContent>(new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json));
}
protected virtual HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments)
protected virtual async Task<HttpContent> GenerateFormPostDataAsync(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments)
{
var parameters = action
.Parameters
@ -70,71 +93,76 @@ namespace Volo.Abp.Http.Client.ClientProxying
return null;
}
if (parameters.Any(x => x.BindingSourceId == ParameterBindingSources.FormFile))
var formData = new MultipartFormDataContent();
foreach (var parameter in parameters)
{
var formData = new MultipartFormDataContent();
foreach (var parameter in parameters)
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
if (value == null)
{
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
if (value == null)
{
continue;
}
continue;
}
if (value is IRemoteStreamContent remoteStreamContent)
{
var stream = remoteStreamContent.GetStream();
var streamContent = new StreamContent(stream);
if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace())
{
streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType);
}
streamContent.Headers.ContentLength = remoteStreamContent.ContentLength;
formData.Add(streamContent, parameter.Name, remoteStreamContent.FileName ?? parameter.Name);
}
else if (value is IEnumerable<IRemoteStreamContent> remoteStreamContents)
if (HttpClientProxyingOptions.FormDataConverts.ContainsKey(value.GetType()))
{
using (var scope = ServiceScopeFactory.CreateScope())
{
foreach (var content in remoteStreamContents)
var formDataContents = await (Task<List<KeyValuePair<string, HttpContent>>>)CallObjectToFormDataAsyncMethod
.MakeGenericMethod(value.GetType())
.Invoke(this, new object[]
{
scope.ServiceProvider.GetRequiredService(HttpClientProxyingOptions.FormDataConverts[value.GetType()]),
value
});
if (formDataContents != null)
{
var stream = content.GetStream();
var streamContent = new StreamContent(stream);
if (!content.ContentType.IsNullOrWhiteSpace())
foreach (var content in formDataContents)
{
streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType);
formData.Add(content.Value, content.Key);
}
streamContent.Headers.ContentLength = content.ContentLength;
formData.Add(streamContent, parameter.Name, content.FileName ?? parameter.Name);
continue;
}
}
else
}
if (value is IRemoteStreamContent remoteStreamContent)
{
var stream = remoteStreamContent.GetStream();
var streamContent = new StreamContent(stream);
if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace())
{
formData.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name);
streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType);
}
streamContent.Headers.ContentLength = remoteStreamContent.ContentLength;
formData.Add(streamContent, parameter.Name, remoteStreamContent.FileName ?? parameter.Name);
}
return formData;
}
else
{
var postDataBuilder = new StringBuilder();
var isFirstParam = true;
foreach (var parameter in parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Form))
else if (value is IEnumerable<IRemoteStreamContent> remoteStreamContents)
{
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
if (value == null)
foreach (var content in remoteStreamContents)
{
continue;
var stream = content.GetStream();
var streamContent = new StreamContent(stream);
if (!content.ContentType.IsNullOrWhiteSpace())
{
streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType);
}
streamContent.Headers.ContentLength = content.ContentLength;
formData.Add(streamContent, parameter.Name, content.FileName ?? parameter.Name);
}
postDataBuilder.Append(isFirstParam ? "?" : "&");
postDataBuilder.Append(parameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
isFirstParam = false;
}
return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded);
else
{
formData.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name);
}
}
return formData;
}
protected virtual async Task<List<KeyValuePair<string, HttpContent>>> ObjectToFormDataAsync<T>(IObjectToFormData<T> converter, T value)
{
return await converter.ConvertAsync(value);
}
}
}

@ -3,8 +3,12 @@ using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Proxying;
using Volo.Abp.Http.Modeling;
@ -15,7 +19,25 @@ namespace Volo.Abp.Http.Client.ClientProxying
{
public class ClientProxyUrlBuilder : ITransientDependency
{
public string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
protected static MethodInfo CallObjectToQueryStringAsyncMethod { get; }
static ClientProxyUrlBuilder()
{
CallObjectToQueryStringAsyncMethod = typeof(ClientProxyUrlBuilder)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.First(m => m.Name == nameof(ObjectToQueryStringAsync) && m.IsGenericMethodDefinition);
}
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected AbpHttpClientProxyingOptions HttpClientProxyingOptions { get; }
public ClientProxyUrlBuilder(IServiceScopeFactory serviceScopeFactory, IOptions<AbpHttpClientProxyingOptions> httpClientProxyingOptions)
{
ServiceScopeFactory = serviceScopeFactory;
HttpClientProxyingOptions = httpClientProxyingOptions.Value;
}
public async Task<string> GenerateUrlWithParametersAsync(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{
// The ASP.NET Core route value provider and query string value provider:
// Treat values as invariant culture.
@ -24,14 +46,14 @@ namespace Volo.Abp.Http.Client.ClientProxying
{
var urlBuilder = new StringBuilder(action.Url);
ReplacePathVariables(urlBuilder, action.Parameters, methodArguments, apiVersion);
AddQueryStringParameters(urlBuilder, action.Parameters, methodArguments, apiVersion);
await ReplacePathVariablesAsync(urlBuilder, action.Parameters, methodArguments, apiVersion);
await AddQueryStringParametersAsync(urlBuilder, action.Parameters, methodArguments, apiVersion);
return urlBuilder.ToString();
}
}
protected virtual void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
protected virtual Task ReplacePathVariablesAsync(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{
var pathParameters = actionParameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Path)
@ -39,7 +61,7 @@ namespace Volo.Abp.Http.Client.ClientProxying
if (!pathParameters.Any())
{
return;
return Task.CompletedTask;
}
if (pathParameters.Any(p => p.Name == "apiVersion"))
@ -71,9 +93,11 @@ namespace Volo.Abp.Http.Client.ClientProxying
urlBuilder = urlBuilder.Replace($"{{{pathParameter.Name}}}", value.ToString());
}
}
return Task.CompletedTask;
}
protected virtual void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
protected virtual async Task AddQueryStringParametersAsync(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{
var queryStringParameters = actionParameters
.Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query))
@ -89,7 +113,29 @@ namespace Volo.Abp.Http.Client.ClientProxying
continue;
}
if (AddQueryStringParameter(urlBuilder, isFirstParam, queryStringParameter.Name, value))
if (HttpClientProxyingOptions.QueryStringConverts.ContainsKey(value.GetType()))
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var queryString = await (Task<string>)CallObjectToQueryStringAsyncMethod
.MakeGenericMethod(value.GetType())
.Invoke(this, new object[]
{
scope.ServiceProvider.GetRequiredService(HttpClientProxyingOptions.QueryStringConverts[value.GetType()]),
value
});
if (queryString != null)
{
urlBuilder.Append(isFirstParam ? "?" : "&");
urlBuilder.Append(queryString);
isFirstParam = false;
continue;
}
}
}
if (await AddQueryStringParameterAsync(urlBuilder, isFirstParam, queryStringParameter.Name, value))
{
isFirstParam = false;
}
@ -97,11 +143,16 @@ namespace Volo.Abp.Http.Client.ClientProxying
if (apiVersion.ShouldSendInQueryString())
{
AddQueryStringParameter(urlBuilder, isFirstParam, "api-version", apiVersion.Version); //TODO: Constant!
await AddQueryStringParameterAsync(urlBuilder, isFirstParam, "api-version", apiVersion.Version); //TODO: Constant!
}
}
protected virtual bool AddQueryStringParameter(
protected virtual async Task<string> ObjectToQueryStringAsync<T>(IObjectToQueryString<T> converter, T value)
{
return await converter.ConvertAsync(value);
}
protected virtual async Task<bool> AddQueryStringParameterAsync(
StringBuilder urlBuilder,
bool isFirstParam,
string name,
@ -116,7 +167,7 @@ namespace Volo.Abp.Http.Client.ClientProxying
{
urlBuilder.Append(isFirstParam ? "?" : "&");
}
urlBuilder.Append(name + $"[{index++}]=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(item)) + "&");
urlBuilder.Append(name + $"[{index++}]=" + System.Net.WebUtility.UrlEncode(await ConvertValueToStringAsync(item)) + "&");
}
if (index > 0)
@ -130,18 +181,18 @@ namespace Volo.Abp.Http.Client.ClientProxying
}
urlBuilder.Append(isFirstParam ? "?" : "&");
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value)));
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(await ConvertValueToStringAsync(value)));
return true;
}
protected virtual string ConvertValueToString([CanBeNull] object value)
protected virtual Task<string> ConvertValueToStringAsync([CanBeNull] object value)
{
if (value is DateTime dateTimeValue)
{
return dateTimeValue.ToUniversalTime().ToString("O");
return Task.FromResult(dateTimeValue.ToUniversalTime().ToString("O"));
}
return value?.ToString();
return Task.FromResult(value?.ToString());
}
}
}

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
namespace Volo.Abp.Http.Client.ClientProxying
{
public interface IObjectToFormData<in TValue>
{
Task<List<KeyValuePair<string, HttpContent>>> ConvertAsync(TValue value);
}
}

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Volo.Abp.Http.Client.ClientProxying
{
public interface IObjectToQueryString<in TValue>
{
Task<string> ConvertAsync(TValue value);
}
}

@ -161,7 +161,7 @@ namespace Volo.Abp.Http.ProxyScripting.Generators
public static string GenerateJsFuncParameterList(ActionApiDescriptionModel action, string ajaxParametersName)
{
var methodParamNames = action.ParametersOnMethod.Select(p => p.Name).Distinct().ToList();
var methodParamNames = action.Parameters.GroupBy(p => p.NameOnMethod).Select(x => x.Key).ToList();
methodParamNames.Add(ajaxParametersName);
return methodParamNames.Select(prmName => NormalizeJsVariableName(prmName.ToCamelCase())).JoinAsString(", ");
}

@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.Http.Client;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.Abp.Http.DynamicProxying;
using Volo.Abp.Http.Localization;
using Volo.Abp.Localization;
@ -52,6 +54,12 @@ namespace Volo.Abp.Http
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput));
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput));
});
Configure<AbpHttpClientProxyingOptions>(options =>
{
options.QueryStringConverts.Add(typeof(List<GetParamsNameValue>), typeof(TestObjectToQueryString));
options.FormDataConverts.Add(typeof(List<GetParamsNameValue>), typeof(TestObjectToFormData));
});
}
}
}

@ -276,5 +276,59 @@ namespace Volo.Abp.Http.DynamicProxying
});
result.ShouldBe("123.rtf:File1:application/rtf:1-1.rtf123.rtf:File2:application/rtf2:1-2.rtf789.rtf:File3:application/rtf3:i-789.rtf");
}
[Fact]
public async Task GetParamsFromQueryAsync()
{
var result = await _peopleAppService.GetParamsFromQueryAsync(new GetParamsInput()
{
NameValues = new List<GetParamsNameValue>()
{
new GetParamsNameValue()
{
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
},
NameValue = new GetParamsNameValue()
{
Name = "name3",
Value = "value3"
}
});
result.ShouldBe("name1-value1:name2-value2:name3-value3");
}
[Fact]
public async Task GetParamsFromFormAsync()
{
var result = await _peopleAppService.GetParamsFromFormAsync(new GetParamsInput()
{
NameValues = new List<GetParamsNameValue>()
{
new GetParamsNameValue()
{
Name = "name1",
Value = "value1"
},
new GetParamsNameValue()
{
Name = "name2",
Value = "value2"
}
},
NameValue = new GetParamsNameValue()
{
Name = "name3",
Value = "value3"
}
});
result.ShouldBe("name1-value1:name2-value2:name3-value3");
}
}
}

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.Abp.TestApp.Application.Dto;
namespace Volo.Abp.Http.DynamicProxying
{
public class TestObjectToFormData : IObjectToFormData<List<GetParamsNameValue>>, ITransientDependency
{
public Task<List<KeyValuePair<string, HttpContent>>> ConvertAsync(List<GetParamsNameValue> values)
{
if (values.IsNullOrEmpty())
{
return null;
}
var formDataContents = new List<KeyValuePair<string, HttpContent>>();
for (var i = 0; i < values.Count; i++)
{
formDataContents.Add(new KeyValuePair<string, HttpContent>($"NameValues[{i}].Name", new StringContent(values[i].Name, Encoding.UTF8)));
formDataContents.Add(new KeyValuePair<string, HttpContent>($"NameValues[{i}].Value", new StringContent(values[i].Value, Encoding.UTF8)));
}
return Task.FromResult(formDataContents);
}
}
}

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.Abp.TestApp.Application.Dto;
namespace Volo.Abp.Http.DynamicProxying
{
public class TestObjectToQueryString : IObjectToQueryString<List<GetParamsNameValue>>, ITransientDependency
{
public Task<string> ConvertAsync(List<GetParamsNameValue> values)
{
if (values.IsNullOrEmpty())
{
return null;
}
var sb = new StringBuilder();
for (var i = 0; i < values.Count; i++)
{
sb.Append($"NameValues[{i}].Name={values[i].Name}&NameValues[{i}].Value={values[i].Value}&");
}
sb.Remove(sb.Length - 1, 1);
return Task.FromResult(sb.ToString());
}
}
}

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\..\common.test.props" />

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Volo.Abp.TestApp.Application.Dto
{
public class GetParamsInput
{
public List<GetParamsNameValue> NameValues { get; set; }
public GetParamsNameValue NameValue { get; set; }
}
public class GetParamsNameValue
{
public string Name { get; set; }
public string Value { get; set; }
}
}

@ -32,5 +32,8 @@ namespace Volo.Abp.TestApp.Application
Task<string> CreateMultipleFileAsync(CreateMultipleFileInput input);
Task<string> GetParamsFromQueryAsync(GetParamsInput input);
Task<string> GetParamsFromFormAsync(GetParamsInput input);
}
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Domain.Repositories;
@ -125,5 +126,21 @@ namespace Volo.Abp.TestApp.Application
return str;
}
public Task<string> GetParamsFromQueryAsync([FromQuery]GetParamsInput input)
{
return Task.FromResult(input.NameValues?.FirstOrDefault()?.Name + "-" +
input.NameValues?.FirstOrDefault()?.Value + ":" +
input.NameValues?.LastOrDefault()?.Name + "-" + input.NameValues?.LastOrDefault()?.Value + ":" +
input.NameValue?.Name + "-" + input.NameValue?.Value);
}
public Task<string> GetParamsFromFormAsync([FromForm]GetParamsInput input)
{
return Task.FromResult(input.NameValues?.FirstOrDefault()?.Name + "-" +
input.NameValues?.FirstOrDefault()?.Value + ":" +
input.NameValues?.LastOrDefault()?.Name + "-" + input.NameValues?.LastOrDefault()?.Value + ":" +
input.NameValue?.Name + "-" + input.NameValue?.Value);
}
}
}

@ -0,0 +1,67 @@
{
"culture": "da",
"texts": {
"UserName": "Brugernavn",
"EmailAddress": "Email",
"UserNameOrEmailAddress": "Brugernavn eller email",
"Password": "Kodeord",
"RememberMe": "Husk mig",
"UseAnotherServiceToLogin": "Brug en anden service til at logge ind",
"UserLockedOutMessage": "Brugerkontoen er blevet låst grundet forkerte login forsøg. Vent venligst og prøv igen.",
"InvalidUserNameOrPassword": "Forkert brugernavn eller kodeord!",
"LoginIsNotAllowed": "Du kan ikke logge ind! Du skal bekræfte den angivne email/telefonnummer.",
"SelfRegistrationDisabledMessage": "Selvregistrering er deaktiveret for denne applikation. Kontakt venligst applikationens administrator for at registrere en ny bruger.",
"LocalLoginDisabledMessage": "Lokalt login er deaktiveret for denne appliaktion.",
"Login": "Login",
"Cancel": "Annullér",
"Register": "Registrér",
"AreYouANewUser": "Er du ny bruger?",
"AlreadyRegistered": "Allerede registreret?",
"InvalidLoginRequest": "Forkert login forespørgsel",
"ThereAreNoLoginSchemesConfiguredForThisClient": "Der er ingen konfigurerede login skemaer for denne klient.",
"LogInUsingYourProviderAccount": "Login med din {0} konto",
"DisplayName:CurrentPassword": "Nuværende kodeord",
"DisplayName:NewPassword": "Nyt kodeord",
"DisplayName:NewPasswordConfirm": "Bekræft nyt kodeord",
"PasswordChangedMessage": "Dit kodeord er blevet ændret.",
"DisplayName:UserName": "Brugernavn",
"DisplayName:Email": "Email",
"DisplayName:Name": "Fornavn",
"DisplayName:Surname": "Efternavn",
"DisplayName:Password": "Kodeord",
"DisplayName:EmailAddress": "Email",
"DisplayName:PhoneNumber": "Telefonnummer",
"PersonalSettings": "Personlige indstillinger",
"PersonalSettingsSaved": "Personlige indstillinger gemt",
"PasswordChanged": "Kodeord gemt",
"NewPasswordConfirmFailed": "Bekræft venligst det nye kodeord.",
"Manage": "Administrer",
"MyAccount": "Min konto",
"DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Er registrering aktiveret",
"Description:Abp.Account.IsSelfRegistrationEnabled": "Om en bruger kan registrere kontoen selv.",
"DisplayName:Abp.Account.EnableLocalLogin": "Godkende med en lokal konto",
"Description:Abp.Account.EnableLocalLogin": "Indikerer om serveren vil tillade brugere at logge ind med lokal konto.",
"LoggedOutTitle": "Logget ud",
"LoggedOutText": "Du er blevet logget ud og vil blive videresendt snarest.",
"ReturnToText": "Klik her for at returnere til applikationen",
"OrLoginWith": "Eller login med:",
"ForgotPassword": "Glemt kodeord?",
"SendPasswordResetLink_Information": "Et link til at nulstille dit kodeord vil blive sendt til din email. Hvis du ikke modtager en email indenfor et par minutter, venligst prøv igen.",
"PasswordResetMailSentMessage": "Gendannelse af konto er sendt til din email. Hvis du ikke har modtaget emailen indenfor 15 minutter, kig i din spam folder. Hvis du finder den dér, venligst markér den som -Ikke Spam-. ",
"ResetPassword": "Nulstil Kodeord",
"ConfirmPassword": "Bekræft (gentag) kodeordet",
"ResetPassword_Information": "Venligst indtast dit nye kodeord.",
"YourPasswordIsSuccessfullyReset": "Dit kodeord blev ændret.",
"GoToTheApplication": "Gå til applikationen",
"BackToLogin": "Tilbage til login",
"ProfileTab:Password": "Skift kodeord",
"ProfileTab:PersonalInfo": "Personlig info",
"ReturnToApplication": "Gå tilbage til applikationen",
"Volo.Account:InvalidEmailAddress": "Kan ikke finde den angivne email: {0}",
"PasswordReset": "Nulstil kodeord",
"PasswordResetInfoInEmail": "Vi har modtaget en forespørgsel for gendannelse af konto! Hvis du har lavet denne forespørgsel, klik på det efterfølgende link for at nulstille dit kodeord.",
"ResetMyPassword": "Nulstil mit kodeord",
"AccessDenied": "Adgang nægtet!",
"AccessDeniedMessage": "Du har ikke adgang til denne ressource."
}
}

@ -14,9 +14,9 @@
"ReIndexAllProjects": "ReIndex all projects",
"ReIndexProject": "ReIndex project",
"ReIndexProjectConfirmationMessage": "Are you sure to reindex for project \"{0}\"",
"SuccessfullyReIndexProject": "Successfully reindex for project \"{0}\"",
"SuccessfullyReIndexProject": "Successfully reindexed: \"{0}\"",
"ReIndexAllProjectConfirmationMessage": "Are you sure to reindex all project?",
"SuccessfullyReIndexAllProject": "Successfully reindex for all projects",
"SuccessfullyReIndexAllProject": "Successfully reindexed all projects",
"InThisDocument": "In this document",
"GoToTop": "Go to top",
"Projects": "Project(s)",

@ -246,16 +246,31 @@ namespace Volo.Docs.Pages.Documents.Project
{
var projects = await _projectAppService.GetListAsync();
var sb = new StringBuilder();
ProjectSelectItems = projects.Items.Select(p => new SelectListItem
{
Text = p.Name,
Value = p.Id != Project.Id ? sb.Append(DocumentsUrlPrefix).Append(LanguageCode).Append("/").Append(p.ShortName).Append("/").Append(DocsAppConsts.Latest).ToString() : null,
Value = CreateProjectLink(p),
Selected = p.Id == Project.Id
}).ToList();
}
private string CreateProjectLink(ProjectDto project)
{
if (project.Id == Project.Id)
{
return null;
}
return new StringBuilder()
.Append(DocumentsUrlPrefix)
.Append(LanguageCode)
.Append('/')
.Append(project.ShortName)
.Append('/')
.Append(DocsAppConsts.Latest)
.ToString();
}
private async Task SetVersionAsync()
{
//TODO: Needs refactoring

@ -1,11 +1,16 @@
import { ContentSecurityStrategy, CONTENT_SECURITY_STRATEGY } from './content-security.strategy';
import { DomStrategy, DOM_STRATEGY } from './dom.strategy';
export type ElementOptions<T extends HTMLScriptElement | HTMLStyleElement = any> = Partial<{
[key in keyof T]: T[key];
}>;
export abstract class ContentStrategy<T extends HTMLScriptElement | HTMLStyleElement = any> {
constructor(
public content: string,
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
protected contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.None(),
protected options: ElementOptions<T> = {},
) {}
abstract createElement(): T;
@ -13,6 +18,10 @@ export abstract class ContentStrategy<T extends HTMLScriptElement | HTMLStyleEle
insertElement(): T {
const element = this.createElement();
if (this.options && Object.keys(this.options).length > 0) {
Object.keys(this.options).forEach(key => (element[key] = this.options[key]));
}
this.contentSecurityStrategy.applyCSP(element);
this.domStrategy.insertElement(element);
@ -39,16 +48,16 @@ export class ScriptContentStrategy extends ContentStrategy<HTMLScriptElement> {
}
export const CONTENT_STRATEGY = {
AppendScriptToBody(content: string) {
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToBody());
AppendScriptToBody(content: string, options?: ElementOptions<HTMLScriptElement>) {
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToBody(), undefined, options);
},
AppendScriptToHead(content: string) {
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToHead());
AppendScriptToHead(content: string, options?: ElementOptions<HTMLScriptElement>) {
return new ScriptContentStrategy(content, DOM_STRATEGY.AppendToHead(), undefined, options);
},
AppendStyleToHead(content: string) {
return new StyleContentStrategy(content, DOM_STRATEGY.AppendToHead());
AppendStyleToHead(content: string, options?: ElementOptions<HTMLStyleElement>) {
return new StyleContentStrategy(content, DOM_STRATEGY.AppendToHead(), undefined, options);
},
PrependStyleToHead(content: string) {
return new StyleContentStrategy(content, DOM_STRATEGY.PrependToHead());
PrependStyleToHead(content: string, options?: ElementOptions<HTMLStyleElement>) {
return new StyleContentStrategy(content, DOM_STRATEGY.PrependToHead(), undefined, options);
},
};

@ -2,3 +2,4 @@ export * from './lib/components';
export * from './lib/directives';
export * from './lib/enums/components';
export * from './lib/feature-management.module';
export * from './lib/models';

@ -65,6 +65,7 @@ $projects = (
"framework/src/Volo.Abp.Autofac",
"framework/src/Volo.Abp.Autofac.WebAssembly",
"framework/src/Volo.Abp.AutoMapper",
"framework/src/Volo.Abp.AzureServiceBus",
"framework/src/Volo.Abp.BackgroundJobs.Abstractions",
"framework/src/Volo.Abp.BackgroundJobs",
"framework/src/Volo.Abp.BackgroundJobs.HangFire",
@ -106,6 +107,7 @@ $projects = (
"framework/src/Volo.Abp.EventBus.RabbitMQ",
"framework/src/Volo.Abp.EventBus.Kafka",
"framework/src/Volo.Abp.EventBus.Rebus",
"framework/src/Volo.Abp.EventBus.Azure",
"framework/src/Volo.Abp.ExceptionHandling",
"framework/src/Volo.Abp.Features",
"framework/src/Volo.Abp.FluentValidation",

Loading…
Cancel
Save