Browse Source

Merge branch 'master' into supports/open-telemetry

master
Savorboard 3 years ago
parent
commit
1c7b612c2f
100 changed files with 2067 additions and 1164 deletions
  1. +2
    -2
      .travis.yml
  2. +20
    -6
      CAP.sln
  3. +2
    -1
      README.md
  4. +2
    -2
      appveyor.yml
  5. +1
    -1
      build/BuildScript.csproj
  6. +3
    -3
      build/version.props
  7. +60
    -0
      docs/content/about/release-notes.md
  8. +15
    -3
      docs/content/user-guide/en/cap/configuration.md
  9. +10
    -2
      docs/content/user-guide/en/cap/filter.md
  10. +18
    -14
      docs/content/user-guide/en/cap/messaging.md
  11. +3
    -1
      docs/content/user-guide/en/transport/general.md
  12. +36
    -0
      docs/content/user-guide/en/transport/kafka.md
  13. +60
    -0
      docs/content/user-guide/en/transport/nats.md
  14. +42
    -0
      docs/content/user-guide/en/transport/pulsar.md
  15. +14
    -1
      docs/content/user-guide/zh/cap/configuration.md
  16. +11
    -2
      docs/content/user-guide/zh/cap/filter.md
  17. +19
    -14
      docs/content/user-guide/zh/cap/messaging.md
  18. +2
    -0
      docs/content/user-guide/zh/transport/general.md
  19. +61
    -0
      docs/content/user-guide/zh/transport/nats.md
  20. +43
    -0
      docs/content/user-guide/zh/transport/pulsar.md
  21. +34
    -31
      docs/mkdocs.yml
  22. +1
    -1
      samples/Sample.AmazonSQS.InMemory/Sample.AmazonSQS.InMemory.csproj
  23. +2
    -2
      samples/Sample.ConsoleApp/Sample.ConsoleApp.csproj
  24. +2
    -3
      samples/Sample.Dashboard.Auth/Sample.Dashboard.Auth.csproj
  25. +1
    -1
      samples/Sample.Kafka.PostgreSql/Sample.Kafka.PostgreSql.csproj
  26. +32
    -0
      samples/Sample.Pulsar.InMemory/Controllers/ValuesController.cs
  27. +20
    -0
      samples/Sample.Pulsar.InMemory/Program.cs
  28. +14
    -0
      samples/Sample.Pulsar.InMemory/Sample.Pulsar.InMemory.csproj
  29. +38
    -0
      samples/Sample.Pulsar.InMemory/Startup.cs
  30. +12
    -0
      samples/Sample.Pulsar.InMemory/appsettings.json
  31. +1
    -1
      samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj
  32. +1
    -10
      samples/Sample.RabbitMQ.MySql/AppDbContext.cs
  33. +6
    -2
      samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj
  34. +35
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Controllers/HomeController.cs
  35. +12
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/TestMessage.cs
  36. +25
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/VeryFastProcessingReceiver.cs
  37. +25
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/XSlowProcessingReceiver.cs
  38. +58
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Program.cs
  39. +26
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Sample.RabbitMQ.SqlServer.DispatcherPerGroup.csproj
  40. +52
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Startup.cs
  41. +4
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandler.cs
  42. +15
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandlerTopicAttribute.cs
  43. +31
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandlersExtensions.cs
  44. +87
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/TypedConsumerServiceSelector.cs
  45. +10
    -0
      samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/appsettings.json
  46. +1
    -1
      samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj
  47. +13
    -5
      samples/Samples.Redis.SqlServer/Controllers/HomeController.cs
  48. +25
    -0
      samples/Samples.Redis.SqlServer/Dockerfile
  49. +2
    -3
      samples/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj
  50. +3
    -3
      samples/Samples.Redis.SqlServer/Startup.cs
  51. +90
    -0
      samples/Samples.Redis.SqlServer/docker-compose.yml
  52. +3
    -3
      src/Directory.Build.props
  53. +85
    -12
      src/DotNetCore.CAP.AmazonSQS/AmazonPolicyExtensions.cs
  54. +46
    -17
      src/DotNetCore.CAP.AmazonSQS/AmazonSQSConsumerClient.cs
  55. +11
    -0
      src/DotNetCore.CAP.AmazonSQS/CAP.AmazonSQSOptions.cs
  56. +2
    -2
      src/DotNetCore.CAP.AmazonSQS/DotNetCore.CAP.AmazonSQS.csproj
  57. +37
    -11
      src/DotNetCore.CAP.AmazonSQS/ITransport.AmazonSQS.cs
  58. +1
    -1
      src/DotNetCore.CAP.AzureServiceBus/DotNetCore.CAP.AzureServiceBus.csproj
  59. +5
    -6
      src/DotNetCore.CAP.Dashboard/DotNetCore.CAP.Dashboard.csproj
  60. +0
    -128
      src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/AwaitableInfo.cs
  61. +0
    -56
      src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/CoercedAwaitableInfo.cs
  62. +0
    -338
      src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutor.cs
  63. +0
    -118
      src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs
  64. +0
    -145
      src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs
  65. +30
    -23
      src/DotNetCore.CAP.Dashboard/UiMiddleware.cs
  66. +1
    -0
      src/DotNetCore.CAP.Dashboard/wwwroot/package.json
  67. +1
    -3
      src/DotNetCore.CAP.Dashboard/wwwroot/public/index.html
  68. +3
    -2
      src/DotNetCore.CAP.Dashboard/wwwroot/src/pages/Published.vue
  69. +3
    -2
      src/DotNetCore.CAP.Dashboard/wwwroot/src/pages/Received.vue
  70. +88
    -61
      src/DotNetCore.CAP.InMemoryStorage/IDataStorage.InMemory.cs
  71. +1
    -1
      src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj
  72. +8
    -1
      src/DotNetCore.CAP.Kafka/IConnectionPool.Default.cs
  73. +1
    -5
      src/DotNetCore.CAP.Kafka/ITransport.Kafka.cs
  74. +9
    -4
      src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs
  75. +2
    -2
      src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs
  76. +2
    -2
      src/DotNetCore.CAP.MongoDB/DotNetCore.CAP.MongoDB.csproj
  77. +24
    -18
      src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj
  78. +7
    -1
      src/DotNetCore.CAP.NATS/CAP.NATSOptions.cs
  79. +7
    -2
      src/DotNetCore.CAP.NATS/CAP.Options.Extensions.cs
  80. +1
    -1
      src/DotNetCore.CAP.NATS/DotNetCore.CAP.NATS.csproj
  81. +4
    -2
      src/DotNetCore.CAP.NATS/IConnectionPool.Default.cs
  82. +22
    -18
      src/DotNetCore.CAP.NATS/ITransport.NATS.cs
  83. +65
    -28
      src/DotNetCore.CAP.NATS/NATSConsumerClient.cs
  84. +26
    -20
      src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj
  85. +40
    -0
      src/DotNetCore.CAP.Pulsar/CAP.Options.Extensions.cs
  86. +32
    -0
      src/DotNetCore.CAP.Pulsar/CAP.PulsarCapOptionsExtension.cs
  87. +37
    -0
      src/DotNetCore.CAP.Pulsar/CAP.PulsarOptions.cs
  88. +23
    -0
      src/DotNetCore.CAP.Pulsar/DotNetCore.CAP.Pulsar.csproj
  89. +77
    -0
      src/DotNetCore.CAP.Pulsar/IConnectionFactory.Default.cs
  90. +17
    -0
      src/DotNetCore.CAP.Pulsar/IConnectionFactory.cs
  91. +55
    -0
      src/DotNetCore.CAP.Pulsar/ITransport.Pulsar.cs
  92. +98
    -0
      src/DotNetCore.CAP.Pulsar/PulsarConsumerClient.cs
  93. +34
    -0
      src/DotNetCore.CAP.Pulsar/PulsarConsumerClientFactory.cs
  94. +10
    -0
      src/DotNetCore.CAP.Pulsar/PulsarHeaders.cs
  95. +1
    -1
      src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj
  96. +3
    -1
      src/DotNetCore.CAP.RabbitMQ/IConnectionChannelPool.Default.cs
  97. +4
    -8
      src/DotNetCore.CAP.RabbitMQ/ITransport.RabbitMQ.cs
  98. +18
    -3
      src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs
  99. +1
    -1
      src/DotNetCore.CAP.RedisStreams/DotNetCore.CAP.RedisStreams.csproj
  100. +20
    -2
      src/DotNetCore.CAP.RedisStreams/IConnectionPool.LazyConnection.cs

+ 2
- 2
.travis.yml View File

@@ -2,7 +2,7 @@ language: csharp
sudo: required
dist: xenial
solution: CAP.sln
dotnet: 5.0.100
dotnet: 6.0.100
mono: none
env:
- Cap_MySql_ConnectionString="Server=127.0.0.1;Database=cap_test;Uid=root;Pwd=;Allow User Variables=True;SslMode=none"
@@ -12,5 +12,5 @@ services:

script:
- export PATH="$PATH:$HOME/.dotnet/tools"
- dotnet tool install --global FlubuCore.GlobalTool --version 6.1.0
- dotnet tool install --global FlubuCore.GlobalTool --version 6.3.2
- flubu build tests

+ 20
- 6
CAP.sln View File

@@ -78,7 +78,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Dashboard.Auth", "sa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.MultiModuleSubscriberTests", "test\DotNetCore.CAP.MultiModuleSubscriberTests\DotNetCore.CAP.MultiModuleSubscriberTests.csproj", "{23684403-7DA8-489A-8A1E-8056D7683E18}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.OpenTelemetry", "src\DotNetCore.CAP.OpenTelemetry\DotNetCore.CAP.OpenTelemetry.csproj", "{D32FBDA5-41AC-4563-8CBA-F40C2C10E864}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.Pulsar", "src\DotNetCore.CAP.Pulsar\DotNetCore.CAP.Pulsar.csproj", "{AB7A10CB-2C7E-49CE-AA21-893772FF6546}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Pulsar.InMemory", "samples\Sample.Pulsar.InMemory\Sample.Pulsar.InMemory.csproj", "{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.SqlServer.DispatcherPerGroup", "samples\Sample.RabbitMQ.SqlServer.DispatcherPerGroup\Sample.RabbitMQ.SqlServer.DispatcherPerGroup.csproj", "{DCDF58E8-F823-4F04-9F8C-E8076DC16A68}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -186,10 +190,18 @@ Global
{23684403-7DA8-489A-8A1E-8056D7683E18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23684403-7DA8-489A-8A1E-8056D7683E18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23684403-7DA8-489A-8A1E-8056D7683E18}.Release|Any CPU.Build.0 = Release|Any CPU
{D32FBDA5-41AC-4563-8CBA-F40C2C10E864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D32FBDA5-41AC-4563-8CBA-F40C2C10E864}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D32FBDA5-41AC-4563-8CBA-F40C2C10E864}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D32FBDA5-41AC-4563-8CBA-F40C2C10E864}.Release|Any CPU.Build.0 = Release|Any CPU
{AB7A10CB-2C7E-49CE-AA21-893772FF6546}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB7A10CB-2C7E-49CE-AA21-893772FF6546}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB7A10CB-2C7E-49CE-AA21-893772FF6546}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB7A10CB-2C7E-49CE-AA21-893772FF6546}.Release|Any CPU.Build.0 = Release|Any CPU
{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5}.Release|Any CPU.Build.0 = Release|Any CPU
{DCDF58E8-F823-4F04-9F8C-E8076DC16A68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DCDF58E8-F823-4F04-9F8C-E8076DC16A68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCDF58E8-F823-4F04-9F8C-E8076DC16A68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCDF58E8-F823-4F04-9F8C-E8076DC16A68}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -220,7 +232,9 @@ Global
{54458B54-49CC-454C-82B2-4AED681D9D07} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{6E059983-DE89-4D53-88F5-D9083BCE257F} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{23684403-7DA8-489A-8A1E-8056D7683E18} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0}
{D32FBDA5-41AC-4563-8CBA-F40C2C10E864} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{AB7A10CB-2C7E-49CE-AA21-893772FF6546} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{B1D95CCD-0123-41D4-8CCB-9F834ED8D5C5} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{DCDF58E8-F823-4F04-9F8C-E8076DC16A68} = {3A6B6931-A123-477A-9469-8B468B5385AF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB}


+ 2
- 1
README.md View File

@@ -227,7 +227,8 @@ public void ShowTime2(DateTime datetime)
}

```
`ShowTime1` and `ShowTime2` will be called at the same time.
`ShowTime1` and `ShowTime2` will be called one after another because all received messages are processed linear.
You can change that behaviour increasing `ConsumerThreadCount`.

BTW, You can specify the default group name in the configuration:



+ 2
- 2
appveyor.yml View File

@@ -1,5 +1,5 @@
version: '{build}'
os: Visual Studio 2019
os: Visual Studio 2022
environment:
BUILDING_ON_PLATFORM: win
BuildEnvironment: appveyor
@@ -7,7 +7,7 @@ environment:
services:
- mysql
before_build:
- ps: dotnet tool install --global FlubuCore.GlobalTool --version 6.1.0
- ps: dotnet tool install --global FlubuCore.GlobalTool --version 6.3.2
build_script:
- ps: flubu
test: off


+ 1
- 1
build/BuildScript.csproj View File

@@ -9,7 +9,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FlubuCore" Version="6.1.0" />
<PackageReference Include="FlubuCore" Version="6.3.2" />
</ItemGroup>

</Project>

+ 3
- 3
build/version.props View File

@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<VersionMajor>5</VersionMajor>
<VersionMinor>1</VersionMinor>
<VersionPatch>1</VersionPatch>
<VersionMajor>6</VersionMajor>
<VersionMinor>0</VersionMinor>
<VersionPatch>0</VersionPatch>
<VersionQuality></VersionQuality>
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
</PropertyGroup>


+ 60
- 0
docs/content/about/release-notes.md View File

@@ -1,5 +1,65 @@
# Release Notes


## Version 5.1.2 (2021-07-26)

**Bug Fixed:**

* Fixed consumer register cancellation token source null referencee bug. (#952)
* Fixed redis streams transport cluster keys cross-hashslot bug. (#944)


## Version 5.1.1 (2021-07-09)

**Features:**

* Improve flow control for message cache of in memory. (#935)
* Add cancellation token support to subscribers. (#912)
* Add pathbase options for dashbaord. (#901)
* Add custom authorization scheme support for dashbaord. (#906)

**Bug Fixed:**

* Fixed mysql connect timeout expired bug. (#931)
* Fixed consul health check path invalid bug. (#921)
* Fixed mongo dashbaord query bug. (#909)

## Version 5.1.0 (2021-06-07)

**Features:**

* Add configure options for json serialization. (#879)
* Add Redis Streams transport support. (#817)
* New dashboard build with vue. (#880)
* Add subscribe filter support. (#894)

**Bug Fixed:**

* Fixed use CapEFDbTransaction to get dbtransaction extension method bug. (#868)
* Fixed pending message has not been deleted from buffer list in SQL Server. (#889)
* Fixed dispatcher processing when storage message exception bug. (#900)


## Version 5.0.3 (2021-05-14)

**Bug Fixed:**

* Fix the bug of getting db transaction through the IDbContextTransaction for SQLServer. (#867)
* Fix RabbitMQ Connection close forced. (#861)

## Version 5.0.2 (2021-04-28)

**Features:**

* Add support for Azure Service Bus sessions. (#829)
* Add custom message headers support for RabbitMQ consumer. (#818)

**Bug Fixed:**

* Downgrading Microsoft.Data.SqlClient to 2.0.1. (#839)
* DiagnosticObserver does not use null connection. (#845)
* Fix null reference in AmazonSQSTransport. (#846)

## Version 5.0.1 (2021-04-07)

**Features:**


+ 15
- 3
docs/content/user-guide/en/cap/configuration.md View File

@@ -85,11 +85,17 @@ During the message sending process if consumption method fails, CAP will try to
By default if failure occurs on send or consume, retry will start after **4 minutes** in order to avoid possible problems caused by setting message state delays.
Failures in the process of sending and consuming messages will be retried 3 times immediately, and will be retried polling after 3 times, at which point the FailedRetryInterval configuration will take effect.

#### CollectorCleaningInterval

> Default: 300 sec

The interval of the collector processor deletes expired messages.

#### ConsumerThreadCount

> Default : 1
> Default: 1

Number of consumer threads, when this value is greater than 1, the order of message execution cannot be guaranteed
Number of consumer threads, when this value is greater than 1, the order of message execution cannot be guaranteed.

#### FailedRetryCount

@@ -109,4 +115,10 @@ Failure threshold callback. This action is called when the retry reaches the val

> Default: 24*3600 sec (1 days)

The expiration time (in seconds) of the success message. When the message is sent or consumed successfully, it will be removed from database storage when the time reaches `SucceedMessageExpiredAfter` seconds. You can set the expiration time by specifying this value.
The expiration time (in seconds) of the success message. When the message is sent or consumed successfully, it will be removed from database storage when the time reaches `SucceedMessageExpiredAfter` seconds. You can set the expiration time by specifying this value.

#### UseDispatchingPerGroup

> Default: false

If `true` then all consumers within the same group pushes received messages to own dispatching pipeline channel. Each channel has set thread count to `ConsumerThreadCount` value.

+ 10
- 2
docs/content/user-guide/en/cap/filter.md View File

@@ -4,7 +4,7 @@ Subscriber filters are similar to ASP.NET MVC filters and are mainly used to pro

## Create subscribe filter

1. Create filter
### Create Filter

Create a new filter class and inherit the `SubscribeFilter` abstract class.

@@ -32,7 +32,15 @@ In some scenarios, if you want to terminate the subscriber method execution, you

To ignore exceptions, you can setting `context.ExceptionHandled = true` in `ExceptionContext`

2. Configuration

```C#
public override void OnSubscribeException(ExceptionContext context)
{
context.ExceptionHandled = true;
}
```

### Configuration Filter

Use `AddSubscribeFilter<>` to add a filter.



+ 18
- 14
docs/content/user-guide/en/cap/messaging.md View File

@@ -23,25 +23,27 @@ _capBus.Publish("place.order.qty.deducted",
// publisher using `callbackName` to subscribe consumer result

[CapSubscribe("place.order.mark.status")]
public void MarkOrderStatus(JToken param)
public void MarkOrderStatus(JsonElement param)
{
var orderId = param.Value<int>("OrderId");
var isSuccess = param.Value<bool>("IsSuccess");
var orderId = param.GetProperty("OrderId").GetInt32();
var isSuccess = param.GetProperty("IsSuccess").GetBoolean();
if(isSuccess)
//mark order status to succeeded
else
//mark order status to failed
if(isSuccess){
// mark order status to succeeded
}
else{
// mark order status to failed
}
}

// ============= Consumer ===================

[CapSubscribe("place.order.qty.deducted")]
public object DeductProductQty(JToken param)
public object DeductProductQty(JsonElement param)
{
var orderId = param.Value<int>("OrderId");
var productId = param.Value<int>("ProductId");
var qty = param.Value<int>("Qty");
var orderId = param.GetProperty("OrderId").GetInt32();
var productId = param.GetProperty("ProductId").GetInt32();
var qty = param.GetProperty("Qty").GetInt32();

//business logic

@@ -109,7 +111,7 @@ Retrying plays an important role in the overall CAP architecture design, CAP ret

During the message sending process, when the broker crashes or the connection fails or an abnormality occurs, CAP will retry the sending. Retry 3 times for the first time, retry every minute after 4 minutes, and +1 retry. When the total number of retries reaches 50,CAP will stop retrying.

You can adjust the total number of retries by setting `FailedRetryCount` in CapOptions.
You can adjust the total number of retries by setting [FailedRetryCount](../configuration#failedretrycount) in CapOptions.

It will stop when the maximum number of times is reached. You can see the reason for the failure in Dashboard and choose whether to manually retry.

@@ -123,6 +125,8 @@ There is an `ExpiresAt` field in the database message table indicating the expir

Consuming failure will change the message status to `Failed` and `ExpiresAt` will be set to **15 days** later.

By default, the data of the message in the table is deleted **every hour** to avoid performance degradation caused by too much data. The cleanup strategy `ExpiresAt` is performed when field is not empty and is less than the current time.
By default, the data of the message in the table is deleted **5 minutes** to avoid performance degradation caused by too much data. The cleanup strategy `ExpiresAt` is performed when field is not empty and is less than the current time.

That is to say, the message with the status Failed (by default they have been retried 50 times), if you do not have manual intervention for 15 days, it will **also be** cleaned up.
That is to say, the message with the status Failed (by default they have been retried 50 times), if you do not have manual intervention for 15 days, it will **also be** cleaned up.

You can use [CollectorCleaningInterval](../configuration#collectorcleaninginterval) configuration items to custom the interval time.

+ 3
- 1
docs/content/user-guide/en/transport/general.md View File

@@ -10,8 +10,10 @@ CAP supports several transport methods:
* [Kafka](kafka.md)
* [Azure Service Bus](azure-service-bus.md)
* [Amazon SQS](aws-sqs.md)
* [NATS](nats.md)
* [In-Memory Queue](in-memory-queue.md)
* [Redis Streams](redis-streams.md)
* [Apache Pulsar](pulsar.md)

## How to select a transport

@@ -29,7 +31,7 @@ CAP supports several transport methods:
>`Kafka` vs `RabbitMQ` :
> https://stackoverflow.com/questions/42151544/is-there-any-reason-to-use-rabbitmq-over-kafka

## Community-supported extensions
## Community-supported transport extensions

Thanks to the community for supporting CAP, the following is the implementation of community-supported transport



+ 36
- 0
docs/content/user-guide/en/transport/kafka.md View File

@@ -40,6 +40,42 @@ NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Servers | Broker server address | string |
ConnectionPoolSize | connection pool size | int | 10
CustomHeaders | Custom subscribe headers | Func<> | N/A

#### CustomHeaders Options

When the message sent from a heterogeneous system, because of the CAP needs to define additional headers, so an exception will occur at this time. By providing this parameter to set the custom headersn to make the subscriber works.

You can find the description of [Header Information](../cap/messaging#heterogeneous-system-integration) here.

Sometimes, if you want to get additional context information from Broker, you can also add it through this option. For example, add information such as Offset or Partition.

Example:

```C#
x.UseKafka(opt =>
{
//...

opt.CustomHeaders = kafkaResult => new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("my.kafka.offset", kafkaResult.Offset.ToString()),
new KeyValuePair<string, string>("my.kafka.partition", kafkaResult.Partition.ToString())
};
});
```

Then you can get the header you added by this way:

```C#
[CapSubscribe("sample.kafka.postgrsql")]
public void HeadersTest(DateTime value, [FromCap]CapHeader header)
{
var offset = header["my.kafka.offset"];
var partition = header["my.kafka.partition"];
}
```


#### Kafka MainConfig Options



+ 60
- 0
docs/content/user-guide/en/transport/nats.md View File

@@ -0,0 +1,60 @@
# NATS

[NATS](https://nats.io/) is a simple, secure and performant communications system for digital systems, services and devices. NATS is part of the Cloud Native Computing Foundation (CNCF).

!!! warning
Versions of CAP below 5.2.0 are implement based on Request/Response mode, and now we are based on JetStream implementation.
see https://github.com/dotnetcore/CAP/issues/983 for more information.

## Configuration

To use NATS transporter, you need to install the following package from NuGet:

```powershell

PM> Install-Package DotNetCore.CAP.NATS

```

Then you can add configuration items to the `ConfigureServices` method of `Startup.cs`.

```csharp

public void ConfigureServices(IServiceCollection services)
{
services.AddCap(capOptions =>
{
capOptions.UseNATS(natsOptions=>{
//NATS Options
});
});
}

```

#### NATS Options

NATS configuration parameters provided directly by the CAP:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Options | NATS client configuration | Options | Options
Servers | Server url/urls used to connect to the NATs server. | string | NULL
ConnectionPoolSize | number of connections pool | uint | 10

#### NATS ConfigurationOptions

If you need **more** native NATS related configuration options, you can set them in the `Options` option:

```csharp
services.AddCap(capOptions =>
{
capOptions.UseNATS(natsOptions=>
{
// NATS options.
natsOptions.Options.Url="";
});
});
```

`Options` is a NATS.Client ConfigurationOptions , you can find more details through this [link](http://nats-io.github.io/nats.net/class_n_a_t_s_1_1_client_1_1_options.html)

+ 42
- 0
docs/content/user-guide/en/transport/pulsar.md View File

@@ -0,0 +1,42 @@
# Apache Pulsar

[Apache Pulsar](https://pulsar.apache.org/) is a cloud-native, distributed messaging and streaming platform originally created at Yahoo! and now a top-level Apache Software Foundation project.

Pulsar can be used in CAP as a message transporter.

## Configuration

To use Pulsar transporter, you need to install the following package from NuGet:

```powershell
PM> Install-Package DotNetCore.CAP.Pulsar

```

Then you can add configuration items to the `ConfigureServices` method of `Startup.cs`.

```csharp

public void ConfigureServices(IServiceCollection services)
{
// ...

services.AddCap(x =>
{
x.UsePulsar(opt => {
//Pulsar options
});
// x.UseXXX ...
});
}

```

#### Pulsar Options

The Pulsar configuration parameters provided directly by the CAP:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
ServiceUrl | Broker server address | string |
TlsOptions | Tls configuration | object |

+ 14
- 1
docs/content/user-guide/zh/cap/configuration.md View File

@@ -92,6 +92,12 @@ services.AddCap(config =>

消费者线程并行处理消息的线程数,当这个值大于1时,将不能保证消息执行的顺序。

#### CollectorCleaningInterval

默认值:300 秒

收集器删除已经过期消息的时间间隔。

#### FailedRetryCount

默认值:50
@@ -110,4 +116,11 @@ services.AddCap(config =>

默认值:24*3600 秒(1天后)

成功消息的过期时间(秒)。 当消息发送或者消费成功时候,在时间达到 `SucceedMessageExpiredAfter` 秒时候将会从 Persistent 中删除,你可以通过指定此值来设置过期的时间。
成功消息的过期时间(秒)。 当消息发送或者消费成功时候,在时间达到 `SucceedMessageExpiredAfter` 秒时候将会从 Persistent 中删除,你可以通过指定此值来设置过期的时间。

#### UseDispatchingPerGroup

> 默认值: false

默认情况下,CAP会将所有消费者组的消息都先放置到内存同一个Channel中,然后线性处理。
如果设置为 true,则每个消费者组都会根据 `ConsumerThreadCount` 设置的值创建单独的线程进行处理。

+ 11
- 2
docs/content/user-guide/zh/cap/filter.md View File

@@ -6,7 +6,9 @@

## 自定义过滤器

1、创建一个过滤器类,并继承 `SubscribeFilter` 抽象类。
### 添加过滤器

创建一个过滤器类,并继承 `SubscribeFilter` 抽象类。

```C#
public class MyCapFilter: SubscribeFilter
@@ -32,7 +34,14 @@ public class MyCapFilter: SubscribeFilter

通过在 `ExceptionContext` 中设置 `context.ExceptionHandled = true` 来忽略异常。

2、集成
```C#
public override void OnSubscribeException(ExceptionContext context)
{
context.ExceptionHandled = true;
}
```

### 配置过滤器

```C#
services.AddCap(opt =>


+ 19
- 14
docs/content/user-guide/zh/cap/messaging.md View File

@@ -19,30 +19,34 @@
```C#
// ============= Publisher =================

_capBus.Publish("place.order.qty.deducted", new { OrderId = 1234, ProductId = 23255, Qty = 1 }, "place.order.mark.status");
_capBus.Publish("place.order.qty.deducted",
contentObj: new { OrderId = 1234, ProductId = 23255, Qty = 1 },
callbackName: "place.order.mark.status");

// publisher using `callbackName` to subscribe consumer result

[CapSubscribe("place.order.mark.status")]
public void MarkOrderStatus(JToken param)
public void MarkOrderStatus(JsonElement param)
{
var orderId = param.Value<int>("OrderId");
var isSuccess = param.Value<bool>("IsSuccess");
var orderId = param.GetProperty("OrderId").GetInt32();
var isSuccess = param.GetProperty("IsSuccess").GetBoolean();
if(isSuccess)
//mark order status to succeeded
else
//mark order status to failed
if(isSuccess){
// mark order status to succeeded
}
else{
// mark order status to failed
}
}

// ============= Consumer ===================

[CapSubscribe("place.order.qty.deducted")]
public object DeductProductQty(JToken param)
public object DeductProductQty(JsonElement param)
{
var orderId = param.Value<int>("OrderId");
var productId = param.Value<int>("ProductId");
var qty = param.Value<int>("Qty");
var orderId = param.GetProperty("OrderId").GetInt32();
var productId = param.GetProperty("ProductId").GetInt32();
var qty = param.GetProperty("Qty").GetInt32();

//business logic

@@ -109,7 +113,7 @@ CAP 接收到消息之后会将消息进行 Persistent(持久化), 有关

在消息发送过程中,当出现 Broker 宕机或者连接失败的情况亦或者出现异常的情况下,这个时候 CAP 会对发送的重试,第一次重试次数为 3,4分钟后以后每分钟重试一次,进行次数 +1,当总次数达到50次后,CAP将不对其进行重试。

你可以在 CapOptions 中设置FailedRetryCount来调整默认重试的总次数。
你可以在 CapOptions 中设置 [FailedRetryCount](../configuration#failedretrycount) 来调整默认重试的总次数。

当失败总次数达到默认失败总次数后,就不会进行重试了,你可以在 Dashboard 中查看消息失败的原因,然后进行人工重试处理。

@@ -121,4 +125,5 @@ CAP 接收到消息之后会将消息进行 Persistent(持久化), 有关

数据库消息表中具有一个 ExpiresAt 字段表示消息的过期时间,当消息发送成功或者消费成功后,CAP会将消息状态为 Successed 的 ExpiresAt 设置为 1天 后过期,会将消息状态为 Failed 的 ExpiresAt 设置为 15天 后过期。

CAP 默认情况下会每隔一个小时将消息表的数据进行清理删除,避免数据量过多导致性能的降低。清理规则为 ExpiresAt 不为空并且小于当前时间的数据。 也就是说状态为Failed的消息(正常情况他们已经被重试了 50 次),如果你15天没有人工介入处理,同样会被清理掉。
CAP 默认情况下会每隔**5分钟**将消息表的数据进行清理删除,避免数据量过多导致性能的降低。清理规则为 ExpiresAt 不为空并且小于当前时间的数据。 也就是说状态为Failed的消息(正常情况他们已经被重试了 50 次),如果你15天没有人工介入处理,同样会被清理掉。你可以通过 [CollectorCleaningInterval](../configuration#collectorcleaninginterval) 配置项来自定义间隔时间。


+ 2
- 0
docs/content/user-guide/zh/transport/general.md View File

@@ -10,8 +10,10 @@ CAP 支持以下几种运输方式:
* [Kafka](kafka.md)
* [Azure Service Bus](azure-service-bus.md)
* [Amazon SQS](aws-sqs.md)
* [NATS](nats.md)
* [In-Memory Queue](in-memory-queue.md)
* [Redis Streams](redis-streams.md)
* [Apache Pulsar](pulsar.md)

## 怎么选择运输器



+ 61
- 0
docs/content/user-guide/zh/transport/nats.md View File

@@ -0,0 +1,61 @@
# NATS

[NATS](https://nats.io/)是一个简单、安全、高性能的数字系统、服务和设备通信系统。NATS 是 CNCF 的一部分。

!!! warning
CAP 5.2.0 以下的版本基于 Request/Response 实现, 现在我们已经基于 JetStream 实现。
查看 https://github.com/dotnetcore/CAP/issues/983 了解更多。

## 配置

要使用NATS 传输器,你需要安装下面的NuGet包:

```powershell

PM> Install-Package DotNetCore.CAP.NATS

```

你可以通过在 `Startup.cs` 文件中配置 `ConfigureServices` 来添加配置:

```csharp

public void ConfigureServices(IServiceCollection services)
{
services.AddCap(capOptions =>
{
capOptions.UseNATS(natsOptions=>{
//NATS Options
});
});
}

```

#### NATS 配置

CAP 直接提供的关于 NATS 的配置参数:


NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Options | NATS 客户端配置 | Options | Options
Servers | 服务器Urls地址 | string | NULL
ConnectionPoolSize | 连接池数量 | uint | 10

#### NATS ConfigurationOptions

如果你需要 **更多** 原生相关的配置项,可以通过 `Options` 配置项进行设定:

```csharp
services.AddCap(capOptions =>
{
capOptions.UseNATS(natsOptions=>
{
// NATS options.
natsOptions.Options.Url="";
});
});
```

`Options` 是 NATS.Client 客户端提供的配置, 你可以在这个[链接](http://nats-io.github.io/nats.net/class_n_a_t_s_1_1_client_1_1_options.html)找到更多详细信息。

+ 43
- 0
docs/content/user-guide/zh/transport/pulsar.md View File

@@ -0,0 +1,43 @@
# Apache Pulsar

[Apache Pulsar](https://pulsar.apache.org/) 是一个用于服务器到服务器的消息系统,具有多租户、高性能等优势。 Pulsar 最初由 Yahoo 开发,目前由 Apache 软件基金会管理。

CAP 支持使用 Apache Pulsar 作为消息传输器。

## Configuration

要使用 Pulsar 作为消息传输器,你需要从 NuGet 安装以下扩展包:

```shell

Install-Package DotNetCore.CAP.Pulsar

```

然后,你可以在 `Startup.cs` 的 `ConfigureServices` 方法中添加基于 Pulsar 的配置项。

```csharp

public void ConfigureServices(IServiceCollection services)
{
// ...

services.AddCap(x =>
{
x.UsePulsar(opt => {
//Pulsar Options
});
// x.UseXXX ...
});
}

```

#### Pulsar Options

CAP 直接对外提供的 Pulsar 配置参数如下:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
ServiceUrl | Broker 地址 | string |
TlsOptions | TLS 配置项 | object |

+ 34
- 31
docs/mkdocs.yml View File

@@ -4,31 +4,30 @@ site_url: http://cap.dotnetcore.xyz
site_description: A distributed transaction solution in micro-service base on eventually consistency, also an eventbus with Outbox pattern.
site_author: CAP Team

repo_name: 'GitHub'
repo_url: 'https://github.com/dotnetcore/CAP'
edit_uri: 'edit/master/docs/content'
docs_dir: 'content'
repo_name: "GitHub"
repo_url: "https://github.com/dotnetcore/CAP"
edit_uri: "edit/master/docs/content"
docs_dir: "content"

# Copyright
copyright: Copyright &copy; 2020 <a href="https://github.com/dotnetcore">NCC</a>, Maintained by the <a href="/about/contact-us/#cap-team">CAP Team</a>.

copyright: Copyright &copy; 2021 <a href="https://github.com/dotnetcore">NCC</a>, Maintained by the <a href="/about/contact-us/#cap-team">CAP Team</a>.

#theme: material
theme:
name: 'material'
name: "material"
palette:
primary: 'deep purple'
accent: 'indigo'
primary: "deep purple"
accent: "indigo"
language: en
include_sidebar: true
logo: 'img/logo.svg'
favicon: 'img/favicon.ico'
logo: "img/logo.svg"
favicon: "img/favicon.ico"
features:
- navigation.tabs
- navigation.instant
i18n:
prev: 'Previous'
next: 'Next'
prev: "Previous"
next: "Next"

#Customization
extra:
@@ -40,12 +39,12 @@ extra:
link: /user-guide/zh/getting-started/quick-start
lang: zh
social:
- icon: 'fontawesome/brands/github'
link: 'https://github.com/dotnetcore/CAP'
- icon: 'fontawesome/brands/twitter'
link: 'https://twitter.com/ncc_community'
- icon: 'fontawesome/brands/weibo'
link: 'https://weibo.com/dotnetcore'
- icon: "fontawesome/brands/github"
link: "https://github.com/dotnetcore/CAP"
- icon: "fontawesome/brands/twitter"
link: "https://twitter.com/ncc_community"
- icon: "fontawesome/brands/weibo"
link: "https://weibo.com/dotnetcore"

# Extensions
markdown_extensions:
@@ -80,12 +79,12 @@ markdown_extensions:
- pymdownx.tilde

nav:
- Home: index.md
- Documentation:
- Home: index.md
- Documentation:
- Getting Started:
- Quick Start: user-guide/en/getting-started/quick-start.md
- Introduction: user-guide/en/getting-started/introduction.md
- Contributing: user-guide/en/getting-started/contributing.md
- Contributing: user-guide/en/getting-started/contributing.md
- CAP:
- Configuration: user-guide/en/cap/configuration.md
- Messaging: user-guide/en/cap/messaging.md
@@ -94,10 +93,12 @@ nav:
- Transactions: user-guide/en/cap/transactions.md
- Idempotence: user-guide/en/cap/idempotence.md
- Transport:
- General: user-guide/en/transport/general.md
- General: user-guide/en/transport/general.md
- Amazon SQS: user-guide/en/transport/aws-sqs.md
- Apache Kafka®: user-guide/en/transport/kafka.md
- Apache Pulsar: user-guide/en/transport/pulsar.md
- Azure Service Bus: user-guide/en/transport/azure-service-bus.md
- NATS: user-guide/en/transport/nats.md
- RabbitMQ: user-guide/en/transport/rabbitmq.md
- Redis Streams: user-guide/en/transport/redis-streams.md
- In-Memory Queue: user-guide/en/transport/in-memory-queue.md
@@ -116,23 +117,25 @@ nav:
- Github: user-guide/en/samples/github.md
- eShopOnContainers: user-guide/en/samples/eshoponcontainers.md
- FAQ: user-guide/en/samples/faq.md
- 文档(中文):
- 文档(中文):
- 入门:
- 快速开始: user-guide/zh/getting-started/quick-start.md
- 介绍: user-guide/zh/getting-started/introduction.md
- 贡献: user-guide/zh/getting-started/contributing.md
- 贡献: user-guide/zh/getting-started/contributing.md
- CAP:
- 配置: user-guide/zh/cap/configuration.md
- 消息: user-guide/zh/cap/messaging.md
- 过滤器: user-guide/zh/cap/filter.md
- 序列化: user-guide/zh/cap/serialization.md
- 事务: user-guide/zh/cap/transactions.md
- 幂等性: user-guide/zh/cap/idempotence.md
- 幂等性: user-guide/zh/cap/idempotence.md
- 传输:
- 简介: user-guide/zh/transport/general.md
- Amazon SQS: user-guide/zh/transport/aws-sqs.md
- Apache Kafka®: user-guide/zh/transport/kafka.md
- 简介: user-guide/zh/transport/general.md
- Amazon SQS: user-guide/zh/transport/aws-sqs.md
- Apache Kafka®: user-guide/zh/transport/kafka.md
- Apache Pulsar: user-guide/zh/transport/pulsar.md
- Azure Service Bus: user-guide/zh/transport/azure-service-bus.md
- NATS: user-guide/zh/transport/nats.md
- RabbitMQ: user-guide/zh/transport/rabbitmq.md
- Redis Streams: user-guide/zh/transport/redis-streams.md
- In-Memory Queue: user-guide/zh/transport/in-memory-queue.md
@@ -153,7 +156,7 @@ nav:
- Github: user-guide/zh/samples/github.md
- eShopOnContainers: user-guide/zh/samples/eshoponcontainers.md
- FAQ: user-guide/zh/samples/faq.md
- About:
- About:
- Contact Us: about/contact-us.md
- Release Notes: about/release-notes.md
- License: about/license.md
@@ -161,4 +164,4 @@ nav:
# Google Analytics
google_analytics:
- !!python/object/apply:os.getenv ["GOOGLE_ANALYTICS_KEY"]
- auto
- auto

+ 1
- 1
samples/Sample.AmazonSQS.InMemory/Sample.AmazonSQS.InMemory.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>


+ 2
- 2
samples/Sample.ConsoleApp/Sample.ConsoleApp.csproj View File

@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup>
<ItemGroup>


+ 2
- 3
samples/Sample.Dashboard.Auth/Sample.Dashboard.Auth.csproj View File

@@ -1,18 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.InMemoryStorage\DotNetCore.CAP.InMemoryStorage.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.MySql\DotNetCore.CAP.MySql.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.RabbitMQ\DotNetCore.CAP.RabbitMQ.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.0" />
</ItemGroup>
</Project>

+ 1
- 1
samples/Sample.Kafka.PostgreSql/Sample.Kafka.PostgreSql.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<WarningsAsErrors>NU1701</WarningsAsErrors>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>


+ 32
- 0
samples/Sample.Pulsar.InMemory/Controllers/ValuesController.cs View File

@@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;

namespace Sample.Pulsar.InMemory.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller, ICapSubscribe
{
private readonly ICapPublisher _capBus;

public ValuesController(ICapPublisher producer)
{
_capBus = producer;
}

[Route("~/without/transaction")]
public async Task<IActionResult> WithoutTransaction()
{
await _capBus.PublishAsync("persistent://public/default/captesttopic", DateTime.Now);

return Ok();
}

[CapSubscribe("persistent://public/default/captesttopic")]
public void Test2T2(string value)
{
Console.WriteLine("Subscriber output message: " + value);
}
}
}

+ 20
- 0
samples/Sample.Pulsar.InMemory/Program.cs View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace Sample.Pulsar.InMemory
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

+ 14
- 0
samples/Sample.Pulsar.InMemory/Sample.Pulsar.InMemory.csproj View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.InMemoryStorage\DotNetCore.CAP.InMemoryStorage.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.Pulsar\DotNetCore.CAP.Pulsar.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>

</Project>

+ 38
- 0
samples/Sample.Pulsar.InMemory/Startup.cs View File

@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Sample.Pulsar.InMemory
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
string pulsarUri = Configuration.GetValue("AppSettings:PulsarUri", "pulsar//localhost:6650");
services.AddCap(x =>
{
x.UseInMemoryStorage();
x.UsePulsar(pulsarUri);
x.UseDashboard();
});

services.AddControllers();
}

public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

+ 12
- 0
samples/Sample.Pulsar.InMemory/appsettings.json View File

@@ -0,0 +1,12 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug"
}
},
"AppSettings": {
"PulsarUri": "pulsar://localhost:6650",
"PulsarTopic": "persistent://public/default/captesttopic"
}
}

+ 1
- 1
samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 1
- 10
samples/Sample.RabbitMQ.MySql/AppDbContext.cs View File

@@ -13,17 +13,8 @@ namespace Sample.RabbitMQ.MySql
return $"Name:{Name}, Id:{Id}";
}
}
public class Person2
{
public int Id { get; set; }

public string Name { get; set; }

public override string ToString()
{
return $"Name:{Name}, Id:{Id}";
}
}
public class AppDbContext : DbContext
{
public const string ConnectionString = "";
@@ -32,7 +23,7 @@ namespace Sample.RabbitMQ.MySql

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql(ConnectionString, ServerVersion.FromString("mysql"));
optionsBuilder.UseMySql(ConnectionString, new MariaDbServerVersion(ServerVersion.AutoDetect(ConnectionString)));
}
}
}

+ 6
- 2
samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj View File

@@ -1,12 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0-alpha.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />


+ 35
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Controllers/HomeController.cs View File

@@ -0,0 +1,35 @@
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;
using Sample.RabbitMQ.SqlServer.DispatcherPerGroup.Messages;
using System;
using System.Threading.Tasks;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.Controllers
{
public class HomeController : Controller
{
private readonly ICapPublisher _capPublisher;

public HomeController(ICapPublisher capPublisher)
{
_capPublisher = capPublisher;
}

public async Task<IActionResult> Index()
{
await using (var connection = new SqlConnection("Server=(local);Database=CAP-Test;Trusted_Connection=True;"))
{
using var transaction = connection.BeginTransaction(_capPublisher);
// This is where you would do other work that is going to persist data to your database

var message = TestMessage.Create($"This is message text created at {DateTime.Now:O}.");

await _capPublisher.PublishAsync(typeof(TestMessage).FullName, message);
transaction.Commit();
}

return Content("ok");
}
}
}

+ 12
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/TestMessage.cs View File

@@ -0,0 +1,12 @@
namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.Messages
{
public class TestMessage
{
public static TestMessage Create(string text) => new()
{
Text = text
};

public string Text { get; private init; }
}
}

+ 25
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/VeryFastProcessingReceiver.cs View File

@@ -0,0 +1,25 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.Messages
{
[QueueHandlerTopic("fasttopic")]
public class VeryFastProcessingReceiver : QueueHandler
{
private readonly ILogger<VeryFastProcessingReceiver> _logger;

public VeryFastProcessingReceiver(ILogger<VeryFastProcessingReceiver> logger)
{
_logger = logger;
}

public async Task Handle(TestMessage value)
{
_logger.LogInformation($"Starting FAST processing handler {DateTime.Now:O}: {value.Text}");
await Task.Delay(50);
_logger.LogInformation($"Ending FAST processing handler {DateTime.Now:O}: {value.Text}");
}
}
}

+ 25
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Messages/XSlowProcessingReceiver.cs View File

@@ -0,0 +1,25 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.Messages
{
[QueueHandlerTopic("slowtopic")]
public class XSlowProcessingReceiver : QueueHandler
{
private readonly ILogger<XSlowProcessingReceiver> _logger;

public XSlowProcessingReceiver(ILogger<XSlowProcessingReceiver> logger)
{
_logger = logger;
}

public async Task Handle(TestMessage value)
{
_logger.LogInformation($"Starting SLOW processing handler {DateTime.Now:O}: {value.Text}");
await Task.Delay(10000);
_logger.LogInformation($"Ending SLOW processing handler {DateTime.Now:O}: {value.Text}");
}
}
}

+ 58
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Program.cs View File

@@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using System;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup
{
public class Program
{
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Debug()
#if DEBUG
.WriteTo.Seq("http://localhost:5341")
#endif
.CreateLogger();

try
{
Log.Information("Starting host...");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex.InnerException ?? ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, builder) =>
{
builder
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", true);
})
.UseSerilog((context, configuration) =>
{
configuration.ReadFrom.Configuration(context.Configuration);
}, true, true)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

+ 26
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Sample.RabbitMQ.SqlServer.DispatcherPerGroup.csproj View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.4" />

<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="5.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.RabbitMQ\DotNetCore.CAP.RabbitMQ.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>

</Project>

+ 52
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/Startup.cs View File

@@ -0,0 +1,52 @@
using DotNetCore.CAP;
using DotNetCore.CAP.Internal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers;
using Serilog;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(x => x.AddSerilog());

services
.AddSingleton<IConsumerServiceSelector, TypedConsumerServiceSelector>()
.AddQueueHandlers(typeof(Startup).Assembly);

services.AddCap(options =>
{
options.UseSqlServer("Server=(local);Database=CAP-Test;Trusted_Connection=True;");
options.UseRabbitMQ("localhost");
options.UseDashboard();
options.GroupNamePrefix = "th";
options.ConsumerThreadCount = 1;

options.UseDispatchingPerGroup = true;
});

services.AddControllersWithViews();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseSerilogRequestLogging();
app.UseCapDashboard();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

+ 4
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandler.cs View File

@@ -0,0 +1,4 @@
namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers
{
public abstract class QueueHandler { }
}

+ 15
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandlerTopicAttribute.cs View File

@@ -0,0 +1,15 @@
using System;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers
{
[AttributeUsage(AttributeTargets.Class)]
public class QueueHandlerTopicAttribute : Attribute
{
public string Topic { get; }

public QueueHandlerTopicAttribute(string topic)
{
Topic = topic;
}
}
}

+ 31
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/QueueHandlersExtensions.cs View File

@@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Reflection;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers
{
internal static class QueueHandlersExtensions
{
private static readonly Type queueHandlerType = typeof(QueueHandler);

public static IServiceCollection AddQueueHandlers(this IServiceCollection services, params Assembly[] assemblies)
{
assemblies ??= new[] { Assembly.GetEntryAssembly() };

foreach (var type in assemblies.Distinct().SelectMany(x => x.GetTypes().Where(FilterHandlers)))
{
services.AddTransient(queueHandlerType, type);
}

return services;
}

private static bool FilterHandlers(Type t)
{
var topic = t.GetCustomAttribute<QueueHandlerTopicAttribute>();

return queueHandlerType.IsAssignableFrom(t) && topic != null && t.IsClass && !t.IsAbstract;
}
}
}

+ 87
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/TypedConsumers/TypedConsumerServiceSelector.cs View File

@@ -0,0 +1,87 @@
using DotNetCore.CAP;
using DotNetCore.CAP.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Sample.RabbitMQ.SqlServer.DispatcherPerGroup.TypedConsumers
{
internal class TypedConsumerServiceSelector : ConsumerServiceSelector
{
private readonly CapOptions _capOptions;

public TypedConsumerServiceSelector(IServiceProvider serviceProvider) : base(serviceProvider)
{
_capOptions = serviceProvider.GetRequiredService<IOptions<CapOptions>>().Value;
}

protected override IEnumerable<ConsumerExecutorDescriptor> FindConsumersFromInterfaceTypes(IServiceProvider provider)
{
var executorDescriptorList = new List<ConsumerExecutorDescriptor>(30);

using var scoped = provider.CreateScope();
var consumerServices = scoped.ServiceProvider.GetServices<QueueHandler>();
foreach (var service in consumerServices)
{
var typeInfo = service.GetType().GetTypeInfo();
if (!typeof(QueueHandler).GetTypeInfo().IsAssignableFrom(typeInfo))
{
continue;
}

executorDescriptorList.AddRange(GetMyDescription(typeInfo));
}

return executorDescriptorList;
}

private IEnumerable<ConsumerExecutorDescriptor> GetMyDescription(TypeInfo typeInfo)
{
var method = typeInfo.DeclaredMethods.FirstOrDefault(x => x.Name == "Handle");
if (method == null) yield break;

var topicAttr = typeInfo.GetCustomAttributes<QueueHandlerTopicAttribute>(true);
var topicAttributes = topicAttr as IList<QueueHandlerTopicAttribute> ?? topicAttr.ToList();

if (topicAttributes.Count == 0) yield break;

foreach (var attr in topicAttributes)
{
var topic = attr.Topic == null
? _capOptions.DefaultGroupName + "." + _capOptions.Version
: attr.Topic + "." + _capOptions.Version;

if (!string.IsNullOrEmpty(_capOptions.GroupNamePrefix))
{
topic = $"{_capOptions.GroupNamePrefix}.{topic}";
}

var parameters = method.GetParameters().Select(p => new ParameterDescriptor
{
Name = p.Name,
ParameterType = p.ParameterType,
IsFromCap = p.GetCustomAttributes(typeof(FromCapAttribute)).Any()
}).ToList();

var capName = parameters.FirstOrDefault(x => !x.IsFromCap)?.ParameterType.FullName;
if (string.IsNullOrEmpty(capName)) continue;

yield return new ConsumerExecutorDescriptor
{
Attribute = new CapSubscribeAttribute(capName)
{
Group = topic
},
Parameters = parameters,
MethodInfo = method,
ImplTypeInfo = typeInfo,
TopicNamePrefix = _capOptions.TopicNamePrefix,
ServiceTypeInfo = typeInfo
};
}
}
}
}

+ 10
- 0
samples/Sample.RabbitMQ.SqlServer.DispatcherPerGroup/appsettings.json View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

+ 1
- 1
samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 13
- 5
samples/Samples.Redis.SqlServer/Controllers/HomeController.cs View File

@@ -1,6 +1,8 @@
using DotNetCore.CAP;
using DotNetCore.CAP.Messages;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Threading.Tasks;

namespace Samples.Redis.SqlServer.Controllers
@@ -11,25 +13,31 @@ namespace Samples.Redis.SqlServer.Controllers
{
private readonly ILogger<HomeController> _logger;
private readonly ICapPublisher _publisher;
private readonly IOptions<CapOptions> _options;

public HomeController(ILogger<HomeController> logger, ICapPublisher publisher)
public HomeController(ILogger<HomeController> logger, ICapPublisher publisher, IOptions<CapOptions> options)
{
_logger = logger;
_publisher = publisher;
this._options = options;
}

[HttpGet]
public async Task Publish()
public async Task Publish([FromQuery] string message = "test-message")
{
await _publisher.PublishAsync("test-message", new Person() { Age = 11, Name = "James" });
await _publisher.PublishAsync(message, new Person() { Age = 11, Name = "James" });
}

[CapSubscribe("test-message")]
[CapSubscribe("test-message-1")]
[CapSubscribe("test-message-2")]
[CapSubscribe("test-message-3")]
[NonAction]
public void Subscribe(Person p)
public void Subscribe(Person p, [FromCap] CapHeader header)
{
_logger.LogInformation($"test-message subscribed with value --> " + p);
_logger.LogInformation($"{header[Headers.MessageName]} subscribed with value --> " + p);
}

}

public class Person


+ 25
- 0
samples/Samples.Redis.SqlServer/Dockerfile View File

@@ -0,0 +1,25 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["samples/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj", "samples/Samples.Redis.SqlServer/"]
COPY ["src/DotNetCore.CAP.RedisStreams/DotNetCore.CAP.RedisStreams.csproj", "src/DotNetCore.CAP.RedisStreams/"]
COPY ["src/DotNetCore.CAP/DotNetCore.CAP.csproj", "src/DotNetCore.CAP/"]
COPY ["src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj", "src/DotNetCore.CAP.SqlServer/"]
RUN dotnet restore "samples/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj"
COPY . .
WORKDIR "/src/samples/Samples.Redis.SqlServer"
RUN dotnet build "Samples.Redis.SqlServer.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Samples.Redis.SqlServer.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Samples.Redis.SqlServer.dll"]

+ 2
- 3
samples/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj View File

@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.RedisStreams\DotNetCore.CAP.RedisStreams.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj" />


+ 3
- 3
samples/Samples.Redis.SqlServer/Startup.cs View File

@@ -16,12 +16,12 @@ namespace Samples.Redis.SqlServer
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddCap(options =>
{
options.UseRedis("");
options.UseRedis("redis-node-0:6379,password=cap");

options.UseSqlServer("");
options.UseSqlServer("Server=db;Database=master;User=sa;Password=P@ssw0rd;");
});
}



+ 90
- 0
samples/Samples.Redis.SqlServer/docker-compose.yml View File

@@ -0,0 +1,90 @@
version: '2'
services:
redis-node-0:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-0:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=cap'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'

redis-node-1:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-1:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=cap'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'

redis-node-2:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-2:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=cap'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'

redis-node-3:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-3:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=cap'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'

redis-node-4:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-4:/bitnami/redis/data
environment:
- 'REDIS_PASSWORD=cap'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'

redis-node-5:
image: docker.io/bitnami/redis-cluster:6.2
volumes:
- redis-cluster_data-5:/bitnami/redis/data
depends_on:
- redis-node-0
- redis-node-1
- redis-node-2
- redis-node-3
- redis-node-4
environment:
- 'REDIS_PASSWORD=cap'
- 'REDISCLI_AUTH=cap'
- 'REDIS_CLUSTER_REPLICAS=1'
- 'REDIS_NODES=redis-node-0 redis-node-1 redis-node-2 redis-node-3 redis-node-4 redis-node-5'
- 'REDIS_CLUSTER_CREATOR=yes'

db:
image: "mcr.microsoft.com/mssql/server"
ports:
- 1433:1433
environment:
SA_PASSWORD: "P@ssw0rd"
ACCEPT_EULA: "Y"

redis-sample:
build:
context: ../..
dockerfile: samples/Samples.Redis.SqlServer/Dockerfile
ports:
- 5000:80
depends_on:
- db
- redis-node-5
volumes:
redis-cluster_data-0:
driver: local
redis-cluster_data-1:
driver: local
redis-cluster_data-2:
driver: local
redis-cluster_data-3:
driver: local
redis-cluster_data-4:
driver: local
redis-cluster_data-5:
driver: local

+ 3
- 3
src/Directory.Build.props View File

@@ -4,7 +4,7 @@

<PropertyGroup Label="Package">
<Product>CAP</Product>
<LangVersion>8</LangVersion>
<LangVersion>10</LangVersion>
<Authors>ncc;savorboard</Authors>
<RepositoryUrl>https://github.com/dotnetcore/CAP</RepositoryUrl>
<RepositoryType>git</RepositoryType>
@@ -25,8 +25,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
</ItemGroup>

</Project>

+ 85
- 12
src/DotNetCore.CAP.AmazonSQS/AmazonPolicyExtensions.cs View File

@@ -102,10 +102,10 @@ namespace DotNetCore.CAP.AmazonSQS
/// "AWS": "*"
/// },
/// "Action": "sqs:SendMessage",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue-v1",
/// "Condition": {
/// "ArnLike": {
/// "aws:SourceArn": "arn:aws:sns:us-east-1:FirstTopic"
/// "aws:SourceArn": "arn:aws:sns:us-east-1:MyQueue-FirstTopic"
/// }
/// }
/// },
@@ -115,13 +115,26 @@ namespace DotNetCore.CAP.AmazonSQS
/// "AWS": "*"
/// },
/// "Action": "sqs:SendMessage",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue-v1",
/// "Condition": {
/// "ArnLike": {
/// "aws:SourceArn": "arn:aws:sns:us-east-1:SecondTopic"
/// "aws:SourceArn": "arn:aws:sns:us-east-1:MyQueue-SecondTopic"
/// }
/// }
/// }]
/// },
/// {
/// "Effect": "Allow",
/// "Principal": {
/// "AWS": "*"
/// },
/// "Action": "sqs:SendMessage",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue-v1",
/// "Condition": {
/// "ArnLike": {
/// "aws:SourceArn": "arn:aws:sns:us-east-1:MyQueue2-FirstTopic"
/// }
/// }
/// },]
/// }
/// </code>
/// into compacted single statement:
@@ -135,13 +148,13 @@ namespace DotNetCore.CAP.AmazonSQS
/// "AWS": "*"
/// },
/// "Action": "sqs:SendMessage",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue-v1",
/// "Condition": {
/// "ArnLike": {
/// "aws:SourceArn": [
/// "arn:aws:sns:us-east-1:FirstTopic",
/// "arn:aws:sns:us-east-1:SecondTopic"
/// ]
/// "arn:aws:sns:us-east-1:MyQueue-*",
/// "arn:aws:sns:us-east-1:MyQueue2-FirstTopic"
/// ]
/// }
/// }
/// }]
@@ -161,7 +174,12 @@ namespace DotNetCore.CAP.AmazonSQS
.Where(s => s.Principals.All(r => string.Equals(r.Id, "*", StringComparison.OrdinalIgnoreCase)))
.ToList();

if (statementsToCompact.Count < 2)
var groupName = GetGroupName(sqsQueueArn);
if (groupName != null)
{
groupName = $":{groupName}-";
}
if (statementsToCompact.Count < 2 && groupName == null)
{
return;
}
@@ -172,11 +190,66 @@ namespace DotNetCore.CAP.AmazonSQS
policy.Statements.Remove(statement);
foreach (var topicArn in statement.Conditions.SelectMany(c => c.Values))
{
topicArns.Add(topicArn);
topicArns.Add(
groupName != null && topicArn.Contains(groupName, StringComparison.InvariantCultureIgnoreCase)
? $"{GetArnGroupPrefix(topicArn)}-*"
: topicArn);
}
}

policy.AddSqsPermissions(topicArns, sqsQueueArn);
policy.AddSqsPermissions(topicArns.OrderBy(a => a), sqsQueueArn);
}

/// <summary>
/// Extract group prefix from ARN
/// For example for ARN:
/// arn:aws:sns:us-east-1:MyQueue-FirstTopic
/// group prefix will be extracted:
/// arn:aws:sns:us-east-1:MyQueue
/// </summary>
/// <param name="arn">Source ARN</param>
/// <returns>Group prefix or null if group not present</returns>
private static string GetArnGroupPrefix(string arn)
{
const char separator = '-';
if (string.IsNullOrEmpty(arn) || !arn.Contains(separator))
{
return null;
}

var groupPaths = arn.Split(separator);
if (groupPaths.Length < 2)
{
return null;
}

return string.Join(separator, groupPaths.Take(groupPaths.Length - 1));
}
/// <summary>
/// Extract group name from ARN
/// For example for ARN:
/// arn:aws:sns:us-east-1:MyQueue-FirstTopic
/// group name will be extracted:
/// MyQueue
/// </summary>
/// <param name="arn">Source ARN</param>
/// <returns>Group name or null if group not present</returns>
private static string GetGroupName(string arn)
{
const char separator = ':';
if (string.IsNullOrEmpty(arn) || !arn.Contains(separator))
{
return null;
}

var name = arn.Split(separator).LastOrDefault();
if(string.IsNullOrEmpty(name))
{
return null;
}

return GetArnGroupPrefix(name);
}
}
}

+ 46
- 17
src/DotNetCore.CAP.AmazonSQS/AmazonSQSConsumerClient.cs View File

@@ -77,8 +77,7 @@ namespace DotNetCore.CAP.AmazonSQS

Connect();

_snsClient.SubscribeQueueToTopicsAsync(topics.ToList(), _sqsClient, _queueUrl)
.GetAwaiter().GetResult();
SubscribeToTopics(topics).GetAwaiter().GetResult();
}

public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
@@ -120,7 +119,7 @@ namespace DotNetCore.CAP.AmazonSQS
{
try
{
_sqsClient.DeleteMessageAsync(_queueUrl, (string)sender);
_ = _sqsClient.DeleteMessageAsync(_queueUrl, (string)sender).GetAwaiter().GetResult();
}
catch (InvalidIdFormatException ex)
{
@@ -133,7 +132,7 @@ namespace DotNetCore.CAP.AmazonSQS
try
{
// Visible again in 3 seconds
_sqsClient.ChangeMessageVisibilityAsync(_queueUrl, (string)sender, 3);
_ = _sqsClient.ChangeMessageVisibilityAsync(_queueUrl, (string)sender, 3).GetAwaiter().GetResult();
}
catch (MessageNotInflightException ex)
{
@@ -160,9 +159,18 @@ namespace DotNetCore.CAP.AmazonSQS

try
{
_snsClient = _amazonSQSOptions.Credentials != null
? new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region)
: new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Region);
if (string.IsNullOrWhiteSpace(_amazonSQSOptions.SNSServiceUrl))
{
_snsClient = _amazonSQSOptions.Credentials != null
? new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region)
: new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Region);
}
else
{
_snsClient = _amazonSQSOptions.Credentials != null
? new AmazonSimpleNotificationServiceClient(_amazonSQSOptions.Credentials, new AmazonSimpleNotificationServiceConfig() { ServiceURL = _amazonSQSOptions.SNSServiceUrl })
: new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig() { ServiceURL = _amazonSQSOptions.SNSServiceUrl });
}
}
finally
{
@@ -176,10 +184,18 @@ namespace DotNetCore.CAP.AmazonSQS

try
{

_sqsClient = _amazonSQSOptions.Credentials != null
? new AmazonSQSClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region)
: new AmazonSQSClient(_amazonSQSOptions.Region);
if (string.IsNullOrWhiteSpace(_amazonSQSOptions.SQSServiceUrl))
{
_sqsClient = _amazonSQSOptions.Credentials != null
? new AmazonSQSClient(_amazonSQSOptions.Credentials, _amazonSQSOptions.Region)
: new AmazonSQSClient(_amazonSQSOptions.Region);
}
else
{
_sqsClient = _amazonSQSOptions.Credentials != null
? new AmazonSQSClient(_amazonSQSOptions.Credentials, new AmazonSQSConfig() { ServiceURL = _amazonSQSOptions.SQSServiceUrl })
: new AmazonSQSClient(new AmazonSQSConfig() { ServiceURL = _amazonSQSOptions.SQSServiceUrl });
}

// If provide the name of an existing queue along with the exact names and values
// of all the queue's attributes, <code>CreateQueue</code> returns the queue URL for
@@ -195,7 +211,7 @@ namespace DotNetCore.CAP.AmazonSQS

#region private methods

private Task InvalidIdFormatLog(string exceptionMessage)
private void InvalidIdFormatLog(string exceptionMessage)
{
var logArgs = new LogMessageEventArgs
{
@@ -204,11 +220,9 @@ namespace DotNetCore.CAP.AmazonSQS
};

OnLog?.Invoke(null, logArgs);

return Task.CompletedTask;
}

private Task MessageNotInflightLog(string exceptionMessage)
private void MessageNotInflightLog(string exceptionMessage)
{
var logArgs = new LogMessageEventArgs
{
@@ -217,8 +231,6 @@ namespace DotNetCore.CAP.AmazonSQS
};

OnLog?.Invoke(null, logArgs);

return Task.CompletedTask;
}

private async Task GenerateSqsAccessPolicyAsync(IEnumerable<string> topicArns)
@@ -248,6 +260,23 @@ namespace DotNetCore.CAP.AmazonSQS
var setAttributes = new Dictionary<string, string> { { "Policy", policy.ToJson() } };
await _sqsClient.SetAttributesAsync(_queueUrl, setAttributes).ConfigureAwait(false);
}
private async Task SubscribeToTopics(IEnumerable<string> topics)
{
var queueAttributes = await _sqsClient.GetAttributesAsync(_queueUrl).ConfigureAwait(false);

var sqsQueueArn = queueAttributes["QueueArn"];
foreach (var topicArn in topics)
{
await _snsClient.SubscribeAsync(new SubscribeRequest
{
TopicArn = topicArn,
Protocol = "sqs",
Endpoint = sqsQueueArn,
})
.ConfigureAwait(false);
}
}

#endregion
}

+ 11
- 0
src/DotNetCore.CAP.AmazonSQS/CAP.AmazonSQSOptions.cs View File

@@ -13,5 +13,16 @@ namespace DotNetCore.CAP
public RegionEndpoint Region { get; set; }

public AWSCredentials Credentials { get; set; }

/// <summary>
/// Overrides Service Url deduced from AWS Region. To use in local development environments like localstack.
/// </summary>
public string SNSServiceUrl { get; set; }

/// <summary>
/// Overrides Service Url deduced from AWS Region. To use in local development environments like localstack.
/// </summary>
public string SQSServiceUrl { get; set; }

}
}

+ 2
- 2
src/DotNetCore.CAP.AmazonSQS/DotNetCore.CAP.AmazonSQS.csproj View File

@@ -12,8 +12,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="3.5.1.50" />
<PackageReference Include="AWSSDK.SQS" Version="3.5.1.27" />
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="3.7.2.64" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.1.36" />
</ItemGroup>

<ItemGroup>


+ 37
- 11
src/DotNetCore.CAP.AmazonSQS/ITransport.AmazonSQS.cs View File

@@ -31,15 +31,15 @@ namespace DotNetCore.CAP.AmazonSQS
_sqsOptions = sqsOptions;
}

public BrokerAddress BrokerAddress => new BrokerAddress("RabbitMQ", string.Empty);
public BrokerAddress BrokerAddress => new BrokerAddress("AmazonSQS", string.Empty);

public async Task<OperateResult> SendAsync(TransportMessage message)
{
try
{
await TryAddTopicArns();
await FetchExistingTopicArns();

if (_topicArnMaps.TryGetValue(message.GetName().NormalizeForAws(), out var arn))
if (TryGetOrCreateTopicArn(message.GetName().NormalizeForAws(), out var arn))
{
string bodyJson = null;
if (message.Body != null)
@@ -89,20 +89,29 @@ namespace DotNetCore.CAP.AmazonSQS
}
}

public async Task<bool> TryAddTopicArns()
private async Task FetchExistingTopicArns()
{
if (_topicArnMaps != null)
{
return true;
return;
}

await _semaphore.WaitAsync();

try
{
_snsClient = _sqsOptions.Value.Credentials != null
? new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Credentials, _sqsOptions.Value.Region)
: new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Region);
if (string.IsNullOrWhiteSpace(_sqsOptions.Value.SNSServiceUrl))
{
_snsClient = _sqsOptions.Value.Credentials != null
? new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Credentials, _sqsOptions.Value.Region)
: new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Region);
}
else
{
_snsClient = _sqsOptions.Value.Credentials != null
? new AmazonSimpleNotificationServiceClient(_sqsOptions.Value.Credentials, new AmazonSimpleNotificationServiceConfig() { ServiceURL = _sqsOptions.Value.SNSServiceUrl })
: new AmazonSimpleNotificationServiceClient(new AmazonSimpleNotificationServiceConfig() { ServiceURL = _sqsOptions.Value.SNSServiceUrl });
}

if (_topicArnMaps == null)
{
@@ -122,8 +131,6 @@ namespace DotNetCore.CAP.AmazonSQS
nextToken = topics.NextToken;
}
while (!string.IsNullOrEmpty(nextToken));

return true;
}
}
catch (Exception e)
@@ -134,8 +141,27 @@ namespace DotNetCore.CAP.AmazonSQS
{
_semaphore.Release();
}
}
private bool TryGetOrCreateTopicArn(string topicName, out string topicArn)
{
topicArn = null;
if (_topicArnMaps.TryGetValue(topicName, out topicArn))
{
return true;
}

return false;
var response = _snsClient.CreateTopicAsync(topicName).GetAwaiter().GetResult();

if (string.IsNullOrEmpty(response.TopicArn))
{
return false;
}
topicArn = response.TopicArn;
_topicArnMaps.Add(topicName, topicArn);
return true;
}
}
}

+ 1
- 1
src/DotNetCore.CAP.AzureServiceBus/DotNetCore.CAP.AzureServiceBus.csproj View File

@@ -13,7 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="5.1.2" />
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="5.2.0" />
</ItemGroup>

<ItemGroup>


+ 5
- 6
src/DotNetCore.CAP.Dashboard/DotNetCore.CAP.Dashboard.csproj View File

@@ -1,24 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>default</LangVersion>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Consul" Version="1.6.10.3" />
<SupportedPlatform Include="browser" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="wwwroot/dist/**/*" Exclude="**/*/*.map" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Consul" Version="1.6.1.1" />
<Compile Include="..\DotNetCore.CAP\Internal\ObjectMethodExecutor\*.cs" Link="ObjectMethodExecutor\%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>
</ItemGroup>

</Project>

+ 0
- 128
src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/AwaitableInfo.cs View File

@@ -1,128 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace Microsoft.Extensions.Internal
{
internal struct AwaitableInfo
{
public Type AwaiterType { get; }
public PropertyInfo AwaiterIsCompletedProperty { get; }
public MethodInfo AwaiterGetResultMethod { get; }
public MethodInfo AwaiterOnCompletedMethod { get; }
public MethodInfo AwaiterUnsafeOnCompletedMethod { get; }
public Type ResultType { get; }
public MethodInfo GetAwaiterMethod { get; }

public AwaitableInfo(
Type awaiterType,
PropertyInfo awaiterIsCompletedProperty,
MethodInfo awaiterGetResultMethod,
MethodInfo awaiterOnCompletedMethod,
MethodInfo awaiterUnsafeOnCompletedMethod,
Type resultType,
MethodInfo getAwaiterMethod)
{
AwaiterType = awaiterType;
AwaiterIsCompletedProperty = awaiterIsCompletedProperty;
AwaiterGetResultMethod = awaiterGetResultMethod;
AwaiterOnCompletedMethod = awaiterOnCompletedMethod;
AwaiterUnsafeOnCompletedMethod = awaiterUnsafeOnCompletedMethod;
ResultType = resultType;
GetAwaiterMethod = getAwaiterMethod;
}

public static bool IsTypeAwaitable(Type type, out AwaitableInfo awaitableInfo)
{
// Based on Roslyn code: http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/ISymbolExtensions.cs,db4d48ba694b9347

// Awaitable must have method matching "object GetAwaiter()"
var getAwaiterMethod = type.GetRuntimeMethods().FirstOrDefault(m =>
m.Name.Equals("GetAwaiter", StringComparison.OrdinalIgnoreCase)
&& m.GetParameters().Length == 0
&& m.ReturnType != null);
if (getAwaiterMethod == null)
{
awaitableInfo = default(AwaitableInfo);
return false;
}

var awaiterType = getAwaiterMethod.ReturnType;

// Awaiter must have property matching "bool IsCompleted { get; }"
var isCompletedProperty = awaiterType.GetRuntimeProperties().FirstOrDefault(p =>
p.Name.Equals("IsCompleted", StringComparison.OrdinalIgnoreCase)
&& p.PropertyType == typeof(bool)
&& p.GetMethod != null);
if (isCompletedProperty == null)
{
awaitableInfo = default(AwaitableInfo);
return false;
}

// Awaiter must implement INotifyCompletion
var awaiterInterfaces = awaiterType.GetInterfaces();
var implementsINotifyCompletion = awaiterInterfaces.Any(t => t == typeof(INotifyCompletion));
if (!implementsINotifyCompletion)
{
awaitableInfo = default(AwaitableInfo);
return false;
}

// INotifyCompletion supplies a method matching "void OnCompleted(Action action)"
var iNotifyCompletionMap = awaiterType
.GetTypeInfo()
.GetRuntimeInterfaceMap(typeof(INotifyCompletion));
var onCompletedMethod = iNotifyCompletionMap.InterfaceMethods.Single(m =>
m.Name.Equals("OnCompleted", StringComparison.OrdinalIgnoreCase)
&& m.ReturnType == typeof(void)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(Action));

// Awaiter optionally implements ICriticalNotifyCompletion
var implementsICriticalNotifyCompletion =
awaiterInterfaces.Any(t => t == typeof(ICriticalNotifyCompletion));
MethodInfo unsafeOnCompletedMethod;
if (implementsICriticalNotifyCompletion)
{
// ICriticalNotifyCompletion supplies a method matching "void UnsafeOnCompleted(Action action)"
var iCriticalNotifyCompletionMap = awaiterType
.GetTypeInfo()
.GetRuntimeInterfaceMap(typeof(ICriticalNotifyCompletion));
unsafeOnCompletedMethod = iCriticalNotifyCompletionMap.InterfaceMethods.Single(m =>
m.Name.Equals("UnsafeOnCompleted", StringComparison.OrdinalIgnoreCase)
&& m.ReturnType == typeof(void)
&& m.GetParameters().Length == 1
&& m.GetParameters()[0].ParameterType == typeof(Action));
}
else
{
unsafeOnCompletedMethod = null;
}

// Awaiter must have method matching "void GetResult" or "T GetResult()"
var getResultMethod = awaiterType.GetRuntimeMethods().FirstOrDefault(m =>
m.Name.Equals("GetResult")
&& m.GetParameters().Length == 0);
if (getResultMethod == null)
{
awaitableInfo = default(AwaitableInfo);
return false;
}

awaitableInfo = new AwaitableInfo(
awaiterType,
isCompletedProperty,
getResultMethod,
onCompletedMethod,
unsafeOnCompletedMethod,
getResultMethod.ReturnType,
getAwaiterMethod);
return true;
}
}
}

+ 0
- 56
src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/CoercedAwaitableInfo.cs View File

@@ -1,56 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;

namespace Microsoft.Extensions.Internal
{
internal struct CoercedAwaitableInfo
{
public AwaitableInfo AwaitableInfo { get; }
public Expression CoercerExpression { get; }
public Type CoercerResultType { get; }
public bool RequiresCoercion => CoercerExpression != null;

public CoercedAwaitableInfo(AwaitableInfo awaitableInfo)
{
AwaitableInfo = awaitableInfo;
CoercerExpression = null;
CoercerResultType = null;
}

public CoercedAwaitableInfo(Expression coercerExpression, Type coercerResultType,
AwaitableInfo coercedAwaitableInfo)
{
CoercerExpression = coercerExpression;
CoercerResultType = coercerResultType;
AwaitableInfo = coercedAwaitableInfo;
}

public static bool IsTypeAwaitable(Type type, out CoercedAwaitableInfo info)
{
if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo))
{
info = new CoercedAwaitableInfo(directlyAwaitableInfo);
return true;
}

// It's not directly awaitable, but maybe we can coerce it.
// Currently we support coercing FSharpAsync<T>.
if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type,
out var coercerExpression,
out var coercerResultType))
{
if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo))
{
info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo);
return true;
}
}

info = default(CoercedAwaitableInfo);
return false;
}
}
}

+ 0
- 338
src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutor.cs View File

@@ -1,338 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.Internal
{
internal class ObjectMethodExecutor
{
// ReSharper disable once InconsistentNaming
private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor =
typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[]
{
typeof(object), // customAwaitable
typeof(Func<object, object>), // getAwaiterMethod
typeof(Func<object, bool>), // isCompletedMethod
typeof(Func<object, object>), // getResultMethod
typeof(Action<object, Action>), // onCompletedMethod
typeof(Action<object, Action>) // unsafeOnCompletedMethod
});

private readonly MethodExecutor _executor;
private readonly MethodExecutorAsync _executorAsync;
private readonly object[] _parameterDefaultValues;

private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues)
{
if (methodInfo == null)
{
throw new ArgumentNullException(nameof(methodInfo));
}

MethodInfo = methodInfo;
MethodParameters = methodInfo.GetParameters();
TargetTypeInfo = targetTypeInfo;
MethodReturnType = methodInfo.ReturnType;

var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo);

IsMethodAsync = isAwaitable;
AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null;

// Upstream code may prefer to use the sync-executor even for async methods, because if it knows
// that the result is a specific Task<T> where T is known, then it can directly cast to that type
// and await it without the extra heap allocations involved in the _executorAsync code path.
_executor = GetExecutor(methodInfo, targetTypeInfo);

if (IsMethodAsync)
{
_executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo);
}

_parameterDefaultValues = parameterDefaultValues;
}

public MethodInfo MethodInfo { get; }

public ParameterInfo[] MethodParameters { get; }

public TypeInfo TargetTypeInfo { get; }

public Type AsyncResultType { get; }

// This field is made internal set because it is set in unit tests.
public Type MethodReturnType { get; internal set; }

public bool IsMethodAsync { get; }

public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo)
{
return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null);
}

public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo,
object[] parameterDefaultValues)
{
if (parameterDefaultValues == null)
{
throw new ArgumentNullException(nameof(parameterDefaultValues));
}

return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues);
}

/// <summary>
/// Executes the configured method on <paramref name="target" />. This can be used whether or not
/// the configured method is asynchronous.
/// </summary>
/// <remarks>
/// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than
/// ExecuteAsync if you know at compile time what the return type is, because then you can directly
/// "await" that value (via a cast), and then the generated code will be able to reference the
/// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated
/// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at
/// compile time what type it would be.
/// </remarks>
/// <param name="target">The object whose method is to be executed.</param>
/// <param name="parameters">Parameters to pass to the method.</param>
/// <returns>The method return value.</returns>
public object Execute(object target, params object[] parameters)
{
return _executor(target, parameters);
}

/// <summary>
/// Executes the configured method on <paramref name="target" />. This can only be used if the configured
/// method is asynchronous.
/// </summary>
/// <remarks>
/// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync,
/// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations
/// as compared with using Execute and then using "await" on the result value typecasted to the known
/// awaitable type. The possible extra heap allocations are for:
/// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally
/// it's a reference type, and you normally create a new instance per call).
/// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance
/// of it, and if it is, it will have to be boxed so the calling code can reference it as an object).
/// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling
/// code doesn't know what type it's going to be).
/// </remarks>
/// <param name="target">The object whose method is to be executed.</param>
/// <param name="parameters">Parameters to pass to the method.</param>
/// <returns>An object that you can "await" to get the method return value.</returns>
public ObjectMethodExecutorAwaitable ExecuteAsync(object target, params object[] parameters)
{
return _executorAsync(target, parameters);
}

public object GetDefaultValueForParameter(int index)
{
if (_parameterDefaultValues == null)
{
throw new InvalidOperationException(
$"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied.");
}

if (index < 0 || index > MethodParameters.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(index));
}

return _parameterDefaultValues[index];
}

private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo)
{
// Parameters to executor
var targetParameter = Expression.Parameter(typeof(object), "target");
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

// Build parameter list
var parameters = new List<Expression>();
var paramInfos = methodInfo.GetParameters();
for (var i = 0; i < paramInfos.Length; i++)
{
var paramInfo = paramInfos[i];
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

// valueCast is "(Ti) parameters[i]"
parameters.Add(valueCast);
}

// Call method
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

// methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)"
// Create function
if (methodCall.Type == typeof(void))
{
var lambda = Expression.Lambda<VoidMethodExecutor>(methodCall, targetParameter, parametersParameter);
var voidExecutor = lambda.Compile();
return WrapVoidMethod(voidExecutor);
}
else
{
// must coerce methodCall to match ActionExecutor signature
var castMethodCall = Expression.Convert(methodCall, typeof(object));
var lambda = Expression.Lambda<MethodExecutor>(castMethodCall, targetParameter, parametersParameter);
return lambda.Compile();
}
}

private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor)
{
return delegate(object target, object[] parameters)
{
executor(target, parameters);
return null;
};
}

private static MethodExecutorAsync GetExecutorAsync(
MethodInfo methodInfo,
TypeInfo targetTypeInfo,
CoercedAwaitableInfo coercedAwaitableInfo)
{
// Parameters to executor
var targetParameter = Expression.Parameter(typeof(object), "target");
var parametersParameter = Expression.Parameter(typeof(object[]), "parameters");

// Build parameter list
var parameters = new List<Expression>();
var paramInfos = methodInfo.GetParameters();
for (var i = 0; i < paramInfos.Length; i++)
{
var paramInfo = paramInfos[i];
var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i));
var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType);

// valueCast is "(Ti) parameters[i]"
parameters.Add(valueCast);
}

// Call method
var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType());
var methodCall = Expression.Call(instanceCast, methodInfo, parameters);

// Using the method return value, construct an ObjectMethodExecutorAwaitable based on
// the info we have about its implementation of the awaitable pattern. Note that all
// the funcs/actions we construct here are precompiled, so that only one instance of
// each is preserved throughout the lifetime of the ObjectMethodExecutor.

// var getAwaiterFunc = (object awaitable) =>
// (object)((CustomAwaitableType)awaitable).GetAwaiter();
var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable");
var awaitableInfo = coercedAwaitableInfo.AwaitableInfo;
var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType;
var getAwaiterFunc = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.Call(
Expression.Convert(customAwaitableParam, postCoercionMethodReturnType),
awaitableInfo.GetAwaiterMethod),
typeof(object)),
customAwaitableParam).Compile();

// var isCompletedFunc = (object awaiter) =>
// ((CustomAwaiterType)awaiter).IsCompleted;
var isCompletedParam = Expression.Parameter(typeof(object), "awaiter");
var isCompletedFunc = Expression.Lambda<Func<object, bool>>(
Expression.MakeMemberAccess(
Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType),
awaitableInfo.AwaiterIsCompletedProperty),
isCompletedParam).Compile();

var getResultParam = Expression.Parameter(typeof(object), "awaiter");
Func<object, object> getResultFunc;
if (awaitableInfo.ResultType == typeof(void))
{
getResultFunc = Expression.Lambda<Func<object, object>>(
Expression.Block(
Expression.Call(
Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
awaitableInfo.AwaiterGetResultMethod),
Expression.Constant(null)
),
getResultParam).Compile();
}
else
{
getResultFunc = Expression.Lambda<Func<object, object>>(
Expression.Convert(
Expression.Call(
Expression.Convert(getResultParam, awaitableInfo.AwaiterType),
awaitableInfo.AwaiterGetResultMethod),
typeof(object)),
getResultParam).Compile();
}

// var onCompletedFunc = (object awaiter, Action continuation) => {
// ((CustomAwaiterType)awaiter).OnCompleted(continuation);
// };
var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
var onCompletedFunc = Expression.Lambda<Action<object, Action>>(
Expression.Call(
Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType),
awaitableInfo.AwaiterOnCompletedMethod,
onCompletedParam2),
onCompletedParam1,
onCompletedParam2).Compile();

Action<object, Action> unsafeOnCompletedFunc = null;
if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null)
{
// var unsafeOnCompletedFunc = (object awaiter, Action continuation) => {
// ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation);
// };
var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter");
var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation");
unsafeOnCompletedFunc = Expression.Lambda<Action<object, Action>>(
Expression.Call(
Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType),
awaitableInfo.AwaiterUnsafeOnCompletedMethod,
unsafeOnCompletedParam2),
unsafeOnCompletedParam1,
unsafeOnCompletedParam2).Compile();
}

// If we need to pass the method call result through a coercer function to get an
// awaitable, then do so.
var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion
? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall)
: (Expression) methodCall;

// return new ObjectMethodExecutorAwaitable(
// (object)coercedMethodCall,
// getAwaiterFunc,
// isCompletedFunc,
// getResultFunc,
// onCompletedFunc,
// unsafeOnCompletedFunc);
var returnValueExpression = Expression.New(
_objectMethodExecutorAwaitableConstructor,
Expression.Convert(coercedMethodCall, typeof(object)),
Expression.Constant(getAwaiterFunc),
Expression.Constant(isCompletedFunc),
Expression.Constant(getResultFunc),
Expression.Constant(onCompletedFunc),
Expression.Constant(unsafeOnCompletedFunc, typeof(Action<object, Action>)));

var lambda =
Expression.Lambda<MethodExecutorAsync>(returnValueExpression, targetParameter, parametersParameter);
return lambda.Compile();
}

private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, params object[] parameters);

private delegate object MethodExecutor(object target, params object[] parameters);

private delegate void VoidMethodExecutor(object target, object[] parameters);
}
}

+ 0
- 118
src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs View File

@@ -1,118 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Runtime.CompilerServices;

namespace Microsoft.Extensions.Internal
{
/// <summary>
/// Provides a common awaitable structure that <see cref="ObjectMethodExecutor.ExecuteAsync" /> can
/// return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an
/// application-defined custom awaitable.
/// </summary>
internal struct ObjectMethodExecutorAwaitable
{
private readonly object _customAwaitable;
private readonly Func<object, object> _getAwaiterMethod;
private readonly Func<object, bool> _isCompletedMethod;
private readonly Func<object, object> _getResultMethod;
private readonly Action<object, Action> _onCompletedMethod;
private readonly Action<object, Action> _unsafeOnCompletedMethod;

// Perf note: since we're requiring the customAwaitable to be supplied here as an object,
// this will trigger a further allocation if it was a value type (i.e., to box it). We can't
// fix this by making the customAwaitable type generic, because the calling code typically
// does not know the type of the awaitable/awaiter at compile-time anyway.
//
// However, we could fix it by not passing the customAwaitable here at all, and instead
// passing a func that maps directly from the target object (e.g., controller instance),
// target method (e.g., action method info), and params array to the custom awaiter in the
// GetAwaiter() method below. In effect, by delaying the actual method call until the
// upstream code calls GetAwaiter on this ObjectMethodExecutorAwaitable instance.
// This optimization is not currently implemented because:
// [1] It would make no difference when the awaitable was an object type, which is
// by far the most common scenario (e.g., System.Task<T>).
// [2] It would be complex - we'd need some kind of object pool to track all the parameter
// arrays until we needed to use them in GetAwaiter().
// We can reconsider this in the future if there's a need to optimize for ValueTask<T>
// or other value-typed awaitables.

public ObjectMethodExecutorAwaitable(
object customAwaitable,
Func<object, object> getAwaiterMethod,
Func<object, bool> isCompletedMethod,
Func<object, object> getResultMethod,
Action<object, Action> onCompletedMethod,
Action<object, Action> unsafeOnCompletedMethod)
{
_customAwaitable = customAwaitable;
_getAwaiterMethod = getAwaiterMethod;
_isCompletedMethod = isCompletedMethod;
_getResultMethod = getResultMethod;
_onCompletedMethod = onCompletedMethod;
_unsafeOnCompletedMethod = unsafeOnCompletedMethod;
}

public Awaiter GetAwaiter()
{
var customAwaiter = _getAwaiterMethod(_customAwaitable);
return new Awaiter(customAwaiter, _isCompletedMethod, _getResultMethod, _onCompletedMethod,
_unsafeOnCompletedMethod);
}

public struct Awaiter : ICriticalNotifyCompletion
{
private readonly object _customAwaiter;
private readonly Func<object, bool> _isCompletedMethod;
private readonly Func<object, object> _getResultMethod;
private readonly Action<object, Action> _onCompletedMethod;
private readonly Action<object, Action> _unsafeOnCompletedMethod;

public Awaiter(
object customAwaiter,
Func<object, bool> isCompletedMethod,
Func<object, object> getResultMethod,
Action<object, Action> onCompletedMethod,
Action<object, Action> unsafeOnCompletedMethod)
{
_customAwaiter = customAwaiter;
_isCompletedMethod = isCompletedMethod;
_getResultMethod = getResultMethod;
_onCompletedMethod = onCompletedMethod;
_unsafeOnCompletedMethod = unsafeOnCompletedMethod;
}

public bool IsCompleted => _isCompletedMethod(_customAwaiter);

public object GetResult()
{
return _getResultMethod(_customAwaiter);
}

public void OnCompleted(Action continuation)
{
_onCompletedMethod(_customAwaiter, continuation);
}

public void UnsafeOnCompleted(Action continuation)
{
// If the underlying awaitable implements ICriticalNotifyCompletion, use its UnsafeOnCompleted.
// If not, fall back on using its OnCompleted.
//
// Why this is safe:
// - Implementing ICriticalNotifyCompletion is a way of saying the caller can choose whether it
// needs the execution context to be preserved (which it signals by calling OnCompleted), or
// that it doesn't (which it signals by calling UnsafeOnCompleted). Obviously it's faster *not*
// to preserve and restore the context, so we prefer that where possible.
// - If a caller doesn't need the execution context to be preserved and hence calls UnsafeOnCompleted,
// there's no harm in preserving it anyway - it's just a bit of wasted cost. That's what will happen
// if a caller sees that the proxy implements ICriticalNotifyCompletion but the proxy chooses to
// pass the call on to the underlying awaitable's OnCompleted method.

var underlyingMethodToUse = _unsafeOnCompletedMethod ?? _onCompletedMethod;
underlyingMethodToUse(_customAwaiter, continuation);
}
}
}
}

+ 0
- 145
src/DotNetCore.CAP.Dashboard/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs View File

@@ -1,145 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Internal
{
/// <summary>
/// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying
/// an <see cref="Expression" /> for mapping instances of that type to a C# awaitable.
/// </summary>
/// <remarks>
/// The main design goal here is to avoid taking a compile-time dependency on
/// FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references
/// to FSharp types have to be constructed dynamically at runtime.
/// </remarks>
internal static class ObjectMethodExecutorFSharpSupport
{
private static readonly object _fsharpValuesCacheLock = new object();
private static Assembly _fsharpCoreAssembly;
private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod;
private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty;
private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty;

public static bool TryBuildCoercerFromFSharpAsyncToAwaitable(
Type possibleFSharpAsyncType,
out Expression coerceToAwaitableExpression,
out Type awaitableType)
{
var methodReturnGenericType = possibleFSharpAsyncType.IsGenericType
? possibleFSharpAsyncType.GetGenericTypeDefinition()
: null;

if (!IsFSharpAsyncOpenGenericType(methodReturnGenericType))
{
coerceToAwaitableExpression = null;
awaitableType = null;
return false;
}

var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single();
awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType);

// coerceToAwaitableExpression = (object fsharpAsync) =>
// {
// return (object)FSharpAsync.StartAsTask<TResult>(
// (Microsoft.FSharp.Control.FSharpAsync<TResult>)fsharpAsync,
// FSharpOption<TaskCreationOptions>.None,
// FSharpOption<CancellationToken>.None);
// };
var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod
.MakeGenericMethod(awaiterResultType);
var coerceToAwaitableParam = Expression.Parameter(typeof(object));
coerceToAwaitableExpression = Expression.Lambda(
Expression.Convert(
Expression.Call(
startAsTaskClosedMethod,
Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType),
Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty),
Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)),
typeof(object)),
coerceToAwaitableParam);

return true;
}

private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType)
{
var typeFullName = possibleFSharpAsyncGenericType?.FullName;
if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal))
{
return false;
}

lock (_fsharpValuesCacheLock)
{
if (_fsharpCoreAssembly != null)
{
return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly;
}

return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType);
}
}

private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType)
{
var assembly = possibleFSharpAsyncGenericType.Assembly;
var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1");
var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync");

if (fsharpOptionType == null || fsharpAsyncType == null)
{
return false;
}

// Get a reference to FSharpOption<TaskCreationOptions>.None
var fsharpOptionOfTaskCreationOptionsType = fsharpOptionType
.MakeGenericType(typeof(TaskCreationOptions));
_fsharpOptionOfTaskCreationOptionsNoneProperty = fsharpOptionOfTaskCreationOptionsType
.GetTypeInfo()
.GetRuntimeProperty("None");

// Get a reference to FSharpOption<CancellationToken>.None
var fsharpOptionOfCancellationTokenType = fsharpOptionType
.MakeGenericType(typeof(CancellationToken));
_fsharpOptionOfCancellationTokenNoneProperty = fsharpOptionOfCancellationTokenType
.GetTypeInfo()
.GetRuntimeProperty("None");

// Get a reference to FSharpAsync.StartAsTask<>
var fsharpAsyncMethods = fsharpAsyncType
.GetRuntimeMethods()
.Where(m => m.Name.Equals("StartAsTask", StringComparison.Ordinal));
foreach (var candidateMethodInfo in fsharpAsyncMethods)
{
var parameters = candidateMethodInfo.GetParameters();
if (parameters.Length == 3
&& TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType)
&& parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType
&& parameters[2].ParameterType == fsharpOptionOfCancellationTokenType)
{
// This really does look like the correct method (and hence assembly).
_fsharpAsyncStartAsTaskGenericMethod = candidateMethodInfo;
_fsharpCoreAssembly = assembly;
break;
}
}

return _fsharpCoreAssembly != null;
}

private static bool TypesHaveSameIdentity(Type type1, Type type2)
{
return type1.Assembly == type2.Assembly
&& string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal)
&& string.Equals(type1.Name, type2.Name, StringComparison.Ordinal);
}
}
}

+ 30
- 23
src/DotNetCore.CAP.Dashboard/UiMiddleware.cs View File

@@ -21,12 +21,17 @@ namespace DotNetCore.CAP.Dashboard

private readonly DashboardOptions _options;
private readonly StaticFileMiddleware _staticFileMiddleware;
private readonly Regex _redirectUrlCheckRegex;
private readonly Regex _homeUrlCheckRegex;

public UiMiddleware(RequestDelegate next, IWebHostEnvironment hostingEnv, ILoggerFactory loggerFactory, DashboardOptions options)
{
_options = options ?? new DashboardOptions();

_staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options);

_redirectUrlCheckRegex = new Regex($"^/?{Regex.Escape(_options.PathMatch)}/?$", RegexOptions.IgnoreCase);
_homeUrlCheckRegex = new Regex($"^/?{Regex.Escape(_options.PathMatch)}/?index.html$", RegexOptions.IgnoreCase);
}

public async Task Invoke(HttpContext httpContext)
@@ -34,37 +39,39 @@ namespace DotNetCore.CAP.Dashboard
var httpMethod = httpContext.Request.Method;
var path = httpContext.Request.Path.Value;


if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.PathMatch)}/?$", RegexOptions.IgnoreCase))
{
var redirectUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") ? "index.html" : $"{path.Split('/').Last()}/index.html";

httpContext.Response.StatusCode = 301;
httpContext.Response.Headers["Location"] = redirectUrl;
return;
}

if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.PathMatch)}/?index.html$", RegexOptions.IgnoreCase))
if (httpMethod == "GET")
{
if (!await CapBuilderExtension.Authentication(httpContext, _options))
if (_redirectUrlCheckRegex.IsMatch(path))
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
var redirectUrl = string.IsNullOrEmpty(path) || path.EndsWith("/") ? "index.html" : $"{path.Split('/').Last()}/index.html";

httpContext.Response.StatusCode = 301;
httpContext.Response.Headers["Location"] = redirectUrl;
return;
}

httpContext.Response.StatusCode = 200;
httpContext.Response.ContentType = "text/html;charset=utf-8";
if (_homeUrlCheckRegex.IsMatch(path))
{
if (!await CapBuilderExtension.Authentication(httpContext, _options))
{
httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}

httpContext.Response.StatusCode = 200;
httpContext.Response.ContentType = "text/html;charset=utf-8";

await using var stream = GetType().Assembly.GetManifestResourceStream(EmbeddedFileNamespace + ".index.html");
if (stream == null) throw new InvalidOperationException();
await using var stream = GetType().Assembly.GetManifestResourceStream(EmbeddedFileNamespace + ".index.html");
if (stream == null) throw new InvalidOperationException();

using var sr = new StreamReader(stream);
var htmlBuilder = new StringBuilder(await sr.ReadToEndAsync());
htmlBuilder.Replace("%(servicePrefix)", _options.PathBase + _options.PathMatch + "/api");
htmlBuilder.Replace("%(pollingInterval)", _options.StatsPollingInterval.ToString());
await httpContext.Response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
using var sr = new StreamReader(stream);
var htmlBuilder = new StringBuilder(await sr.ReadToEndAsync());
htmlBuilder.Replace("%(servicePrefix)", _options.PathBase + _options.PathMatch + "/api");
htmlBuilder.Replace("%(pollingInterval)", _options.StatsPollingInterval.ToString());
await httpContext.Response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);

return;
return;
}
}

await _staticFileMiddleware.Invoke(httpContext);


+ 1
- 0
src/DotNetCore.CAP.Dashboard/wwwroot/package.json View File

@@ -13,6 +13,7 @@
"bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5",
"echarts": "^5.1.1",
"json-bigint": "^1.0.0",
"vue": "^2.6.11",
"vue-echarts": "^6.0.0-rc.5",
"vue-json-pretty": "^1.8.0",


+ 1
- 3
src/DotNetCore.CAP.Dashboard/wwwroot/public/index.html View File

@@ -6,9 +6,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<title>CAP Dashboard</title>
</head>

<body>


+ 3
- 2
src/DotNetCore.CAP.Dashboard/wwwroot/src/pages/Published.vue View File

@@ -64,12 +64,13 @@
</b-col>
</b-row>
<b-modal size="lg" :id="infoModal.id" :title="'Id: ' + infoModal.title" ok-only>
<vue-json-pretty showSelectController :key="infoModal.id" :data="JSON.parse(infoModal.content.trim())" />
<vue-json-pretty showSelectController :key="infoModal.id" :data="infoModal.content" />
</b-modal>
</div>
</template>
<script>
import axios from "axios";
import JSONBIG from "json-bigint";

const formDataTpl = {
currentPage: 1,
@@ -196,7 +197,7 @@ export default {
},
info(item, button) {
this.infoModal.title = item.id.toString();
this.infoModal.content = item.content;
this.infoModal.content = JSONBIG({ storeAsString: true }).parse(item.content.trim());
this.$root.$emit("bv::show::modal", this.infoModal.id, button);
},
pageSizeChange: function (size) {


+ 3
- 2
src/DotNetCore.CAP.Dashboard/wwwroot/src/pages/Received.vue View File

@@ -71,12 +71,13 @@
</b-col>
</b-row>
<b-modal size="lg" :id="infoModal.id" :title="'Id: ' + infoModal.title" ok-only>
<vue-json-pretty showSelectController :key="infoModal.id" :data="JSON.parse(infoModal.content.trim())" />
<vue-json-pretty showSelectController :key="infoModal.id" :data="infoModal.content" />
</b-modal>
</div>
</template>
<script>
import axios from "axios";
import JSONBIG from "json-bigint";

const formDataTpl = {
currentPage: 1,
@@ -204,7 +205,7 @@ export default {
},
info(item, button) {
this.infoModal.title = item.id.toString();
this.infoModal.content = item.content;
this.infoModal.content = JSONBIG({ storeAsString: true }).parse(item.content.trim());
this.$root.$emit("bv::show::modal", this.infoModal.id, button);
},
pageSizeChange: function (size) {


+ 88
- 61
src/DotNetCore.CAP.InMemoryStorage/IDataStorage.InMemory.cs View File

@@ -58,16 +58,19 @@ namespace DotNetCore.CAP.InMemoryStorage
Retries = 0
};

PublishedMessages[message.DbId] = new MemoryMessage()
lock (PublishedMessages)
{
DbId = message.DbId,
Name = name,
Content = message.Content,
Retries = message.Retries,
Added = message.Added,
ExpiresAt = message.ExpiresAt,
StatusName = StatusName.Scheduled
};
PublishedMessages[message.DbId] = new MemoryMessage
{
DbId = message.DbId,
Name = name,
Content = message.Content,
Retries = message.Retries,
Added = message.Added,
ExpiresAt = message.ExpiresAt,
StatusName = StatusName.Scheduled
};
}

return message;
}
@@ -76,18 +79,21 @@ namespace DotNetCore.CAP.InMemoryStorage
{
var id = SnowflakeId.Default().NextId().ToString();

ReceivedMessages[id] = new MemoryMessage
lock (ReceivedMessages)
{
DbId = id,
Group = group,
Origin = null,
Name = name,
Content = content,
Retries = _capOptions.Value.FailedRetryCount,
Added = DateTime.Now,
ExpiresAt = DateTime.Now.AddDays(15),
StatusName = StatusName.Failed
};
ReceivedMessages[id] = new MemoryMessage
{
DbId = id,
Group = group,
Origin = null,
Name = name,
Content = content,
Retries = _capOptions.Value.FailedRetryCount,
Added = DateTime.Now,
ExpiresAt = DateTime.Now.AddDays(15),
StatusName = StatusName.Failed
};
}
}

public MediumMessage StoreReceivedMessage(string name, string @group, Message message)
@@ -101,90 +107,111 @@ namespace DotNetCore.CAP.InMemoryStorage
Retries = 0
};

ReceivedMessages[mdMessage.DbId] = new MemoryMessage
lock (ReceivedMessages)
{
DbId = mdMessage.DbId,
Origin = mdMessage.Origin,
Group = group,
Name = name,
Content = _serializer.Serialize(mdMessage.Origin),
Retries = mdMessage.Retries,
Added = mdMessage.Added,
ExpiresAt = mdMessage.ExpiresAt,
StatusName = StatusName.Scheduled
};
ReceivedMessages[mdMessage.DbId] = new MemoryMessage
{
DbId = mdMessage.DbId,
Origin = mdMessage.Origin,
Group = group,
Name = name,
Content = _serializer.Serialize(mdMessage.Origin),
Retries = mdMessage.Retries,
Added = mdMessage.Added,
ExpiresAt = mdMessage.ExpiresAt,
StatusName = StatusName.Scheduled
};
}
return mdMessage;
}

public Task<int> DeleteExpiresAsync(string table, DateTime timeout, int batchCount = 1000, CancellationToken token = default)
{

var removed = 0;
if (table == nameof(PublishedMessages))
{
var ids = PublishedMessages.Values
.Where(x => x.ExpiresAt < timeout)
.Select(x => x.DbId)
.Take(batchCount);
foreach (var id in ids)
lock (PublishedMessages)
{
if (PublishedMessages.Remove(id))
var ids = PublishedMessages.Values
.Where(x => x.ExpiresAt < timeout)
.Select(x => x.DbId)
.Take(batchCount);

foreach (var id in ids)
{
removed++;
if (PublishedMessages.Remove(id))
{
removed++;
}
}
}

}
else
{
var ids = ReceivedMessages.Values
.Where(x => x.ExpiresAt < timeout)
.Select(x => x.DbId)
.Take(batchCount);

foreach (var id in ids)
lock (ReceivedMessages)
{
if (ReceivedMessages.Remove(id))
var ids = ReceivedMessages.Values
.Where(x => x.ExpiresAt < timeout)
.Select(x => x.DbId)
.Take(batchCount);

foreach (var id in ids)
{
removed++;
if (ReceivedMessages.Remove(id))
{
removed++;
}
}
}
}
}

return Task.FromResult(removed);
}

public Task<IEnumerable<MediumMessage>> GetPublishedMessagesOfNeedRetry()
{
var ret = PublishedMessages.Values
IEnumerable<MediumMessage> result;

lock (PublishedMessages)
{
result = PublishedMessages.Values
.Where(x => x.Retries < _capOptions.Value.FailedRetryCount
&& x.Added < DateTime.Now.AddSeconds(-10)
&& (x.StatusName == StatusName.Scheduled || x.StatusName == StatusName.Failed))
.Take(200)
.Select(x => (MediumMessage)x);
.Select(x => (MediumMessage)x).ToList();
}

foreach (var message in ret)
foreach (var message in result)
{
message.Origin = _serializer.Deserialize(message.Content);
}

return Task.FromResult(ret);
return Task.FromResult(result);
}

public Task<IEnumerable<MediumMessage>> GetReceivedMessagesOfNeedRetry()
{
var ret = ReceivedMessages.Values
.Where(x => x.Retries < _capOptions.Value.FailedRetryCount
&& x.Added < DateTime.Now.AddSeconds(-10)
&& (x.StatusName == StatusName.Scheduled || x.StatusName == StatusName.Failed))
.Take(200)
.Select(x => (MediumMessage)x);

foreach (var message in ret)
IEnumerable<MediumMessage> result;

lock (ReceivedMessages)
{
result = ReceivedMessages.Values
.Where(x => x.Retries < _capOptions.Value.FailedRetryCount
&& x.Added < DateTime.Now.AddSeconds(-10)
&& (x.StatusName == StatusName.Scheduled || x.StatusName == StatusName.Failed))
.Take(200)
.Select(x => (MediumMessage)x).ToList();
}

foreach (var message in result)
{
message.Origin = _serializer.Deserialize(message.Content);
}

return Task.FromResult(ret);
return Task.FromResult(result);
}

public IMonitoringApi GetMonitoringApi()


+ 1
- 1
src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj View File

@@ -13,7 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="1.6.2" />
<PackageReference Include="Confluent.Kafka" Version="1.8.2" />
</ItemGroup>

<ItemGroup>


+ 8
- 1
src/DotNetCore.CAP.Kafka/IConnectionPool.Default.cs View File

@@ -46,11 +46,16 @@ namespace DotNetCore.CAP.Kafka
RequestTimeoutMs = 3000
};

producer = new ProducerBuilder<string, byte[]>(config).Build();
producer = BuildProducer(config);

return producer;
}

protected virtual IProducer<string, byte[]> BuildProducer(ProducerConfig config)
{
return new ProducerBuilder<string, byte[]>(config).Build();
}

public bool Return(IProducer<string, byte[]> producer)
{
if (Interlocked.Increment(ref _pCount) <= _maxSize)
@@ -60,6 +65,8 @@ namespace DotNetCore.CAP.Kafka
return true;
}

producer.Dispose();

Interlocked.Decrement(ref _pCount);

return false;


+ 1
- 5
src/DotNetCore.CAP.Kafka/ITransport.Kafka.cs View File

@@ -64,11 +64,7 @@ namespace DotNetCore.CAP.Kafka
}
finally
{
var returned = _connectionPool.Return(producer);
if (!returned)
{
producer.Dispose();
}
_connectionPool.Return(producer);
}
}
}

+ 9
- 4
src/DotNetCore.CAP.Kafka/KafkaConsumerClient.cs View File

@@ -15,7 +15,7 @@ using Microsoft.Extensions.Options;

namespace DotNetCore.CAP.Kafka
{
internal sealed class KafkaConsumerClient : IConsumerClient
public class KafkaConsumerClient : IConsumerClient
{
private static readonly SemaphoreSlim ConnectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);

@@ -153,9 +153,7 @@ namespace DotNetCore.CAP.Kafka
config.EnableAutoCommit ??= false;
config.LogConnectionClose ??= false;

_consumerClient = new ConsumerBuilder<string, byte[]>(config)
.SetErrorHandler(ConsumerClient_OnConsumeError)
.Build();
_consumerClient = BuildConsumer(config);
}
}
finally
@@ -164,6 +162,13 @@ namespace DotNetCore.CAP.Kafka
}
}

protected virtual IConsumer<string, byte[]> BuildConsumer(ConsumerConfig config)
{
return new ConsumerBuilder<string, byte[]>(config)
.SetErrorHandler(ConsumerClient_OnConsumeError)
.Build();
}

private void ConsumerClient_OnConsumeError(IConsumer<string, byte[]> consumer, Error e)
{
var logArgs = new LogMessageEventArgs


+ 2
- 2
src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs View File

@@ -6,7 +6,7 @@ using Microsoft.Extensions.Options;

namespace DotNetCore.CAP.Kafka
{
internal sealed class KafkaConsumerClientFactory : IConsumerClientFactory
public class KafkaConsumerClientFactory : IConsumerClientFactory
{
private readonly IOptions<KafkaOptions> _kafkaOptions;

@@ -15,7 +15,7 @@ namespace DotNetCore.CAP.Kafka
_kafkaOptions = kafkaOptions;
}

public IConsumerClient Create(string groupId)
public virtual IConsumerClient Create(string groupId)
{
try
{


+ 2
- 2
src/DotNetCore.CAP.MongoDB/DotNetCore.CAP.MongoDB.csproj View File

@@ -16,8 +16,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="2.12.0" />
<PackageReference Include="MongoDB.Driver" Version="2.12.0" />
<PackageReference Include="MongoDB.Bson" Version="2.13.2" />
<PackageReference Include="MongoDB.Driver" Version="2.13.2" />
</ItemGroup>

</Project>

+ 24
- 18
src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj View File

@@ -1,24 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DotNetCore.CAP.MySql</AssemblyName>
<PackageTags>$(PackageTags);MySQL</PackageTags>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
<AssemblyName>DotNetCore.CAP.MySql</AssemblyName>
<PackageTags>$(PackageTags);MySQL</PackageTags>
</PropertyGroup>

<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.MySql.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.MySql.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.4" />
<PackageReference Include="MySqlConnector" Version="1.3.11" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="MySqlConnector" Version="2.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.12" />
<PackageReference Include="MySqlConnector" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>

</Project>

+ 7
- 1
src/DotNetCore.CAP.NATS/CAP.NATSOptions.cs View File

@@ -1,7 +1,9 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using NATS.Client;
using NATS.Client.JetStream;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
@@ -15,7 +17,7 @@ namespace DotNetCore.CAP
/// Gets or sets the server url/urls used to connect to the NATs server.
/// </summary>
/// <remarks>This may contain username/password information.</remarks>
public string Servers { get; set; }
public string Servers { get; set; } = "nats://localhost:4222";

/// <summary>
/// connection pool size, default is 10
@@ -26,5 +28,9 @@ namespace DotNetCore.CAP
/// Used to setup all NATs client options
/// </summary>
public Options Options { get; set; }

public Action<StreamConfiguration.StreamConfigurationBuilder> StreamOptions { get; set; }

public Func<string, string> NormalizeStreamName { get; set; } = origin => origin.Split('.')[0];
}
}

+ 7
- 2
src/DotNetCore.CAP.NATS/CAP.Options.Extensions.cs View File

@@ -3,6 +3,7 @@

using System;
using DotNetCore.CAP;
using JetBrains.Annotations;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
@@ -14,9 +15,13 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary>
/// <param name="options">CAP configuration options</param>
/// <param name="bootstrapServers">NATS bootstrap server urls.</param>
public static CapOptions UseNATS(this CapOptions options, string bootstrapServers)
public static CapOptions UseNATS(this CapOptions options, [CanBeNull] string bootstrapServers = null)
{
return options.UseNATS(opt => { opt.Servers = bootstrapServers; });
return options.UseNATS(opt =>
{
if (bootstrapServers != null)
opt.Servers = bootstrapServers;
});
}

/// <summary>


+ 1
- 1
src/DotNetCore.CAP.NATS/DotNetCore.CAP.NATS.csproj View File

@@ -13,7 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NATS.Client" Version="0.12.0" />
<PackageReference Include="NATS.Client" Version="0.14.1" />
</ItemGroup>

<ItemGroup>


+ 4
- 2
src/DotNetCore.CAP.NATS/IConnectionPool.Default.cs View File

@@ -17,7 +17,7 @@ namespace DotNetCore.CAP.NATS
private readonly ConnectionFactory _connectionFactory;
private int _pCount;
private int _maxSize;
public ConnectionPool(ILogger<ConnectionPool> logger, IOptions<NATSOptions> options)
{
_options = options.Value;
@@ -51,7 +51,7 @@ namespace DotNetCore.CAP.NATS
{
connection = _connectionFactory.CreateConnection(_options.Servers);
}
return connection;
}

@@ -64,6 +64,8 @@ namespace DotNetCore.CAP.NATS
return true;
}

connection.Dispose();

Interlocked.Decrement(ref _pCount);

return false;


+ 22
- 18
src/DotNetCore.CAP.NATS/ITransport.NATS.cs View File

@@ -2,13 +2,13 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Messages;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Logging;
using NATS.Client;
using NATS.Client.JetStream;

namespace DotNetCore.CAP.NATS
{
@@ -16,50 +16,54 @@ namespace DotNetCore.CAP.NATS
{
private readonly IConnectionPool _connectionPool;
private readonly ILogger _logger;
private readonly JetStreamOptions _jetStreamOptions;

public NATSTransport(ILogger<NATSTransport> logger, IConnectionPool connectionPool)
{
_logger = logger;
_connectionPool = connectionPool;

_jetStreamOptions = JetStreamOptions.Builder().WithPublishNoAck(false).WithRequestTimeout(3000).Build();
}

public BrokerAddress BrokerAddress => new BrokerAddress("NATS", _connectionPool.ServersAddress);

public Task<OperateResult> SendAsync(TransportMessage message)
public async Task<OperateResult> SendAsync(TransportMessage message)
{
var connection = _connectionPool.RentConnection();

try
{
var binFormatter = new BinaryFormatter();
using var mStream = new MemoryStream();
binFormatter.Serialize(mStream, message);
var msg = new Msg(message.GetName(), message.Body);
foreach (var header in message.Headers)
{
msg.Header[header.Key] = header.Value;
}

var js = connection.CreateJetStreamContext(_jetStreamOptions);

//connection.Publish(message.GetName(), mStream.ToArray());
//return Task.FromResult(OperateResult.Success);
var builder = PublishOptions.Builder().WithMessageId(message.GetId());

var reply = connection.Request(message.GetName(), mStream.ToArray(), 2000);
if (reply.Data != null && reply.Data[0] == 1)
var resp = await js.PublishAsync(msg, builder.Build());

if (resp.Seq > 0)
{
_logger.LogDebug($"NATS subject message [{message.GetName()}] has been consumed.");
_logger.LogDebug($"NATS stream message [{message.GetName()}] has been published.");

return Task.FromResult(OperateResult.Success);
return OperateResult.Success;
}
throw new PublisherSentFailedException("NATS message send failed, no consumer reply!");
}
catch (Exception ex)
{
var warpEx = new PublisherSentFailedException(ex.Message, ex);

return Task.FromResult(OperateResult.Failed(warpEx));
return OperateResult.Failed(warpEx);
}
finally
{
var returned = _connectionPool.Return(connection);
if (!returned)
{
connection.Dispose();
}
_connectionPool.Return(connection);
}
}
}

+ 65
- 28
src/DotNetCore.CAP.NATS/NATSConsumerClient.cs View File

@@ -3,13 +3,14 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Linq;
using System.Threading;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Messages;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Options;
using NATS.Client;
using NATS.Client.JetStream;

namespace DotNetCore.CAP.NATS
{
@@ -19,14 +20,12 @@ namespace DotNetCore.CAP.NATS

private readonly string _groupId;
private readonly NATSOptions _natsOptions;
private readonly IList<IAsyncSubscription> _asyncSubscriptions;

private IConnection _consumerClient;

public NATSConsumerClient(string groupId, IOptions<NATSOptions> options)
{
_groupId = groupId;
_asyncSubscriptions = new List<IAsyncSubscription>();
_natsOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
}

@@ -36,6 +35,42 @@ namespace DotNetCore.CAP.NATS

public BrokerAddress BrokerAddress => new BrokerAddress("NATS", _natsOptions.Servers);

public ICollection<string> FetchTopics(IEnumerable<string> topicNames)
{
var jsm = _consumerClient.CreateJetStreamManagementContext();

var streamGroup = topicNames.GroupBy(x => _natsOptions.NormalizeStreamName(x));

foreach (var subjectStream in streamGroup)
{
try
{
jsm.GetStreamInfo(subjectStream.Key); // this throws if the stream does not exist
}
catch (NATSJetStreamException)
{
var builder = StreamConfiguration.Builder()
.WithName(subjectStream.Key)
.WithNoAck(false)
.WithStorageType(StorageType.Memory)
.WithSubjects(subjectStream.ToList());

_natsOptions.StreamOptions?.Invoke(builder);

try
{
jsm.AddStream(builder.Build());
}
catch
{
// ignored
}
}
}

return topicNames.ToList();
}

public void Subscribe(IEnumerable<string> topics)
{
if (topics == null)
@@ -45,22 +80,22 @@ namespace DotNetCore.CAP.NATS

Connect();

foreach (var topic in topics)
var js = _consumerClient.CreateJetStreamContext();

foreach (var subject in topics)
{
_asyncSubscriptions.Add(_consumerClient.SubscribeAsync(topic, _groupId));
var streamName = _natsOptions.NormalizeStreamName(subject);
var pso = PushSubscribeOptions.Builder()
.WithStream(streamName)
.WithConfiguration(ConsumerConfiguration.Builder().WithDeliverPolicy(DeliverPolicy.New).Build())
.Build();

js.PushSubscribeAsync(subject, Helper.Normalized(_groupId), Subscription_MessageHandler, false, pso);
}
}

public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
{
Connect();

foreach (var subscription in _asyncSubscriptions)
{
subscription.MessageHandler += Subscription_MessageHandler;
subscription.Start();
}

while (true)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -71,30 +106,31 @@ namespace DotNetCore.CAP.NATS

private void Subscription_MessageHandler(object sender, MsgHandlerEventArgs e)
{
using var mStream = new MemoryStream();
var binFormatter = new BinaryFormatter();
var headers = new Dictionary<string, string>();

foreach (string h in e.Message.Header.Keys)
{
headers.Add(h, e.Message.Header[h]);
}

mStream.Write(e.Message.Data, 0, e.Message.Data.Length);
mStream.Position = 0;
headers.Add(Headers.Group, _groupId);

var message = (TransportMessage)binFormatter.Deserialize(mStream);
message.Headers.Add(Headers.Group, _groupId);
OnMessageReceived?.Invoke(e.Message.Reply, message);
OnMessageReceived?.Invoke(e.Message, new TransportMessage(headers, e.Message.Data));
}

public void Commit(object sender)
{
if (sender is string reply)
if (sender is Msg msg)
{
_consumerClient.Publish(reply, new byte[] { 1 });
msg.Ack();
}
}

public void Reject(object sender)
{
if (sender is string reply)
if (sender is Msg msg)
{
_consumerClient.Publish(reply, new byte[] { 0 });
msg.Nak();
}
}

@@ -121,6 +157,7 @@ namespace DotNetCore.CAP.NATS
opts.ClosedEventHandler = ConnectedEventHandler;
opts.DisconnectedEventHandler = ConnectedEventHandler;
opts.AsyncErrorEventHandler = AsyncErrorEventHandler;
opts.Timeout = 5000;
_consumerClient = new ConnectionFactory().CreateConnection(opts);
}
}
@@ -134,8 +171,8 @@ namespace DotNetCore.CAP.NATS
{
var logArgs = new LogMessageEventArgs
{
LogType = MqLogType.ServerConnError,
Reason = $"An error occurred during connect NATS --> {e.Error}"
LogType = MqLogType.ConnectError,
Reason = e.Error?.ToString()
};
OnLog?.Invoke(null, logArgs);
}
@@ -145,7 +182,7 @@ namespace DotNetCore.CAP.NATS
var logArgs = new LogMessageEventArgs
{
LogType = MqLogType.AsyncErrorEvent,
Reason = $"An error occurred out of band --> {e.Error}"
Reason = e.Error
};
OnLog?.Invoke(null, logArgs);
}


+ 26
- 20
src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj View File

@@ -1,24 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DotNetCore.CAP.PostgreSql</AssemblyName>
<PackageTags>$(PackageTags);PostgreSQL</PackageTags>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.PostgreSql.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.4" />
<PackageReference Include="Npgsql" Version="5.0.3" />
</ItemGroup>
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
<AssemblyName>DotNetCore.CAP.PostgreSql</AssemblyName>
<PackageTags>$(PackageTags);PostgreSQL</PackageTags>
</PropertyGroup>

<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.PostgreSql.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="Npgsql" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.12" />
<PackageReference Include="Npgsql" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>
</Project>

+ 40
- 0
src/DotNetCore.CAP.Pulsar/CAP.Options.Extensions.cs View File

@@ -0,0 +1,40 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using DotNetCore.CAP;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class CapOptionsExtensions
{
/// <summary>
/// Configuration to use pulsar in CAP.
/// </summary>
/// <param name="options">CAP configuration options</param>
/// <param name="serverUrl">Pulsar bootstrap server urls.</param>
public static CapOptions UsePulsar(this CapOptions options, string serverUrl)
{
return options.UsePulsar(opt => { opt.ServiceUrl = serverUrl; });
}

/// <summary>
/// Configuration to use pulsar in CAP.
/// </summary>
/// <param name="options">CAP configuration options</param>
/// <param name="configure">Provides programmatic configuration for the pulsar .</param>
/// <returns></returns>
public static CapOptions UsePulsar(this CapOptions options, Action<PulsarOptions> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}

options.RegisterExtension(new PulsarCapOptionsExtension(configure));

return options;
}
}
}

+ 32
- 0
src/DotNetCore.CAP.Pulsar/CAP.PulsarCapOptionsExtension.cs View File

@@ -0,0 +1,32 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using DotNetCore.CAP.Pulsar;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
internal sealed class PulsarCapOptionsExtension : ICapOptionsExtension
{
private readonly Action<PulsarOptions> _configure;

public PulsarCapOptionsExtension(Action<PulsarOptions> configure)
{
_configure = configure;
}

public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapMessageQueueMakerService>();

services.Configure(_configure);

services.AddSingleton<ITransport, PulsarTransport>();
services.AddSingleton<IConsumerClientFactory, PulsarConsumerClientFactory>();
services.AddSingleton<IConnectionFactory, ConnectionFactory>();
}
}
}

+ 37
- 0
src/DotNetCore.CAP.Pulsar/CAP.PulsarOptions.cs View File

@@ -0,0 +1,37 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
using Pulsar;

/// <summary>
/// Provides programmatic configuration for the CAP pulsar project.
/// </summary>
public class PulsarOptions
{
public string ServiceUrl { get; set; }

public TlsOptions TlsOptions { get; set; }
}
}

namespace DotNetCore.CAP.Pulsar
{
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

public class TlsOptions
{
private static readonly global::Pulsar.Client.Api.PulsarClientConfiguration Default =
global::Pulsar.Client.Api.PulsarClientConfiguration.Default;

public bool UseTls { get; set; } = Default.UseTls;
public bool TlsHostnameVerificationEnable { get; set; } = Default.TlsHostnameVerificationEnable;
public bool TlsAllowInsecureConnection { get; set; } = Default.TlsAllowInsecureConnection;
public X509Certificate2 TlsTrustCertificate { get; set; } = Default.TlsTrustCertificate;
public global::Pulsar.Client.Api.Authentication Authentication { get; set; } = Default.Authentication;
public SslProtocols TlsProtocols { get; set; } = Default.TlsProtocols;
}
}

+ 23
- 0
src/DotNetCore.CAP.Pulsar/DotNetCore.CAP.Pulsar.csproj View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DotNetCore.CAP.Pulsar</AssemblyName>
<PackageTags>$(PackageTags);Pulsar</PackageTags>
</PropertyGroup>

<PropertyGroup>
<WarningsAsErrors>NU1605;NU1701</WarningsAsErrors>
<NoWarn>NU1701;CS1591</NoWarn>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.Pulsar.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Pulsar.Client" Version="2.9.1" />
</ItemGroup>

</Project>

+ 77
- 0
src/DotNetCore.CAP.Pulsar/IConnectionFactory.Default.cs View File

@@ -0,0 +1,77 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Pulsar.Client.Api;

namespace DotNetCore.CAP.Pulsar
{
public class ConnectionFactory : IConnectionFactory, IAsyncDisposable
{
private PulsarClient _client;
private readonly PulsarOptions _options;
private readonly ConcurrentDictionary<string, Task<IProducer<byte[]>>> _topicProducers;

public ConnectionFactory(ILogger<ConnectionFactory> logger, IOptions<PulsarOptions> options)
{
_options = options.Value;
_topicProducers = new ConcurrentDictionary<string, Task<IProducer<byte[]>>>();

logger.LogDebug("CAP Pulsar configuration: {0}", JsonConvert.SerializeObject(_options, Formatting.Indented));
}

public string ServersAddress => _options.ServiceUrl;

public async Task<IProducer<byte[]>> CreateProducerAsync(string topic)
{
_client ??= RentClient();

async Task<IProducer<byte[]>> ValueFactory(string top)
{
return await _client.NewProducer()
.Topic(top)
.CreateAsync();
}

//connection may lost
return await _topicProducers.GetOrAdd(topic, ValueFactory);
}

public PulsarClient RentClient()
{
lock (this)
{
if (_client == null)
{
var builder = new PulsarClientBuilder().ServiceUrl(_options.ServiceUrl);
if (_options.TlsOptions != null)
{
builder.EnableTls(_options.TlsOptions.UseTls);
builder.EnableTlsHostnameVerification(_options.TlsOptions.TlsHostnameVerificationEnable);
builder.AllowTlsInsecureConnection(_options.TlsOptions.TlsAllowInsecureConnection);
builder.TlsTrustCertificate(_options.TlsOptions.TlsTrustCertificate);
builder.Authentication(_options.TlsOptions.Authentication);
builder.TlsProtocols(_options.TlsOptions.TlsProtocols);
}

_client = builder.BuildAsync().Result;
}

return _client;
}
}

public async ValueTask DisposeAsync()
{
foreach (var value in _topicProducers.Values)
{
_ = (await value).DisposeAsync();
}
}
}
}

+ 17
- 0
src/DotNetCore.CAP.Pulsar/IConnectionFactory.cs View File

@@ -0,0 +1,17 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Pulsar.Client.Api;

namespace DotNetCore.CAP.Pulsar
{
public interface IConnectionFactory
{
string ServersAddress { get; }

Task<IProducer<byte[]>> CreateProducerAsync(string topic);

PulsarClient RentClient();
}
}

+ 55
- 0
src/DotNetCore.CAP.Pulsar/ITransport.Pulsar.cs View File

@@ -0,0 +1,55 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Messages;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Logging;

namespace DotNetCore.CAP.Pulsar
{
internal class PulsarTransport : ITransport
{
private readonly IConnectionFactory _connectionFactory;
private readonly ILogger _logger;

public PulsarTransport(ILogger<PulsarTransport> logger, IConnectionFactory connectionFactory)
{
_logger = logger;
_connectionFactory = connectionFactory;
}

public BrokerAddress BrokerAddress => new BrokerAddress("Pulsar", _connectionFactory.ServersAddress);

public async Task<OperateResult> SendAsync(TransportMessage message)
{
var producer = await _connectionFactory.CreateProducerAsync(message.GetName());

try
{
var headerDic = new Dictionary<string, string>(message.Headers);
headerDic.TryGetValue(PulsarHeaders.PulsarKey, out var key);
var pulsarMessage = producer.NewMessage(message.Body, key, headerDic);
var result = await producer.SendAsync(pulsarMessage);

if (result.Type != null)
{
_logger.LogDebug($"pulsar topic message [{message.GetName()}] has been published.");

return OperateResult.Success;
}

throw new PublisherSentFailedException("pulsar message persisted failed!");
}
catch (Exception ex)
{
var wrapperEx = new PublisherSentFailedException(ex.Message, ex);

return OperateResult.Failed(wrapperEx);
}
}
}
}

+ 98
- 0
src/DotNetCore.CAP.Pulsar/PulsarConsumerClient.cs View File

@@ -0,0 +1,98 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using DotNetCore.CAP.Messages;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Options;
using Pulsar.Client.Api;
using Pulsar.Client.Common;

namespace DotNetCore.CAP.Pulsar
{
internal sealed class PulsarConsumerClient : IConsumerClient
{
private static PulsarClient _client;
private readonly string _groupId;
private readonly PulsarOptions _pulsarOptions;
private IConsumer<byte[]> _consumerClient;

public PulsarConsumerClient(PulsarClient client,string groupId, IOptions<PulsarOptions> options)
{
_client = client;
_groupId = groupId;
_pulsarOptions = options.Value;
}

public event EventHandler<TransportMessage> OnMessageReceived;

public event EventHandler<LogMessageEventArgs> OnLog;

public BrokerAddress BrokerAddress => new BrokerAddress("Pulsar", _pulsarOptions.ServiceUrl);

public void Subscribe(IEnumerable<string> topics)
{
if (topics == null)
{
throw new ArgumentNullException(nameof(topics));
}

var serviceName = Assembly.GetEntryAssembly()?.GetName().Name.ToLower();

_consumerClient = _client.NewConsumer()
.Topics(topics)
.SubscriptionName(_groupId)
.ConsumerName(serviceName)
.SubscriptionType(SubscriptionType.Shared)
.SubscribeAsync().Result;
}

public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var consumerResult = _consumerClient.ReceiveAsync().Result;

var headers = new Dictionary<string, string>(consumerResult.Properties.Count);
foreach (var header in consumerResult.Properties)
{
headers.Add(header.Key, header.Value);
}
headers.Add(Headers.Group, _groupId);

var message = new TransportMessage(headers, consumerResult.Data);

OnMessageReceived?.Invoke(consumerResult.MessageId, message);
}
// ReSharper disable once FunctionNeverReturns
}

public void Commit(object sender)
{
_consumerClient.AcknowledgeAsync((MessageId)sender);
}

public void Reject(object sender)
{
_consumerClient.NegativeAcknowledge((MessageId)sender);
}

public void Dispose()
{
_consumerClient?.DisposeAsync();
}

private void ConsumerClient_OnConsumeError(IConsumer<byte[]> consumer, Exception e)
{
var logArgs = new LogMessageEventArgs
{
LogType = MqLogType.ServerConnError,
Reason = $"An error occurred during connect pulsar --> {e.Message}"
};
OnLog?.Invoke(null, logArgs);
}
}
}

+ 34
- 0
src/DotNetCore.CAP.Pulsar/PulsarConsumerClientFactory.cs View File

@@ -0,0 +1,34 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Options;

namespace DotNetCore.CAP.Pulsar
{
internal sealed class PulsarConsumerClientFactory : IConsumerClientFactory
{
private readonly IConnectionFactory _connection;
private readonly IOptions<PulsarOptions> _pulsarOptions;

public PulsarConsumerClientFactory(IConnectionFactory connection, IOptions<PulsarOptions> pulsarOptions)
{
_connection = connection;
_pulsarOptions = pulsarOptions;
}

public IConsumerClient Create(string groupId)
{
try
{
var client = _connection.RentClient();
var consumerClient = new PulsarConsumerClient(client,groupId, _pulsarOptions);
return consumerClient;
}
catch (System.Exception e)
{
throw new BrokerConnectionException(e);
}
}
}
}

+ 10
- 0
src/DotNetCore.CAP.Pulsar/PulsarHeaders.cs View File

@@ -0,0 +1,10 @@
// Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace DotNetCore.CAP.Pulsar
{
public static class PulsarHeaders
{
public const string PulsarKey = "cap-pulsar-key";
}
}

+ 1
- 1
src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj View File

@@ -12,7 +12,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
</ItemGroup>

<ItemGroup>


+ 3
- 1
src/DotNetCore.CAP.RabbitMQ/IConnectionChannelPool.Default.cs View File

@@ -105,7 +105,7 @@ namespace DotNetCore.CAP.RabbitMQ
if (options.HostName.Contains(","))
{
options.ConnectionFactoryOptions?.Invoke(factory);
return () => factory.CreateConnection(
options.HostName.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries));
}
@@ -149,6 +149,8 @@ namespace DotNetCore.CAP.RabbitMQ
return true;
}

connection.Dispose();

Interlocked.Decrement(ref _count);

Debug.Assert(_maxSize == 0 || _pool.Count <= _maxSize);


+ 4
- 8
src/DotNetCore.CAP.RabbitMQ/ITransport.RabbitMQ.cs View File

@@ -40,15 +40,15 @@ namespace DotNetCore.CAP.RabbitMQ

var props = channel.CreateBasicProperties();
props.DeliveryMode = 2;
props.Headers = message.Headers.ToDictionary(x => x.Key, x => (object) x.Value);
props.Headers = message.Headers.ToDictionary(x => x.Key, x => (object)x.Value);
channel.ExchangeDeclare(_exchange, RabbitMQOptions.ExchangeType, true);

channel.BasicPublish(_exchange, message.GetName(), props, message.Body);

channel.WaitForConfirmsOrDie(TimeSpan.FromSeconds(5));

_logger.LogDebug($"RabbitMQ topic message [{message.GetName()}] has been published.");
_logger.LogInformation("CAP message '{0}' published, internal id '{1}'", message.GetName(), message.GetId());

return Task.FromResult(OperateResult.Success);
}
@@ -67,11 +67,7 @@ namespace DotNetCore.CAP.RabbitMQ
{
if (channel != null)
{
var returned = _connectionChannelPool.Return(channel);
if (!returned)
{
channel.Dispose();
}
_connectionChannelPool.Return(channel);
}
}
}


+ 18
- 3
src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs View File

@@ -81,16 +81,23 @@ namespace DotNetCore.CAP.RabbitMQ

public void Commit(object sender)
{
_channel.BasicAck((ulong)sender, false);
if (_channel.IsOpen)
{
_channel.BasicAck((ulong)sender, false);
}
}

public void Reject(object sender)
{
_channel.BasicReject((ulong)sender, true);
if (_channel.IsOpen)
{
_channel.BasicReject((ulong)sender, true);
}
}

public void Dispose()
{

_channel?.Dispose();
//The connection should not be closed here, because the connection is still in use elsewhere.
//_connection?.Dispose();
@@ -169,11 +176,19 @@ namespace DotNetCore.CAP.RabbitMQ
private void OnConsumerReceived(object sender, BasicDeliverEventArgs e)
{
var headers = new Dictionary<string, string>();

if (e.BasicProperties.Headers != null)
{
foreach (var header in e.BasicProperties.Headers)
{
headers.Add(header.Key, header.Value == null ? null : Encoding.UTF8.GetString((byte[])header.Value));
if (header.Value is byte[] val)
{
headers.Add(header.Key, Encoding.UTF8.GetString(val));
}
else
{
headers.Add(header.Key, header.Value?.ToString());
}
}
}



+ 1
- 1
src/DotNetCore.CAP.RedisStreams/DotNetCore.CAP.RedisStreams.csproj View File

@@ -12,7 +12,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="2.2.4" />
<PackageReference Include="StackExchange.Redis" Version="2.2.79" />
</ItemGroup>

<ItemGroup>


+ 20
- 2
src/DotNetCore.CAP.RedisStreams/IConnectionPool.LazyConnection.cs View File

@@ -24,12 +24,30 @@ namespace DotNetCore.CAP.RedisStreams
private static async Task<RedisConnection> ConnectAsync(CapRedisOptions redisOptions,
ILogger<AsyncLazyRedisConnection> logger)
{
int attemp = 1;

var redisLogger = new RedisLogger(logger);

var connection = await ConnectionMultiplexer.ConnectAsync(redisOptions.Configuration, redisLogger)
ConnectionMultiplexer connection = null;

while (attemp <= 5)
{
connection = await ConnectionMultiplexer.ConnectAsync(redisOptions.Configuration, redisLogger)
.ConfigureAwait(false);

connection.LogEvents(logger);
connection.LogEvents(logger);

if (!connection.IsConnected)
{
logger.LogWarning($"Can't establish redis connection,trying to establish connection [attemp {attemp}].");
await Task.Delay(TimeSpan.FromSeconds(2));
++attemp;
}
else
attemp = 6;
}
if (connection == null)
throw new Exception($"Can't establish redis connection,after [{attemp}] attemps.");

return new RedisConnection(connection);
}


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save