浏览代码

Merge branch 'master' into supports/pulsar

# Conflicts:
#	CAP.sln
master
Savorboard 3 年前
父节点
当前提交
1b8290f9a5
共有 100 个文件被更改,包括 2548 次插入1205 次删除
  1. +25
    -0
      .dockerignore
  2. +66
    -0
      .github/workflows/deploy-docs-and-dashbaord.yml
  3. +2
    -13
      .gitignore
  4. +2
    -2
      .travis.yml
  5. +42
    -6
      CAP.sln
  6. +2
    -0
      CAP.sln.DotSettings
  7. +4
    -10
      README.md
  8. +6
    -4
      README.zh-cn.md
  9. +2
    -2
      appveyor.yml
  10. +6
    -8
      build/BuildScript.Version.cs
  11. +2
    -2
      build/BuildScript.csproj
  12. +2
    -2
      build/version.props
  13. +149
    -0
      docs/content/about/release-notes.md
  14. 二进制
     
  15. +24
    -9
      docs/content/user-guide/en/cap/configuration.md
  16. +54
    -0
      docs/content/user-guide/en/cap/filter.md
  17. +94
    -3
      docs/content/user-guide/en/cap/messaging.md
  18. +0
    -7
      docs/content/user-guide/en/cap/sagas.md
  19. +2
    -0
      docs/content/user-guide/en/getting-started/introduction.md
  20. +27
    -1
      docs/content/user-guide/en/getting-started/quick-start.md
  21. +49
    -22
      docs/content/user-guide/en/monitoring/dashboard.md
  22. +0
    -3
      docs/content/user-guide/en/monitoring/health-checks.md
  23. +10
    -10
      docs/content/user-guide/en/samples/faq.md
  24. +12
    -1
      docs/content/user-guide/en/storage/general.md
  25. +1
    -1
      docs/content/user-guide/en/storage/sqlserver.md
  26. +20
    -1
      docs/content/user-guide/en/transport/azure-service-bus.md
  27. +14
    -0
      docs/content/user-guide/en/transport/general.md
  28. +36
    -0
      docs/content/user-guide/en/transport/kafka.md
  29. +60
    -0
      docs/content/user-guide/en/transport/nats.md
  30. +23
    -2
      docs/content/user-guide/en/transport/rabbitmq.md
  31. +59
    -0
      docs/content/user-guide/en/transport/redis-streams.md
  32. +27
    -10
      docs/content/user-guide/zh/cap/configuration.md
  33. +53
    -0
      docs/content/user-guide/zh/cap/filter.md
  34. +51
    -3
      docs/content/user-guide/zh/cap/messaging.md
  35. +0
    -7
      docs/content/user-guide/zh/cap/sagas.md
  36. +2
    -0
      docs/content/user-guide/zh/getting-started/introduction.md
  37. +1
    -1
      docs/content/user-guide/zh/getting-started/quick-start.md
  38. +55
    -21
      docs/content/user-guide/zh/monitoring/dashboard.md
  39. +0
    -3
      docs/content/user-guide/zh/monitoring/health-checks.md
  40. +123
    -0
      docs/content/user-guide/zh/samples/castle.dynamicproxy.md
  41. +10
    -10
      docs/content/user-guide/zh/samples/faq.md
  42. +7
    -1
      docs/content/user-guide/zh/storage/general.md
  43. +1
    -1
      docs/content/user-guide/zh/storage/sqlserver.md
  44. +19
    -0
      docs/content/user-guide/zh/transport/azure-service-bus.md
  45. +14
    -2
      docs/content/user-guide/zh/transport/general.md
  46. +61
    -0
      docs/content/user-guide/zh/transport/nats.md
  47. +24
    -2
      docs/content/user-guide/zh/transport/rabbitmq.md
  48. +58
    -0
      docs/content/user-guide/zh/transport/redis-streams.md
  49. +31
    -13
      docs/mkdocs.yml
  50. +1
    -1
      samples/Sample.AmazonSQS.InMemory/Sample.AmazonSQS.InMemory.csproj
  51. +1
    -1
      samples/Sample.AmazonSQS.InMemory/appsettings.json
  52. +2
    -2
      samples/Sample.ConsoleApp/Program.cs
  53. +2
    -2
      samples/Sample.ConsoleApp/Sample.ConsoleApp.csproj
  54. +49
    -0
      samples/Sample.Dashboard.Auth/Controllers/ValuesController.cs
  55. +50
    -0
      samples/Sample.Dashboard.Auth/MyDashboardAuthenticationHandler.cs
  56. +17
    -0
      samples/Sample.Dashboard.Auth/Program.cs
  57. +18
    -0
      samples/Sample.Dashboard.Auth/Sample.Dashboard.Auth.csproj
  58. +94
    -0
      samples/Sample.Dashboard.Auth/Startup.cs
  59. +13
    -0
      samples/Sample.Dashboard.Auth/appsettings.json
  60. +2
    -2
      samples/Sample.Kafka.PostgreSql/Sample.Kafka.PostgreSql.csproj
  61. +1
    -1
      samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj
  62. +1
    -1
      samples/Sample.RabbitMQ.MongoDB/Startup.cs
  63. +2
    -2
      samples/Sample.RabbitMQ.MySql/AppDbContext.cs
  64. +3
    -3
      samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj
  65. +2
    -14
      samples/Sample.RabbitMQ.SqlServer/AppDbContext.cs
  66. +18
    -8
      samples/Sample.RabbitMQ.SqlServer/Controllers/ValuesController.cs
  67. +4
    -4
      samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj
  68. +4
    -1
      samples/Sample.RabbitMQ.SqlServer/Startup.cs
  69. +54
    -0
      samples/Samples.Redis.SqlServer/Controllers/HomeController.cs
  70. +25
    -0
      samples/Samples.Redis.SqlServer/Dockerfile
  71. +20
    -0
      samples/Samples.Redis.SqlServer/Program.cs
  72. +14
    -0
      samples/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj
  73. +47
    -0
      samples/Samples.Redis.SqlServer/Startup.cs
  74. +9
    -0
      samples/Samples.Redis.SqlServer/appsettings.Development.json
  75. +10
    -0
      samples/Samples.Redis.SqlServer/appsettings.json
  76. +90
    -0
      samples/Samples.Redis.SqlServer/docker-compose.yml
  77. +1
    -1
      src/Directory.Build.props
  78. +255
    -0
      src/DotNetCore.CAP.AmazonSQS/AmazonPolicyExtensions.cs
  79. +69
    -11
      src/DotNetCore.CAP.AmazonSQS/AmazonSQSConsumerClient.cs
  80. +5
    -5
      src/DotNetCore.CAP.AmazonSQS/DotNetCore.CAP.AmazonSQS.csproj
  81. +50
    -17
      src/DotNetCore.CAP.AmazonSQS/ITransport.AmazonSQS.cs
  82. +52
    -12
      src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusConsumerClient.cs
  83. +16
    -0
      src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusConsumerCommitInput.cs
  84. +7
    -0
      src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusHeaders.cs
  85. +7
    -0
      src/DotNetCore.CAP.AzureServiceBus/CAP.AzureServiceBusOptions.cs
  86. +4
    -4
      src/DotNetCore.CAP.AzureServiceBus/DotNetCore.CAP.AzureServiceBus.csproj
  87. +10
    -6
      src/DotNetCore.CAP.AzureServiceBus/ITransport.AzureServiceBus.cs
  88. +0
    -128
      src/DotNetCore.CAP.Dashboard/AwaitableInfo.cs
  89. +0
    -37
      src/DotNetCore.CAP.Dashboard/BatchCommandDispatcher.cs
  90. +140
    -0
      src/DotNetCore.CAP.Dashboard/CAP.BuilderExtension.cs
  91. +0
    -151
      src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs
  92. +26
    -9
      src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs
  93. +14
    -3
      src/DotNetCore.CAP.Dashboard/CAP.DashboardOptionsExtensions.cs
  94. +7
    -10
      src/DotNetCore.CAP.Dashboard/CapCache.cs
  95. +0
    -44
      src/DotNetCore.CAP.Dashboard/CoercedAwaitableInfo.cs
  96. +0
    -37
      src/DotNetCore.CAP.Dashboard/CombinedResourceDispatcher.cs
  97. +0
    -42
      src/DotNetCore.CAP.Dashboard/CommandDispatcher.cs
  98. +0
    -5
      src/DotNetCore.CAP.Dashboard/Content/css/bootstrap.min.css
  99. +0
    -457
      src/DotNetCore.CAP.Dashboard/Content/css/cap.css
  100. +0
    -1
      src/DotNetCore.CAP.Dashboard/Content/css/jsonview.min.css

+ 25
- 0
.dockerignore 查看文件

@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

+ 66
- 0
.github/workflows/deploy-docs-and-dashbaord.yml 查看文件

@@ -0,0 +1,66 @@
name: Publish docs & Build dashboard
on:
push:
branches:
- master
jobs:
changes:
runs-on: ubuntu-latest
outputs:
docs: ${{ steps.filter.outputs.docs }}
dashboard: ${{ steps.filter.outputs.dashboard }}
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Check for changes 🎯
uses: dorny/paths-filter@v2
id: filter
with:
filters: |
docs:
- 'docs/**'
dashboard:
- 'src/DotNetCore.CAP.Dashboard/wwwroot/src/**'
build-dashbaord-and-push:
needs: changes
if: ${{ needs.changes.outputs.dashboard == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/DotNetCore.CAP.Dashboard/wwwroot
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Use Node.js 🥽
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: Install dependencies 🧵
run: npm install
- name: Build to dist 🧨
run: npm run build
- name: Commit & Push dist changes 🚀
uses: actions-js/push@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

build-docs-and-deploy:
needs: changes
if: ${{ needs.changes.outputs.docs == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false
- name: Deploy docs 🚀
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }}
CONFIG_FILE: docs/mkdocs.yml

+ 2
- 13
.gitignore 查看文件

@@ -1,4 +1,5 @@
[Oo]bj/
node_modules
[Oo]bj/
[Bb]in/
TestResults/
.nuget/
@@ -12,8 +13,6 @@ PublishProfiles/
*.docstates
_ReSharper.*
nuget.exe
*net45.csproj
*net451.csproj
*k10.csproj
*.psess
*.vsp
@@ -24,21 +23,11 @@ nuget.exe
*.*sdf
*.ipch
*.sln.ide
project.lock.json
.vs
.build/
.testPublish/
obj/
bin/
/.idea/.idea.CAP
/.idea/.idea.CAP
/.idea
Properties
/pack.bat
/src/DotNetCore.CAP/project.json
/src/DotNetCore.CAP/packages.config
/src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj
/NuGet.config
.vscode/*
samples/Sample.RabbitMQ.MongoDB/appsettings.Development.json
site/

+ 2
- 2
.travis.yml 查看文件

@@ -2,7 +2,7 @@ language: csharp
sudo: required
dist: xenial
solution: CAP.sln
dotnet: 3.1.100
dotnet: 5.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 5.1.1
- dotnet tool install --global FlubuCore.GlobalTool --version 6.1.0
- flubu build tests

+ 42
- 6
CAP.sln 查看文件

@@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.gitignore = .gitignore
.travis.yml = .travis.yml
appveyor.yml = appveyor.yml
.github\workflows\deploy-docs-and-dashbaord.yml = .github\workflows\deploy-docs-and-dashbaord.yml
.github\ISSUE_TEMPLATE = .github\ISSUE_TEMPLATE
LICENSE.txt = LICENSE.txt
README.md = README.md
@@ -67,7 +68,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.AmazonSQS",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.AmazonSQS.InMemory", "samples\Sample.AmazonSQS.InMemory\Sample.AmazonSQS.InMemory.csproj", "{B187DD15-092D-4B72-9807-50856607D237}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.Pulsar", "src\DotNetCore.CAP.Pulsar\DotNetCore.CAP.Pulsar.csproj", "{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.NATS", "src\DotNetCore.CAP.NATS\DotNetCore.CAP.NATS.csproj", "{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.Redis.SqlServer", "samples\Samples.Redis.SqlServer\Samples.Redis.SqlServer.csproj", "{375AF85D-8C81-47C6-BE5B-D0874D4971EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.RedisStreams", "src\DotNetCore.CAP.RedisStreams\DotNetCore.CAP.RedisStreams.csproj", "{54458B54-49CC-454C-82B2-4AED681D9D07}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Dashboard.Auth", "samples\Sample.Dashboard.Auth\Sample.Dashboard.Auth.csproj", "{6E059983-DE89-4D53-88F5-D9083BCE257F}"
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.Pulsar", "src\DotNetCore.CAP.Pulsar\DotNetCore.CAP.Pulsar.csproj", "{AB7A10CB-2C7E-49CE-AA21-893772FF6546}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -155,10 +166,30 @@ Global
{B187DD15-092D-4B72-9807-50856607D237}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B187DD15-092D-4B72-9807-50856607D237}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B187DD15-092D-4B72-9807-50856607D237}.Release|Any CPU.Build.0 = Release|Any CPU
{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1}.Release|Any CPU.Build.0 = Release|Any CPU
{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07}.Release|Any CPU.Build.0 = Release|Any CPU
{375AF85D-8C81-47C6-BE5B-D0874D4971EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{375AF85D-8C81-47C6-BE5B-D0874D4971EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{375AF85D-8C81-47C6-BE5B-D0874D4971EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{375AF85D-8C81-47C6-BE5B-D0874D4971EA}.Release|Any CPU.Build.0 = Release|Any CPU
{54458B54-49CC-454C-82B2-4AED681D9D07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54458B54-49CC-454C-82B2-4AED681D9D07}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54458B54-49CC-454C-82B2-4AED681D9D07}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54458B54-49CC-454C-82B2-4AED681D9D07}.Release|Any CPU.Build.0 = Release|Any CPU
{6E059983-DE89-4D53-88F5-D9083BCE257F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E059983-DE89-4D53-88F5-D9083BCE257F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E059983-DE89-4D53-88F5-D9083BCE257F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E059983-DE89-4D53-88F5-D9083BCE257F}.Release|Any CPU.Build.0 = Release|Any CPU
{23684403-7DA8-489A-8A1E-8056D7683E18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -184,7 +215,12 @@ Global
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{B187DD15-092D-4B72-9807-50856607D237} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{33C48DD1-5B7D-475B-B849-FFE1D9A4FBD1} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{8B2FD3EA-E72B-4A82-B182-B87EC0C15D07} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{375AF85D-8C81-47C6-BE5B-D0874D4971EA} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{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}
{AB7A10CB-2C7E-49CE-AA21-893772FF6546} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB}


+ 2
- 0
CAP.sln.DotSettings 查看文件

@@ -1,5 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NATS/@EntryIndexedValue">NATS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SNS/@EntryIndexedValue">SNS</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mongo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=NATS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 4
- 10
README.md 查看文件

@@ -32,12 +32,13 @@ CAP can be installed in your project with the following command.
PM> Install-Package DotNetCore.CAP
```

CAP supports RabbitMQ, Kafka and AzureService as message queue, following packages are available to install:
CAP supports RabbitMQ, Kafka, AzureService, AmazonSQS as message queue, following packages are available to install:

```
PM> Install-Package DotNetCore.CAP.Kafka
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.AzureServiceBus
PM> Install-Package DotNetCore.CAP.AmazonSQS
```

CAP supports SqlServer, MySql, PostgreSql,MongoDB as event log storage.
@@ -80,6 +81,7 @@ public void ConfigureServices(IServiceCollection services)
x.UseRabbitMQ("ConnectionString");
x.UseKafka("ConnectionString");
x.UseAzureServiceBus("ConnectionString");
x.UseAmazonSQS();
});
}

@@ -239,7 +241,7 @@ services.AddCap(x =>

### Dashboard

CAP v2.1+ provides dashboard pages, you can easily view messages that were sent and received. In addition, you can also view the message status in real time in the dashboard. Use the following command to install the Dashboard in your project.
CAP also provides dashboard pages, you can easily view messages that were sent and received. In addition, you can also view the message status in real time in the dashboard. Use the following command to install the Dashboard in your project.

```
PM> Install-Package DotNetCore.CAP.Dashboard
@@ -270,14 +272,6 @@ services.AddCap(x =>

The default dashboard address is :[http://localhost:xxx/cap](http://localhost:xxx/cap), you can configure relative path `/cap` with `x.UseDashboard(opt =>{ opt.MatchPath="/mycap"; })`.

![dashboard](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220827302-189215107.png)

![received](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220934115-1107747665.png)

![subscibers](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004220949193-884674167.png)

![nodes](http://images2017.cnblogs.com/blog/250417/201710/250417-20171004221001880-1162918362.png)


## Contribute



+ 6
- 4
README.zh-cn.md 查看文件

@@ -21,7 +21,7 @@ CAP 采用的是和当前数据库集成的本地消息表的方案来解决在

你同样可以把 CAP 当做 EventBus 来使用,CAP提供了一种更加简单的方式来实现事件消息的发布和订阅,在订阅以及发布的过程中,你不需要继承或实现任何接口。

这是CAP集在ASP.NET Core 微服务架构中的一个示意图:
这是 CAP 集在ASP.NET Core 微服务架构中的一个示意图:

## 架构预览

@@ -39,12 +39,13 @@ CAP 采用的是和当前数据库集成的本地消息表的方案来解决在
PM> Install-Package DotNetCore.CAP
```

CAP 支持 Kafka、RabbitMQ、AzureServiceBus 等消息队列,你可以按需选择下面的包进行安装:
CAP 支持 Kafka、RabbitMQ、AzureServiceBus、AmazonSQS 等消息队列,你可以按需选择下面的包进行安装:

```
PM> Install-Package DotNetCore.CAP.Kafka
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.AzureServiceBus
PM> Install-Package DotNetCore.CAP.AmazonSQS
```

CAP 提供了 Sql Server, MySql, PostgreSQL,MongoDB 的扩展作为数据库存储:
@@ -81,10 +82,11 @@ public void ConfigureServices(IServiceCollection services)
//如果你使用的 MongoDB,你可以添加如下配置:
x.UseMongoDB("ConnectionStrings"); //注意,仅支持MongoDB 4.0+集群
//CAP支持 RabbitMQ、Kafka、AzureServiceBus 等作为MQ,根据使用选择配置:
//CAP支持 RabbitMQ、Kafka、AzureServiceBus、AmazonSQS 等作为MQ,根据使用选择配置:
x.UseRabbitMQ("ConnectionStrings");
x.UseKafka("ConnectionStrings");
x.UseAzureServiceBus("ConnectionStrings");
x.UseAmazonSQS();
});
}

@@ -237,7 +239,7 @@ services.AddCap(x =>

### Dashboard

CAP 2.1+ 以上版本中提供了仪表盘(Dashboard)功能,你可以很方便的查看发出和接收到的消息。除此之外,你还可以在仪表盘中实时查看发送或者接收到的消息。
CAP 同时提供了仪表盘(Dashboard)功能,你可以很方便的查看发出和接收到的消息。 除此之外,你还可以在仪表盘中实时查看发送或者接收到的消息。

使用一下命令安装 Dashboard:



+ 2
- 2
appveyor.yml 查看文件

@@ -7,7 +7,7 @@ environment:
services:
- mysql
before_build:
- ps: dotnet tool install --global FlubuCore.GlobalTool --version 5.1.1
- ps: dotnet tool install --global FlubuCore.GlobalTool --version 6.1.0
build_script:
- ps: flubu
test: off
@@ -18,6 +18,6 @@ deploy:
on:
appveyor_repo_tag: true
api_key:
secure: PZXRBOGLyhYLP7ulHfrh6MnkqB8CstuitgbLcJr3cZkLJLLzPH0ahvuTtmhWxtR2
secure: Q/7aMFCMA363iQv1r2fgW2PyvAFL3H409s9Pq8SgmYAbTH+c6ZQcYC9evHpipuNR
skip_symbols: false
artifact: /artifacts\/.+\.s?nupkg/

+ 6
- 8
build/BuildScript.Version.cs 查看文件

@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.IO;
using System.Xml;
using FlubuCore.Context;
using FlubuCore.Scripting.Attributes;
@@ -11,7 +10,7 @@ namespace BuildScript
{
public BuildVersion FetchBuildVersion(ITaskContext context)
{
var content = System.IO.File.ReadAllText(RootDirectory.CombineWith("build/version.props"));
var content = File.ReadAllText(RootDirectory.CombineWith("build/version.props"));

XmlDocument doc = new XmlDocument();
doc.LoadXml(content);
@@ -26,13 +25,12 @@ namespace BuildScript

bool isCi = false;
bool isTagged = false;
if (!context.BuildSystems().IsLocalBuild)
if (!context.BuildServers().IsLocalBuild)
{
isCi = true;
bool isTagAppveyor = context.BuildSystems().AppVeyor().IsTag;
if (context.BuildSystems().RunningOn == BuildSystemType.AppVeyor && isTagAppveyor ||
context.BuildSystems().RunningOn == BuildSystemType.TravisCI && string.IsNullOrWhiteSpace(context.BuildSystems().Travis().TagName))
bool isTagAppveyor = context.BuildServers().AppVeyor().IsTag;
if (context.BuildServers().RunningOn == BuildServerType.AppVeyor && isTagAppveyor ||
context.BuildServers().RunningOn == BuildServerType.TravisCI && string.IsNullOrWhiteSpace(context.BuildServers().Travis().TagName))
{
isTagged = true;
}


+ 2
- 2
build/BuildScript.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

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

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

</Project>

+ 2
- 2
build/version.props 查看文件

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


+ 149
- 0
docs/content/about/release-notes.md 查看文件

@@ -1,5 +1,154 @@
# 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:**

* Add KafkaOptions.MainConfig to AutoCreateTopic. (#810)
* Add support rewriting the default configuration of Kafka consumer. (#822)
* Add DefaultChallengeScheme dashboard options to specify dashboard auth challenge scheme. (#815)

**Bug Fixed:**
* Fixed topic selector in IConsumerServiceSelector. (#806)
* Update AWS topic subscription and SQS access policy generation. (#808)
* Fixed memory leak when using transction to publish message. (#816)
* Fixed SQL content filter on IMonitoringApi.PostgreSql.cs. (#814)
* Fixed the expiration time display problem in the dashboard due to time zone issues (#820)
* Fixed the creation timing of Kafka automatically creating Topic. (#823)
* Fixed Dashboard metric not update. (#819)

## Version 5.0.0 (2021-03-23)
**Features:**

* Upgrade to .NET Standard 2.1 and support .NET 5. (#727)
* Replace Newtonsoft.Json to System.Text.Json. (#740)
* Support NATS Transport. (#595,#743)
* Enabling publiser confirms for RabbitMQ. (#730)
* Support query subscription from DI implementation factory. (#756)
* Add options to create lazy queue for RabbitMQ. (#772)
* Support to add custom tags for Consul. (#786)
* Support custom group and topic prefiex. (#780)
* Renemae DefaultGroup option to DefaultGroupName.
* Add auto create topic at startup for Kafka. (#795,#744)

**Bug Fixed:**

* Fixed retrying process earlier than consumer registration to DI. (#760)
* Fixed Amazon SQS missing pagination topics. (#765)
* Fixed RabbitMQ MessageTTL option to int type. (#787)
* Fixed Dashboard auth. (#793)
* Fixed ClientProvidedName could not be renamed for RabbitMQ. (#791)
* Fixed EntityFramework transaction will not rollback when exception occurred. (#798)

## Version 3.1.2 (2020-12-03)

**Features:**
* Support record the exception message in the headers. (#679)
* Support consul service check for https. (#722)
* Support custom producer threads count options for sending. (#731)
* Upgrade dependent nuget packages to latest.

**Bug Fixed:**

* Fixed InmemoryQueue expired messages are not removed bug. (#691)
* Fixed Executor key change lead to possible null reference exception. (#698)
* Fixed Postgresql delete expires data logic error. (#714)

## Version 3.1.1 (2020-09-23)

**Features:**

* Add consumer parameter with interface suppport. (#669)
* Add custom correlation id and message id support. (#668)
* Enhanced custom serialization support. (#641)

**Bug Fixed:**

* Solve the issue of being duplicated executors from different assemblies. (#666)
* Added comparer to remove duplicate ConsumerExecutors. (#653)
* Add re-enable the auto create topics configuration item for Kafka, it's false by default. now is true. (#635)
* Fixed postgresql transaction rollback invoke bug. (#640)
* Fixed SQLServer table name customize bug. (#632)

## Version 3.1.0 (2020-08-15)

**Features:**

* Add Amazon SQS support. (#597)
* Remove Dapper and replace with ADO.NET in storage project. (#583)
* Add debug symbols package to nuget.
* Upgrade dependent nuget package version to latest.
* English docs grammar correction. Thanks @mzorec

**Bug Fixed:**

* Fix mysql transaction rollback bug. (#598)
* Fix dashboard query bug. (#600)
* Fix mongo db query bug. (#611)
* Fix dashboard browser language detection bug. (#631)

## Version 3.0.4 (2020-05-27)

**Bug Fixed:**


二进制
查看文件


+ 24
- 9
docs/content/user-guide/en/cap/configuration.md 查看文件

@@ -31,7 +31,7 @@ For specific transport and storage configuration, you can take a look at the con

The `CapOptions` is used to store configuration information. By default they have default values, sometimes you may need to customize them.

#### DefaultGroup
#### DefaultGroupName

> Default: cap.queue.{assembly name}

@@ -41,6 +41,20 @@ The default consumer group name, corresponds to different names in different Tra
Map to [Queue Names](https://www.rabbitmq.com/queues.html#names) in RabbitMQ.
Map to [Consumer Group Id](http://kafka.apache.org/documentation/#group.id) in Apache Kafka.
Map to Subscription Name in Azure Service Bus.
Map to [Queue Group Name](https://docs.nats.io/nats-concepts/queue) in NATS.
Map to [Consumer Group](https://redis.io/topics/streams-intro#creating-a-consumer-group) in Redis Streams.

#### GroupNamePrefix

> Default: Null

Add unified prefixes for consumer group. https://github.com/dotnetcore/CAP/pull/780

#### TopicNamePrefix

> Default: Null

Add unified prefixes for topic/queue name. https://github.com/dotnetcore/CAP/pull/780

#### Versioning

@@ -71,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

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

@@ -87,14 +107,9 @@ Maximum number of retries. When this value is reached, retry will stop and the m

> Default: NULL

Type: `Action<MessageType, string, string>`

>
T1 : Message Type
T2 : Message Name
T3 : Message Content
Type: `Action<FailedInfo>`

Failure threshold callback. This action is called when the retry reaches the value set by `FailedRetryCount`, you can receive notification by specifying this parameter to make a manual intervention. For example, send an email or notification.
Failure threshold callback. This action is called when the retry reaches the value set by `FailedRetryCount`, you can receive notification by specifying this parameter to make a manual intervention. For example, send an email or notification.

#### SucceedMessageExpiredAfter



+ 54
- 0
docs/content/user-guide/en/cap/filter.md 查看文件

@@ -0,0 +1,54 @@
# Filter

Subscriber filters are similar to ASP.NET MVC filters and are mainly used to process additional work before and after the subscriber method is executed. Such as transaction management or logging, etc.

## Create subscribe filter

### Create Filter

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

```C#
public class MyCapFilter: SubscribeFilter
{
public override void OnSubscribeExecuting(ExecutingContext context)
{
// before subscribe method exectuing
}

public override void OnSubscribeExecuted(ExecutedContext context)
{
// after subscribe method executed
}

public override void OnSubscribeException(ExceptionContext context)
{
// subscribe method execution exception
}
}
```

In some scenarios, if you want to terminate the subscriber method execution, you can throw an exception in `OnSubscribeExecuting`, and choose to ignore the exception in `OnSubscribeException`.

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


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

### Configuration Filter

Use `AddSubscribeFilter<>` to add a filter.

```C#
services.AddCap(opt =>
{
// ***
}.AddSubscribeFilter<MyCapFilter>();
```

Currently, we do not support adding multiple filters.

+ 94
- 3
docs/content/user-guide/en/cap/messaging.md 查看文件

@@ -2,6 +2,95 @@

The data sent by using the `ICapPublisher` interface is called `Message`.

## Compensating transaction

Wiki :
[Compensating transaction](https://en.wikipedia.org/wiki/Compensating_transaction)

In some cases, consumers need to return the execution value to tell the publisher, so that the publisher can implement some compensation actions, usually we called message compensation.

Usually you can notify the upstream by republishing a new message in the consumer code. CAP provides a simple way to do this. You can specify `callbackName` parameter when publishing message, usually this only applies to point-to-point consumption. The following is an example.

For example, in an e-commerce application, the initial status of the order is pending, and the status is marked as succeeded when the product quantity is successfully deducted, otherwise it is failed.

```C#
// ============= Publisher =================

_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(JsonElement param)
{
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
}
}

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

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

//business logic

return new { OrderId = orderId, IsSuccess = true };
}
```

## Heterogeneous system integration

In version 3.0+, we reconstructed the message structure. We used the Header in the message protocol in the message queue to transmit some additional information, so that we can do it in the Body without modifying or packaging the user’s original The message data format and content are sent.

This approach is reasonable. It helps to better integrate in heterogeneous systems. Compared with previous versions, users do not need to know the message structure used inside CAP to complete the integration work.

Now we divide the message into Header and Body for transmission.

The data in the body is the content of the original message sent by the user, that is, the content sent by calling the Publish method. We do not perform any packaging, but send it to the message queue after serialization.

In the Header, we need to pass some additional information so that the CAP can extract the key features for operation when the message is received.

The following is the content that needs to be written into the header of the message when sending a message in a heterogeneous system:

Key | DataType | Description
-- | --| --
cap-msg-id | string | Message Id, Generated by snowflake algorithm, can also be guid
cap-msg-name | string | The name of the message
cap-msg-type | string | The type of message, `typeof(T).FullName`(not required)
cap-senttime | string | sending time (not required)

### Custom headers
To consume messages sent without CAP headers, both Kafka and RabbitMQ consumers can inject a minimal set of headers using custom headers as shown below:
```C#
container.AddCap(x =>
{
x.UseRabbitMQ(z =>
{
z.ExchangeName = "TestExchange";
z.CustomHeaders = e => new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(DotNetCore.CAP.Messages.Headers.MessageId, SnowflakeId.Default().NextId().ToString()),
new KeyValuePair<string, string>(DotNetCore.CAP.Messages.Headers.MessageName, e.RoutingKey)
};
});
});
```

After adding `cap-msg-id` and `cap-msg-name`, CAP consumers receive messages sent directly from the RabbitMQ management tool.

## Scheduling

After CAP receives a message, it sends the message to Transport(RabitMq, Kafka...), which is transported by transport.
@@ -22,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.

@@ -36,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.

+ 0
- 7
docs/content/user-guide/en/cap/sagas.md 查看文件

@@ -1,7 +0,0 @@
# Sagas

Sagas (also known in the literature as "process managers") are stateful services. You can think of them as state machines whose transitions are driven by messages.

## Sagas on CAP

TODO

+ 2
- 0
docs/content/user-guide/en/getting-started/introduction.md 查看文件

@@ -25,6 +25,8 @@ CAP is modular in design and highly scalable. You have many options to choose fr

[Article: Introduction and how to use](http://www.cnblogs.com/savorboard/p/cap.html)

[Article: New features in version 5.0](https://www.cnblogs.com/savorboard/p/cap-5-0.html)

[Article: New features in version 3.0](https://www.cnblogs.com/savorboard/p/cap-3-0.html)

[Article: New features in version 2.6](https://www.cnblogs.com/savorboard/p/cap-2-6.html)


+ 27
- 1
docs/content/user-guide/en/getting-started/quick-start.md 查看文件

@@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services)
public class PublishController : Controller
{
[Route("~/send")]
public IActionResult SendMessage([FromService]ICapPublisher capBus)
public IActionResult SendMessage([FromServices]ICapPublisher capBus)
{
capBus.Publish("test.show.time", DateTime.Now);

@@ -45,6 +45,19 @@ public class PublishController : Controller
}
```

### Publish with extra header

```c#
var header = new Dictionary<string, string>()
{
["my.header.first"] = "first",
["my.header.second"] = "second"
};

capBus.Publish("test.show.time", DateTime.Now, header);

```

## Process Message

```C#
@@ -59,6 +72,19 @@ public class ConsumerController : Controller
}
```

### Process with extra header

```c#
[CapSubscribe("test.show.time")]
public void ReceiveMessage(DateTime time, [FromCap]CapHeader header)
{
Console.WriteLine("message time is:" + time);
Console.WriteLine("message firset header :" + header["my.header.first"]);
Console.WriteLine("message second header :" + header["my.header.second"]);
}

```

## Summary

One of the most powerful advantages of asynchronous messaging over direct integrated message queues is reliability, where failures in one part of the system do not propagate or cause the entire system to crash. Messages are stored inside the CAP to ensure the reliability of the message, and strategies such as retry are used to achieve the final consistency of data between services.

+ 49
- 22
docs/content/user-guide/en/monitoring/dashboard.md 查看文件

@@ -32,39 +32,66 @@ You can change the path of the Dashboard by modifying this configuration option.

This configuration option is used to configure the Dashboard front end to get the polling time of the status interface (/stats).

* Authorization
* UseAuth

This configuration option is used to configure the authorization filter when accessing the Dashboard. The default filter allows LAN access. When your application wants to provide external network access, you can customize authentication rules by setting this configuration. See the next section for details.
> Default:false

### Custom authentication
Enable authentication on dashboard request.

Dashboard authentication can be customized by implementing the `IDashboardAuthorizationFilter` interface.
* DefaultAuthenticationScheme

The following is a sample code that determines if access is allowed by reading the accesskey from the url request parameter.
Default scheme used for authentication. If no scheme is set, the DefaultScheme set up in AddAuthentication will be used.

* UseChallengeOnAuth

> Default:false

Enable authentication challenge on dashboard request.

* DefaultChallengeScheme

Default scheme used for authentication challenge. If no scheme is set, the DefaultChallengeScheme set up in AddAuthentication will be used.

### Custom authentication

From version 5.1.0, Dashboard authorization uses ASP.NET Core style by default and no longer provides custom authorization filters.

During Dashabord authentication, the value will be taken from `HttpContext.User?.Identity?.IsAuthenticated`. If it is not available, the authentication will fail and the `DefaultChallengeScheme` will be called (if configured).

You can view the usage details in the sample project `Sample.Dashboard.Auth`.

```C#
public class TestAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
services
.AddAuthorization()
.AddAuthentication(options =>
{
if(context.Request.GetQuery("accesskey")=="xxxxxx"){
return true;
}
return false;
}
}
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.UsePkce = true;

options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
})
```

Then configure this filter when registration Dashboard.
configuration:

```C#
services.AddCap(x =>
services.AddCap(cap =>
{
//...

// Register Dashboard
x.UseDashboard(opt => {
opt.Authorization = new[] {new TestAuthorizationFilter()};
cap.UseDashboard(d =>
{
d.UseChallengeOnAuth = true;
d.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
});
}
```

+ 0
- 3
docs/content/user-guide/en/monitoring/health-checks.md 查看文件

@@ -1,3 +0,0 @@
# Health Checks

TODO

+ 10
- 10
docs/content/user-guide/en/samples/faq.md 查看文件

@@ -2,19 +2,19 @@

!!! faq "Any IM group(e.g Tencent QQ group) to learn and chat about CAP?"

None for that. Better than wasting much time in IM group, I hope developers could be capable of independent thinking more, and solve problems yourselves with referenced documents, even create issues or send emails when errors are remaining present.
None of that. Better than wasting much time in IM group, I hope developers could be capable of independent thinking more, and solve problems yourselves with referenced documents, even create issues or send emails when errors are remaining present.

!!! faq "Does it require certain different databases, one each for productor and resumer in CAP?"
!!! faq "Does it require different databases, one each for producer and consumer in CAP?"

Not required differences necessary, a given advice is that using a special database for each program.
No difference necessary, a recommendation is to use a dedicated database for each program.

Otherwise, look at Q&A below.

!!! faq "How to use the same database for different applications?"
defining a prefix name of table in `ConfigureServices` method。
!!! faq "How to use the same database for different applications? (Only for MySQL)"
Define a table prefix name in `ConfigureServices` method.
codes exsample:
Code example:

```c#
public void ConfigureServices(IServiceCollection services)
@@ -31,14 +31,14 @@
}
```

!!! faq "Can CAP not use the database as event storage? I just want to sent the message"
!!! faq "Can CAP not use the database as event storage? I just want to send the message"

Not yet.

The purpose of CAP is that ensure consistency principle right in microservice or SOA architechtrues. The solution is based on ACID features of database, there is no sense about a single client wapper of message queue without database.
The purpose of CAP is that ensure consistency principle right in microservice or SOA architectures. The solution is based on ACID features of database, there is no sense about a single client wapper of message queue without database.

!!! faq "If the consumer is abnormal, can I roll back the database executed sql that the producer has executed?"

Can't roll back, CAP is the ultimate consistency solution.

You can imagine your scenario is to call a third party payment. If you are doing a third-party payment operation, after calling Alipay's interface successfully, and your own code is wrong, will Alipay roll back? If you don't roll back, what should you do? The same is true here.
You can imagine your scenario is to call a third party payment. If you are doing a third-party payment operation, after calling Alipay's interface successfully, and your own code is wrong, will Alipay roll back? If you don't roll back, what should you do? The same is true here.

+ 12
- 1
docs/content/user-guide/en/storage/general.md 查看文件

@@ -67,4 +67,15 @@ Timestamp | Message created time | string
Content | Message content | string
CallbackName | Consumer callback topic name | string

The `Id` field is generate using the mongo [objectid algorithm](https://www.mongodb.com/blog/post/generating-globally-unique-identifiers-for-use-with-mongodb).
The `Id` field is generate using the mongo [objectid algorithm](https://www.mongodb.com/blog/post/generating-globally-unique-identifiers-for-use-with-mongodb).


## Community-supported extensions

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

* SQLite ([@colinin](https://github.com/colinin)) :https://github.com/colinin/DotNetCore.CAP.Sqlite

* LiteDB ([@maikebing](https://github.com/maikebing)) :https://github.com/maikebing/CAP.Extensions

* SQLite & Oracle ([@cocosip](https://github.com/cocosip)) :https://github.com/cocosip/CAP-Extensions

+ 1
- 1
docs/content/user-guide/en/storage/sqlserver.md 查看文件

@@ -24,7 +24,7 @@ public void ConfigureServices(IServiceCollection services)

services.AddCap(x =>
{
x.UsePostgreSql(opt=>{
x.UseSqlServer(opt=>{
//SqlServerOptions
});
// x.UseXXX ...


+ 20
- 1
docs/content/user-guide/en/transport/azure-service-bus.md 查看文件

@@ -41,5 +41,24 @@ The AzureServiceBus configuration options provided directly by the CAP:
NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
ConnectionString | Endpoint address | string |
EnableSessions | Enable [Service bus sessions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/message-sessions) | bool | false
TopicPath | Topic entity path | string | cap
ManagementTokenProvider | Token provider | ITokenProvider | null
ManagementTokenProvider | Token provider | ITokenProvider | null

#### Sessions

When sessions are enabled (see `EnableSessions` option above), every message sent will have a session id. To control the session id, include
an extra header with name `AzureServiceBusHeaders.SessionId` when publishing events:

```csharp
ICapPublisher capBus = ...;
string yourEventName = ...;
YourEventType yourEvent = ...;

Dictionary<string, string> extraHeaders = new Dictionary<string, string>();
extraHeaders.Add(AzureServiceBusHeaders.SessionId, <your-session-id>);

capBus.Publish(yourEventName, yourEvent, extraHeaders);
```

If no session id header is present, the message id will be used as the session id.

+ 14
- 0
docs/content/user-guide/en/transport/general.md 查看文件

@@ -10,7 +10,9 @@ 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)

## How to select a transport

@@ -28,3 +30,15 @@ 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

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

* ActiveMQ (@[Lukas Zhang](https://github.com/lukazh/Lukaz.CAP.ActiveMQ)): https://github.com/lukazh

* RedisMQ ([@木木](https://github.com/difudotnet)) https://github.com/difudotnet/CAP.RedisMQ.Extensions

* ZeroMQ ([@maikebing](https://github.com/maikebing)): https://github.com/maikebing/CAP.Extensions




+ 36
- 0
docs/content/user-guide/en/transport/kafka.md 查看文件

@@ -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 查看文件

@@ -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
We currently implement NATS provider based on Request/Response mode, and we plan to replace it with JetStream in future version.
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)

+ 23
- 2
docs/content/user-guide/en/transport/rabbitmq.md 查看文件

@@ -44,8 +44,10 @@ UserName | Broker user name | string | guest
Password | Broker password | string | guest
VirtualHost | Broker virtual host | string | /
Port | Port | int | -1
TopicExchangeName | Default exchange name of cap created | string | cap.default.topic
QueueMessageExpires | Message expries after to delete, in milliseconds | int | (10 days) milliseconds
ExchangeName | Default exchange name | string | cap.default.topic
QueueArguments | Extra queue `x-arguments` | QueueArgumentsOptions | N/A
ConnectionFactoryOptions | RabbitMQClient native connection options | ConnectionFactory | N/A
CustomHeaders | Custom subscribe headers | Func<BasicDeliverEventArgs, List<KeyValuePair<string, string>>> | N/A

#### ConnectionFactory Options

@@ -66,6 +68,25 @@ services.AddCap(x =>

```

#### CustomHeaders Options

When the message sent from the RabbitMQ management console or 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.

Example:

```cs
x.UseRabbitMQ(aa =>
{
aa.CustomHeaders = e => new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(Headers.MessageId, SnowflakeId.Default().NextId().ToString()),
new KeyValuePair<string, string>(Headers.MessageName, e.RoutingKey),
};
});
```

#### How to connect cluster

using comma split connection string, like this:


+ 59
- 0
docs/content/user-guide/en/transport/redis-streams.md 查看文件

@@ -0,0 +1,59 @@
# Redis Streams

[Redis](https://redis.io/) is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker.

[Redis Stream](https://redis.io/topics/streams-intro) is a new data type introduced with Redis 5.0, which models a log data structure in a more abstract way with an append only data structure.

Redis Streams can be used in CAP as a message transporter.

## Configuration

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

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

```

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

```csharp

public void ConfigureServices(IServiceCollection services)
{
services.AddCap(capOptions =>
{
capOptions.UseRedis(redisOptions=>{
//redisOptions
});
});
}

```

#### Redis Streams Options

Redis configuration parameters provided directly by the CAP:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Configuration | redis connection configuration (StackExchange.Redis) | ConfigurationOptions | ConfigurationOptions
StreamEntriesCount | number of entries returned from a stream while reading | uint | 10
ConnectionPoolSize | number of connections pool | uint | 10

#### Redis ConfigurationOptions

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

```csharp
services.AddCap(capOptions =>
{
capOptions.UseRedis(redisOptions=>
{
// redis options.
redisOptions.Configuration.EndPoints.Add(IPAddress.Loopback, 0);
});
});
```

`Configuration` is a StackExchange.Redis ConfigurationOptions , you can find more details through this [link](https://stackexchange.github.io/StackExchange.Redis/Configuration)

+ 27
- 10
docs/content/user-guide/zh/cap/configuration.md 查看文件

@@ -30,15 +30,31 @@ services.AddCap(config =>

在 `AddCap` 中 `CapOptions` 对象是用来存储配置相关信息,默认情况下它们都具有一些默认值,有些时候你可能需要自定义。

#### DefaultGroup
#### DefaultGroupName

默认值:cap.queue.{程序集名称}

默认的消费者组的名字,在不同的 Transports 中对应不同的名字,可以通过自定义此值来自定义不同 Transports 中的名字,以便于查看。

> 在 RabbitMQ 中映射到 [Queue Names](https://www.rabbitmq.com/queues.html#names)。
> 在 Apache Kafka 中映射到 [Consumer Group Id](http://kafka.apache.org/documentation/#group.id)。
> 在 Azure Service Bus 中映射到 Subscription Name。
!!! info "Mapping"
在 RabbitMQ 中映射到 [Queue Names](https://www.rabbitmq.com/queues.html#names)。
在 Apache Kafka 中映射到 [Consumer Group Id](http://kafka.apache.org/documentation/#group.id)。
在 Azure Service Bus 中映射到 Subscription Name。
在 NATS 中映射到 [Queue Group Name](https://docs.nats.io/nats-concepts/queue).
在 Redis Streams 中映射到 [Consumer Group](https://redis.io/topics/streams-intro#creating-a-consumer-group).


#### GroupNamePrefix

默认值:Null

为订阅 Group 统一添加前缀。 https://github.com/dotnetcore/CAP/pull/780

#### TopicNamePrefix

默认值: Null

为 Topic 统一添加前缀。 https://github.com/dotnetcore/CAP/pull/780

#### Version

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

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

#### CollectorCleaningInterval

默认值:300 秒

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

#### FailedRetryCount

默认值:50
@@ -86,12 +108,7 @@ services.AddCap(config =>

默认值:NULL

类型:`Action<MessageType, string, string>`

>
T1 : Message Type
T2 : Message Name
T3 : Message Content
类型:`Action<FailedInfo>`

重试阈值的失败回调。当重试达到 FailedRetryCount 设置的值的时候,将调用此 Action 回调,你可以通过指定此回调来接收失败达到最大的通知,以做出人工介入。例如发送邮件或者短信。



+ 53
- 0
docs/content/user-guide/zh/cap/filter.md 查看文件

@@ -0,0 +1,53 @@
# 过滤器

在 5.1.0 版本中,我们支持了在订阅者中添加过滤器。在过去,我们通过对第三方 AOP 组件提供支持来做到这一点,例如我们写了一篇[博客](https://www.cnblogs.com/savorboard/p/cap-castle.html) 来描述如何在 CAP 5.0 版本中使用 Castle 来对订阅方法进行拦截,但了这种方式存在一些缺点,例如无法方便的在代理类中进行构造函数注入以及方法需要设定为 virtual 另外还有拦截器生命周期控制等问题。

所以我们引入了对订阅者过滤器的支持,以使在某些场景(如事务处理,日志记录等)中变得容易。

## 自定义过滤器

### 添加过滤器

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

```C#
public class MyCapFilter: SubscribeFilter
{
public override void OnSubscribeExecuting(ExecutingContext context)
{
// 订阅方法执行前
}

public override void OnSubscribeExecuted(ExecutedContext context)
{
// 订阅方法执行后
}

public override void OnSubscribeException(ExceptionContext context)
{
// 订阅方法执行异常
}
}
```

在一些场景中,如果想终止订阅者方法执行,可以在 `OnSubscribeExecuting` 中抛出异常,并且在 `OnSubscribeException` 中选择忽略该异常。

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

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

### 配置过滤器

```C#
services.AddCap(opt =>
{
// ***
}.AddSubscribeFilter<MyCapFilter>();
```

目前, 我们还不支持同时添加多个过滤器。

+ 51
- 3
docs/content/user-guide/zh/cap/messaging.md 查看文件

@@ -6,6 +6,54 @@

你可以阅读 [quick-start](../getting-started/quick-start.md#_3) 来学习如何发送和处理消息。

## 补偿事务

[Compensating transaction](https://en.wikipedia.org/wiki/Compensating_transaction)

某些情况下,消费者需要返回值以告诉发布者执行结果,以便于发布者实施一些动作,通常情况下这属于补偿范围。

你可以在消费者执行的代码中通过重新发布一个新消息来通知上游,CAP 提供了一种简单的方式来做到这一点。 你可以在发送的时候指定 `callbackName` 来得到消费者的执行结果,通常这仅适用于点对点的消费。以下是一个示例。

例如,在一个电商程序中,订单初始状态为 pending,当商品数量成功扣除时将状态标记为 succeeded ,否则为 failed。

```C#
// ============= Publisher =================

_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(JsonElement param)
{
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
}
}

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

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

//business logic

return new { OrderId = orderId, IsSuccess = true };
}
```

## 异构系统集成

在 3.0+ 版本中,我们对消息结构进行了重构,我们利用了消息队列中消息协议中的 Header 来传输一些额外信息,以便于在 Body 中我们可以做到不需要修改或包装使用者的原始消息数据格式和内容进行发送。
@@ -45,7 +93,6 @@ channel.basicPublish(exchangeName, routingKey,

```


## 消息调度

CAP 接收到消息之后会将消息发送到 Transport, 由 Transport 进行运输。
@@ -66,7 +113,7 @@ CAP 接收到消息之后会将消息进行 Persistent(持久化), 有关

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

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

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

@@ -78,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) 配置项来自定义间隔时间。


+ 0
- 7
docs/content/user-guide/zh/cap/sagas.md 查看文件

@@ -1,7 +0,0 @@
# Sagas

Sagas (also known in the literature as "process managers") are stateful services. You can think of them as state machines whose transitions are driven by messages.

## Sagas on CAP

TODO

+ 2
- 0
docs/content/user-guide/zh/getting-started/introduction.md 查看文件

@@ -25,6 +25,8 @@ CAP 采用模块化设计,具有高度的可扩展性。你有许多选项可

[Article: CAP 介绍及使用](http://www.cnblogs.com/savorboard/p/cap.html)

[Article: CAP 5.0 版本中的新特性](https://www.cnblogs.com/savorboard/p/cap-5-0.html)

[Article: CAP 3.0 版本中的新特性](https://www.cnblogs.com/savorboard/p/cap-3-0.html)

[Article: CAP 2.6 版本中的新特性](https://www.cnblogs.com/savorboard/p/cap-2-6.html)


+ 1
- 1
docs/content/user-guide/zh/getting-started/quick-start.md 查看文件

@@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services)
public class PublishController : Controller
{
[Route("~/send")]
public IActionResult SendMessage([FromService]ICapPublisher capBus)
public IActionResult SendMessage([FromServices]ICapPublisher capBus)
{
capBus.Publish("test.show.time", DateTime.Now);



+ 55
- 21
docs/content/user-guide/zh/monitoring/dashboard.md 查看文件

@@ -26,6 +26,12 @@ services.AddCap(x =>

### Dashboard 配置项

* PathBase

默认值:N/A

当位于代理后时,通过配置此参数可以指定代理请求前缀。

* PathMatch

默认值:'/cap'
@@ -38,39 +44,67 @@ services.AddCap(x =>

此配置项用来配置Dashboard 前端 获取状态接口(/stats)的轮询时间

* Authorization
* UseAuth

默认值:false

指定是否开启授权

* DefaultAuthenticationScheme

授权默认使用的 Scheme

* UseChallengeOnAuth

默认值:false

授权是否启用 Challenge

* DefaultChallengeScheme

Challenge 默认使用的 Scheme

此配置项用来配置访问 Dashboard 时的授权过滤器,默认过滤器允许局域网访问,当你的应用想提供外网访问时候,可以通过设置此配置来自定义认证规则。详细参看下一节

### 自定义认证
自 5.1.0 开始,CAP Dashboard 授权默认使用 ASP.NET Core 的方式,不再提供自定义授权过滤器。

通过实现 `IDashboardAuthorizationFilter` 接口可以自定义Dashboard认证。
在 Dashabord 认证时,会从 HttpContext.User?.Identity?.IsAuthenticated 中取值,如果取不到则认证失败,并调用 Challenge Scheme(如进行配置)

以下是一个示例代码,通过从url请求参数中读取 accesskey 判断是否允许访问。
你可以在 Sample.Dashboard.Auth 这个示例项目中查看使用细节

```C#
public class TestAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
services
.AddAuthorization()
.AddAuthentication(options =>
{
if(context.Request.GetQuery("accesskey")=="xxxxxx"){
return true;
}
return false;
}
}
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.UsePkce = true;

options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
})
```

然后在修改注册 Dashboard 时候配置此过滤对象。
配置

```C#
services.AddCap(x =>
services.AddCap(cap =>
{
//...

// Register Dashboard
x.UseDashboard(opt => {
opt.Authorization = new[] {new TestAuthorizationFilter()};
cap.UseDashboard(d =>
{
d.UseChallengeOnAuth = true;
d.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
});
});
}
```

+ 0
- 3
docs/content/user-guide/zh/monitoring/health-checks.md 查看文件

@@ -1,3 +0,0 @@
# 健康检查

TODO

+ 123
- 0
docs/content/user-guide/zh/samples/castle.dynamicproxy.md 查看文件

@@ -0,0 +1,123 @@
# 和 Castle DynamicProxy 集成

Castle DynamicProxy 是一个用于在运行时动态生成轻量级.NET代理的库。代理对象允许在不修改类代码的情况下截取对对象成员的调用。可以代理类和接口,但是只能拦截虚拟成员。

Castle.DynamicProxy 可以帮助你方便的创建代理对象,代理对象可以帮助构建灵活的应用程序体系结构,因为它允许将功能透明地添加到代码中,而无需对其进行修改。例如,可以代理一个类来添加日志记录或安全检查,而无需使代码知道已添加此功能。

下面可以看到如何在 CAP 中集成使用 Castle.DynamicProxy。


## 1、安装 NuGet 包

在 集成了 CAP 的项目中安装包,有关如何集成 CAP 的文档请看[这里](https://cap.dotnetcore.xyz/)。

注意,`Castle.DynamicProxy` 这个包已经被废弃,请使用最新的 `Castle.Core` 包。

```xml
<PackageReference Include="Castle.Core" Version="4.4.1" />
```

## 2、创建一个 Castle 切面拦截器

可以在这里 [dynamicproxy.md](https://github.com/castleproject/Core/blob/master/docs/dynamicproxy.md) 找到相关的文档。

下面为示例代码,继承 Castle 提供的 `IInterceptor` 接口即可:

```
[Serializable]
public class MyInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("Before target call");
try
{
invocation.Proceed();
}
catch (Exception)
{
Console.WriteLine("Target threw an exception!");
throw;
}
finally
{
Console.WriteLine("After target call");
}
}
}

```

拦截器此处命名为 `MyInterceptor`,你可以在其中处理你的业务逻辑,比如添加日志或其他的一些行为。

## 3、创建 IServiceCollection 的扩展类

为 `IServiceCollection` 创建扩展,方面后续调用。

```csharp
using Castle.DynamicProxy;

public static class ServicesExtensions
{
public static void AddProxiedSingleton<TImplementation>(this IServiceCollection services)
where TImplementation : class
{
services.AddSingleton(serviceProvider =>
{
var proxyGenerator = serviceProvider.GetRequiredService<ProxyGenerator>();
var interceptors = serviceProvider.GetServices<IInterceptor>().ToArray();
return proxyGenerator.CreateClassProxy<TImplementation>(interceptors);
});
}
}
```

此处我创建了一个 Singleton 声明周期的扩展方法,建议所有 CAP 的订阅者都创建为 Singleton 即可,因为在 CAP 内部实际执行的时候也会创建一个 scope 来执行,所以无需担心资源释放问题。


## 4、创建 CAP 订阅服务

创建一个 CAP 订阅类,注意不能放在 Controller 中了。

**注意:方法需要为虚方法 virtual,才能被 Castle 重写,别搞忘了加!!!**

```cs
public class CapSubscribeService: ICapSubscribe
{
[CapSubscribe("sample.rabbitmq.mysql")]
public virtual void Subscriber(DateTime p)
{
Console.WriteLine($@"{DateTime.Now} Subscriber invoked, Info: {p}");
}
}
```

## 5、在 Startup 中集成

```cs
public void ConfigureServices(IServiceCollection services)
{
// 添加 Castle 的代理生成器
services.AddSingleton(new ProxyGenerator());
// 添加第2步的自定义的拦截类,声明周期为
services.AddSingleton<IInterceptor, MyInterceptor>();
// 此处为上面的扩展方法, 添加 CAP 订阅 Service
services.AddProxiedSingleton<CapSubscribeService>();
services.AddCap(x =>
{
x.UseMySql("");
x.UseRabbitMQ("");
x.UseDashboard();
});
// ...
}
```

以上就完成了所有的集成工作,可以开始进行测试了,有问题欢迎到 [Github issue](https://github.com/dotnetcore/CAP/issues) 反馈。


**注意: CAP 需要使用 5.0 + 版本,目前(2021年1月6日)只有 preview 版本。**

+ 10
- 10
docs/content/user-guide/zh/samples/faq.md 查看文件

@@ -2,19 +2,19 @@

!!! faq "Any IM group(e.g Tencent QQ group) to learn and chat about CAP?"

None for that. Better than wasting much time in IM group, I hope developers could be capable of independent thinking more, and solve problems yourselves with referenced documents, even create issues or send emails when errors are remaining present.
None of that. Better than wasting much time in IM group, I hope developers could be capable of independent thinking more, and solve problems yourselves with referenced documents, even create issues or send emails when errors are remaining present.

!!! faq "Does it require certain different databases, one each for productor and resumer in CAP?"
!!! faq "Does it require different databases, one each for producer and consumer in CAP?"

Not requird differences necessary, a given advice is that using a special database for each program.
No difference necessary, a recommendation is to use a dedicated database for each program.

Otherwise, look at Q&A below.

!!! faq "How to use the same database for different applications?"
defining a prefix name of table in `ConfigureServices` method。
!!! faq "How to use the same database for different applications? (Only for MySQL)"
Define a table prefix name in `ConfigureServices` method.
codes exsample:
Code example:

```c#
public void ConfigureServices(IServiceCollection services)
@@ -31,14 +31,14 @@
}
```

!!! faq "Can CAP not use the database as event storage? I just want to sent the message"
!!! faq "Can CAP not use the database as event storage? I just want to send the message"

Not yet.

The purpose of CAP is that ensure consistency principle right in microservice or SOA architechtrues. The solution is based on ACID features of database, there is no sense about a single client wapper of message queue without database.
The purpose of CAP is that ensure consistency principle right in microservice or SOA architectures. The solution is based on ACID features of database, there is no sense about a single client wapper of message queue without database.

!!! faq "If the consumer is abnormal, can I roll back the database executed sql that the producer has executed?"

Can't roll back, CAP is the ultimate consistency solution.

You can imagine your scenario is to call a third party payment. If you are doing a third-party payment operation, after calling Alipay's interface successfully, and your own code is wrong, will Alipay roll back? If you don't roll back, what should you do? The same is true here.
You can imagine your scenario is to call a third party payment. If you are doing a third-party payment operation, after calling Alipay's interface successfully, and your own code is wrong, will Alipay roll back? If you don't roll back, what should you do? The same is true here.

+ 7
- 1
docs/content/user-guide/zh/storage/general.md 查看文件

@@ -68,4 +68,10 @@ CallbackName | 回调的订阅者名称 | string

感谢社区对CAP的支持,以下是社区支持的持久化的实现

* SQLite ([@colinin](https://github.com/colinin)) : https://github.com/colinin/DotNetCore.CAP.Sqlite
* SQLite ([@colinin](https://github.com/colinin)) :https://github.com/colinin/DotNetCore.CAP.Sqlite

* LiteDB ([@maikebing](https://github.com/maikebing)) :https://github.com/maikebing/CAP.Extensions

* SQLite & Oracle ([@cocosip](https://github.com/cocosip)) :https://github.com/cocosip/CAP-Extensions

* SmartSql ([@xiangxiren](https://github.com/xiangxiren)) :https://github.com/xiangxiren/SmartSql.CAP

+ 1
- 1
docs/content/user-guide/zh/storage/sqlserver.md 查看文件

@@ -25,7 +25,7 @@ public void ConfigureServices(IServiceCollection services)

services.AddCap(x =>
{
x.UsePostgreSql(opt=>{
x.UseSqlServer(opt=>{
//SqlServerOptions
});
// x.UseXXX ...


+ 19
- 0
docs/content/user-guide/zh/transport/azure-service-bus.md 查看文件

@@ -45,4 +45,23 @@ NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
ConnectionString | Endpoint 地址 | string |
TopicPath | Topic entity path | string | cap
EnableSessions | 启用 [Service bus sessions](https://docs.microsoft.com/en-us/azure/service-bus-messaging/message-sessions) | bool | false
ManagementTokenProvider | Token提供 | ITokenProvider | null

#### Sessions

当使用 `EnableSessions` 选项启用 sessions 后,每个发送的消息都会具有一个 session id。 要控制 seesion id 你可以在发送消息时在消息头中使用 `AzureServiceBusHeaders.SessionId` 携带它。


```csharp
ICapPublisher capBus = ...;
string yourEventName = ...;
YourEventType yourEvent = ...;

Dictionary<string, string> extraHeaders = new Dictionary<string, string>();
extraHeaders.Add(AzureServiceBusHeaders.SessionId, <your-session-id>);

capBus.Publish(yourEventName, yourEvent, extraHeaders);
```

如果头中没有 session id , 那么消息 Id 仍然使用的 Message Id.

+ 14
- 2
docs/content/user-guide/zh/transport/general.md 查看文件

@@ -10,12 +10,14 @@ 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)

## 怎么选择运输器

🏳‍🌈 | RabbitMQ | Kafka | Azure Service Bus | In-Memory
:-- | :--: | :--: | :--: | :-- :
:-- | :--: | :--: | :--: | :--:
**定位** | 可靠消息传输 | 实时数据处理 | 云 | 内存型,测试
**分布式** | ✔ | ✔ | ✔ |❌
**持久化** | ✔ | ✔ | ✔ | ❌
@@ -26,4 +28,14 @@ CAP 支持以下几种运输方式:
> http://geekswithblogs.net/michaelstephenson/archive/2012/08/12/150399.aspx

>`Kafka` vs `RabbitMQ` :
> https://stackoverflow.com/questions/42151544/is-there-any-reason-to-use-rabbitmq-over-kafka
> https://stackoverflow.com/questions/42151544/is-there-any-reason-to-use-rabbitmq-over-kafka

## 社区支持的运输器

感谢社区对CAP的支持,以下是社区支持的运输器实现

* ActiveMQ (@[Lukas Zhang](https://github.com/lukazh/Lukaz.CAP.ActiveMQ)): https://github.com/lukazh

* RedisMQ ([@木木](https://github.com/difudotnet)): https://github.com/difudotnet/CAP.RedisMQ.Extensions

* ZeroMQ ([@maikebing](https://github.com/maikebing)): https://github.com/maikebing/CAP.Extensions/tree/master/src/DotNetCore.CAP.ZeroMQ

+ 61
- 0
docs/content/user-guide/zh/transport/nats.md 查看文件

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

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

!!! warning
我们当前基于 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)找到更多详细信息。

+ 24
- 2
docs/content/user-guide/zh/transport/rabbitmq.md 查看文件

@@ -45,8 +45,10 @@ UserName | 用户名 | string | guest
Password | 密码 | string | guest
VirtualHost | 虚拟主机 | string | /
Port | 端口号 | int | -1
TopicExchangeName | CAP默认Exchange名称 | string | cap.default.topic
QueueMessageExpires | 队列中消息自动删除时间 | int | (10天) 毫秒
ExchangeName | CAP默认Exchange名称 | string | cap.default.topic
QueueArguments | 创建队列额外参数 x-arguments | QueueArgumentsOptions | N/A
ConnectionFactoryOptions | RabbitMQClient原生参数 | ConnectionFactory | N/A
CustomHeaders | 订阅者自定义头信息 | Func<BasicDeliverEventArgs, List<KeyValuePair<string, string>>> | N/A

#### ConnectionFactory Options

@@ -67,6 +69,26 @@ services.AddCap(x =>

```

#### CustomHeaders Options

当需要从异构系统或者直接接收从RabbitMQ 控制台发送的消息时,由于 CAP 需要定义额外的头信息才能正常订阅,所以此时会出现异常。通过提供此参数来进行自定义头信息的设置来使订阅者正常工作。

你可以在这里找到有关 [头信息](../cap/messaging#异构系统集成) 的说明。

用法如下:

```cs
x.UseRabbitMQ(aa =>
{
aa.CustomHeaders = e => new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(Headers.MessageId, SnowflakeId.Default().NextId().ToString()),
new KeyValuePair<string, string>(Headers.MessageName, e.RoutingKey),
};
});
```


#### 如何连接 RabbitMQ 集群?

使用逗号分隔连接字符串即可,如下:


+ 58
- 0
docs/content/user-guide/zh/transport/redis-streams.md 查看文件

@@ -0,0 +1,58 @@
# Redis Streams

[Redis](https://redis.io/) 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

[Redis Stream](https://redis.io/topics/streams-intro) 是 Redis 5.0 引入的一种新数据类型,它用一种仅附加的数据结构以更抽象的方式模拟日志数据结构。

Redis Streams 可以在 CAP 中用作消息传输器。

## 配置

要使用 Redis Streams 传输器,您需要从 NuGet 安装以下包:

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

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

```csharp

public void ConfigureServices(IServiceCollection services)
{
services.AddCap(capOptions =>
{
capOptions.UseRedis(redisOptions=>{
//redisOptions
});
});
}

```

#### Redis Streams Options

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

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Configuration | redis连接配置 (StackExchange.Redis) | ConfigurationOptions | ConfigurationOptions
StreamEntriesCount | 读取时从 stream 返回的条目数 | uint | 10
ConnectionPoolSize | 连接池数 | uint | 10

#### Redis ConfigurationOptions

如果需要**更多**原生Redis相关配置选项,您可以在 `Configuration` 选项中进行设置 :

```csharp
services.AddCap(capOptions =>
{
capOptions.UseRedis(redisOptions=>
{
// redis options.
redisOptions.Configuration.EndPoints.Add(IPAddress.Loopback, 0);
});
});
```

`Configuration` 是 StackExchange.Redis ConfigurationOptions ,您可以通过此[链接](https://stackexchange.github.io/StackExchange.Redis/Configuration)找到更多详细信息。

+ 31
- 13
docs/mkdocs.yml 查看文件

@@ -10,7 +10,7 @@ edit_uri: 'edit/master/docs/content'
docs_dir: 'content'

# Copyright
copyright: Copyright &copy; 2017 <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
@@ -19,18 +19,26 @@ theme:
palette:
primary: 'deep purple'
accent: 'indigo'
language: 'en'
language: en
include_sidebar: true
logo: 'img/logo.svg'
favicon: 'img/favicon.ico'
features:
- tabs
- navigation.tabs
- navigation.instant
i18n:
prev: 'Previous'
next: 'Next'

#Customization
extra:
alternate:
- name: English
link: /user-guide/en/getting-started/quick-start
lang: en
- name: 中文
link: /user-guide/zh/getting-started/quick-start
lang: zh
social:
- icon: 'fontawesome/brands/github'
link: 'https://github.com/dotnetcore/CAP'
@@ -81,16 +89,18 @@ nav:
- CAP:
- Configuration: user-guide/en/cap/configuration.md
- Messaging: user-guide/en/cap/messaging.md
- Sagas: user-guide/en/cap/sagas.md
- Filter: user-guide/en/cap/filter.md
- Serialization: user-guide/en/cap/serialization.md
- Transactions: user-guide/en/cap/transactions.md
- Idempotence: user-guide/en/cap/idempotence.md
- Transport:
- General: user-guide/en/transport/general.md
- RabbitMQ: user-guide/en/transport/rabbitmq.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
- Azure Service Bus: user-guide/en/transport/azure-service-bus.md
- Amazon SQS: user-guide/en/transport/aws-sqs.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
- Storage:
- General: user-guide/en/storage/general.md
@@ -115,16 +125,18 @@ nav:
- CAP:
- 配置: user-guide/zh/cap/configuration.md
- 消息: user-guide/zh/cap/messaging.md
- Sagas: user-guide/zh/cap/sagas.md
- 过滤器: user-guide/zh/cap/filter.md
- 序列化: user-guide/zh/cap/serialization.md
- 运输: user-guide/zh/cap/transactions.md
- 事务: user-guide/zh/cap/transactions.md
- 幂等性: user-guide/zh/cap/idempotence.md
- 传输:
- 简介: user-guide/zh/transport/general.md
- RabbitMQ: user-guide/zh/transport/rabbitmq.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
- Azure Service Bus: user-guide/zh/transport/azure-service-bus.md
- Amazon SQS: user-guide/zh/transport/aws-sqs.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
- 存储:
- 简介: user-guide/zh/storage/general.md
@@ -139,6 +151,7 @@ nav:
- 性能追踪: user-guide/zh/monitoring/diagnostics.md
- 健康检查: user-guide/zh/monitoring/health-checks.md
- 示例:
- Castle DynamicProxy: user-guide/zh/samples/castle.dynamicproxy.md
- Github: user-guide/zh/samples/github.md
- eShopOnContainers: user-guide/zh/samples/eshoponcontainers.md
- FAQ: user-guide/zh/samples/faq.md
@@ -146,3 +159,8 @@ nav:
- Contact Us: about/contact-us.md
- Release Notes: about/release-notes.md
- License: about/license.md

# Google Analytics
google_analytics:
- !!python/object/apply:os.getenv ["GOOGLE_ANALYTICS_KEY"]
- auto

+ 1
- 1
samples/Sample.AmazonSQS.InMemory/Sample.AmazonSQS.InMemory.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>


+ 1
- 1
samples/Sample.AmazonSQS.InMemory/appsettings.json 查看文件

@@ -2,7 +2,7 @@
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Error"
"Default": "Information"
}
}
}

+ 2
- 2
samples/Sample.ConsoleApp/Program.cs 查看文件

@@ -16,7 +16,7 @@ namespace Sample.ConsoleApp
{
//console app does not support dashboard

x.UseMySql("Server=192.168.3.57;Port=3307;Database=captest;Uid=root;Pwd=123123;");
x.UseMySql("<ConnectionString>");
x.UseRabbitMQ(z =>
{
z.HostName = "192.168.3.57";
@@ -29,7 +29,7 @@ namespace Sample.ConsoleApp

var sp = container.BuildServiceProvider();

sp.GetService<IBootstrapper>().BootstrapAsync(default);
sp.GetService<IBootstrapper>().BootstrapAsync();

Console.ReadLine();
}


+ 2
- 2
samples/Sample.ConsoleApp/Sample.ConsoleApp.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

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


+ 49
- 0
samples/Sample.Dashboard.Auth/Controllers/ValuesController.cs 查看文件

@@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace Sample.Dashboard.Auth.Controllers
{
[Authorize]
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly ICapPublisher _capBus;
private readonly ILogger<ValuesController> _logger;
private const string MyTopic = "sample.dashboard.auth";

public ValuesController(ICapPublisher capPublisher, ILogger<ValuesController> logger)
{
_capBus = capPublisher;
_logger = logger;
}

[Route("publish")]
public async Task<IActionResult> Publish()
{
await _capBus.PublishAsync(MyTopic, new Person()
{
Id = new Random().Next(1, 100),
Name = "Bar"
});

return Ok();
}

[NonAction]
[CapSubscribe(MyTopic)]
public void Subscribe(Person p, [FromCap] CapHeader header)
{
_logger.LogInformation("Subscribe Invoked: " + MyTopic + p);
}

public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
}
}

+ 50
- 0
samples/Sample.Dashboard.Auth/MyDashboardAuthenticationHandler.cs 查看文件

@@ -0,0 +1,50 @@
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Sample.Dashboard.Auth
{
public class MyDashboardAuthenticationSchemeOptions : AuthenticationSchemeOptions
{

}

public class MyDashboardAuthenticationHandler : AuthenticationHandler<MyDashboardAuthenticationSchemeOptions>
{
public MyDashboardAuthenticationHandler(IOptionsMonitor<MyDashboardAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
options.CurrentValue.ForwardChallenge = "";
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var testAuthHeaderPresent = Request.Headers["X-Base-Token"].Contains("xxx");

var authResult = testAuthHeaderPresent ? AuthenticatedTestUser() : AuthenticateResult.NoResult();

return Task.FromResult(authResult);
}

protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = "MyDashboardScheme";

return base.HandleChallengeAsync(properties);
}

private AuthenticateResult AuthenticatedTestUser()
{
var claims = new[] { new Claim(ClaimTypes.Name, "My Dashboard user") };
var identity = new ClaimsIdentity(claims, "MyDashboardScheme");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "MyDashboardScheme");

return AuthenticateResult.Success(ticket);
}
}
}

+ 17
- 0
samples/Sample.Dashboard.Auth/Program.cs 查看文件

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

namespace Sample.Dashboard.Auth
{
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>(); });
}
}

+ 18
- 0
samples/Sample.Dashboard.Auth/Sample.Dashboard.Auth.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>net5.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" />
</ItemGroup>
</Project>

+ 94
- 0
samples/Sample.Dashboard.Auth/Startup.cs 查看文件

@@ -0,0 +1,94 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Sample.Dashboard.Auth
{
public class Startup
{
private readonly IConfiguration _configuration;

public Startup(IConfiguration configuration)
{
_configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthorization()
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.UsePkce = true;

options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
})
.AddScheme<MyDashboardAuthenticationSchemeOptions, MyDashboardAuthenticationHandler>("MyDashboardScheme",null);

services.AddCors(x =>
{
x.AddDefaultPolicy(p =>
{
p.WithOrigins("http://localhost:8080").AllowCredentials().AllowAnyHeader().AllowAnyMethod();
});
});

services.AddCap(cap =>
{
cap.UseDashboard(d =>
{
d.UseChallengeOnAuth = true;
d.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
d.UseAuth = true;
d.DefaultAuthenticationScheme = "MyDashboardScheme";
});
cap.UseMySql(_configuration.GetValue<string>("ConnectionString"));
cap.UseRabbitMQ(aa =>
{
aa.HostName = "192.168.3.57";
aa.UserName = "user";
aa.Password = "wJ0p5gSs17";
});
//cap.UseDiscovery(_ =>
//{
// _.DiscoveryServerHostName = "localhost";
// _.DiscoveryServerPort = 8500;
// _.CurrentNodeHostName = _configuration.GetValue<string>("ASPNETCORE_HOSTNAME");
// _.CurrentNodePort = _configuration.GetValue<int>("ASPNETCORE_PORT");
// _.NodeId = _configuration.GetValue<string>("NodeId");
// _.NodeName = _configuration.GetValue<string>("NodeName");
//});
});

services.AddControllers();
}

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

}

+ 13
- 0
samples/Sample.Dashboard.Auth/appsettings.json 查看文件

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Postgres": "Server=127.0.0.1;Port=5432;Database=cap;Uid=postgres;Pwd=root;Include Error Detail=true;"
}
}

+ 2
- 2
samples/Sample.Kafka.PostgreSql/Sample.Kafka.PostgreSql.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<WarningsAsErrors>NU1701</WarningsAsErrors>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="Dapper" Version="2.0.78" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />


+ 1
- 1
samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 1
- 1
samples/Sample.RabbitMQ.MongoDB/Startup.cs 查看文件

@@ -20,7 +20,7 @@ namespace Sample.RabbitMQ.MongoDB
services.AddCap(x =>
{
x.UseMongoDB(Configuration.GetConnectionString("MongoDB"));
x.UseRabbitMQ("192.168.2.120");
x.UseRabbitMQ("");
x.UseDashboard();
});
services.AddControllers();


+ 2
- 2
samples/Sample.RabbitMQ.MySql/AppDbContext.cs 查看文件

@@ -26,13 +26,13 @@ namespace Sample.RabbitMQ.MySql
}
public class AppDbContext : DbContext
{
public const string ConnectionString = "Server=localhost;Database=testcap;UserId=root;Password=123123;";
public const string ConnectionString = "";

public DbSet<Person> Persons { get; set; }

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

+ 3
- 3
samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
<PackageReference Include="Dapper" Version="2.0.78" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0-alpha.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" />


+ 2
- 14
samples/Sample.RabbitMQ.SqlServer/AppDbContext.cs 查看文件

@@ -12,23 +12,11 @@ namespace Sample.RabbitMQ.SqlServer
{
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 = "Server=192.168.2.120;Database=captest;User Id=sa;Password=P@ssw0rd;";
public const string ConnectionString = "";

public DbSet<Person> Persons { get; set; }



+ 18
- 8
samples/Sample.RabbitMQ.SqlServer/Controllers/ValuesController.cs 查看文件

@@ -1,8 +1,8 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP;
using DotNetCore.CAP.Messages;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.SqlClient;

@@ -21,7 +21,7 @@ namespace Sample.RabbitMQ.SqlServer.Controllers
[Route("~/without/transaction")]
public async Task<IActionResult> WithoutTransaction()
{
await _capBus.PublishAsync("sample.rabbitmq.mysql", new Person()
await _capBus.PublishAsync("sample.rabbitmq.sqlserver", new Person()
{
Id = 123,
Name = "Bar"
@@ -40,7 +40,11 @@ namespace Sample.RabbitMQ.SqlServer.Controllers
//your business code
connection.Execute("insert into test(name) values('test')", transaction: transaction);

_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
_capBus.Publish("sample.rabbitmq.sqlserver", new Person()
{
Id = 123,
Name = "Bar"
});
}
}

@@ -54,22 +58,28 @@ namespace Sample.RabbitMQ.SqlServer.Controllers
{
dbContext.Persons.Add(new Person() { Name = "ef.transaction" });

_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
_capBus.Publish("sample.rabbitmq.sqlserver", new Person()
{
Id = 123,
Name = "Bar"
});
}
return Ok();
}

[NonAction]
[CapSubscribe("sample.rabbitmq.mysql")]
public void Subscriber(DateTime p)
[CapSubscribe("sample.rabbitmq.sqlserver")]
public void Subscriber(Person p)
{
Console.WriteLine($@"{DateTime.Now} Subscriber invoked, Info: {p}");
}

[NonAction]
[CapSubscribe("sample.rabbitmq.mysql", Group = "group.test2")]
public void Subscriber2(DateTime p, [FromCap]CapHeader header)
[CapSubscribe("sample.rabbitmq.sqlserver", Group = "group.test2")]
public void Subscriber2(Person p, [FromCap]CapHeader header)
{
var id = header[Headers.MessageId];

Console.WriteLine($@"{DateTime.Now} Subscriber invoked, Info: {p}");
}
}


+ 4
- 4
samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5">
<PackageReference Include="Dapper" Version="2.0.78" />
<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="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.4" />
</ItemGroup>
<ItemGroup>


+ 4
- 1
samples/Sample.RabbitMQ.SqlServer/Startup.cs 查看文件

@@ -1,4 +1,6 @@
using System;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using DotNetCore.CAP.Messages;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@@ -15,7 +17,7 @@ namespace Sample.RabbitMQ.SqlServer
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>();
x.UseRabbitMQ("192.168.2.120");
x.UseRabbitMQ("");
x.UseDashboard();
x.FailedRetryCount = 5;
x.FailedThresholdCallback = failed =>
@@ -24,6 +26,7 @@ namespace Sample.RabbitMQ.SqlServer
logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times,
requiring manual troubleshooting. Message name: {failed.Message.GetName()}");
};
x.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});

services.AddControllers();


+ 54
- 0
samples/Samples.Redis.SqlServer/Controllers/HomeController.cs 查看文件

@@ -0,0 +1,54 @@
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
{
[ApiController]
[Route("[controller]/[action]")]
public class HomeController : ControllerBase
{
private readonly ILogger<HomeController> _logger;
private readonly ICapPublisher _publisher;
private readonly IOptions<CapOptions> _options;

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

[HttpGet]
public async Task Publish([FromQuery] string message = "test-message")
{
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, [FromCap] CapHeader header)
{
_logger.LogInformation($"{header[Headers.MessageName]} subscribed with value --> " + p);
}

}

public class Person
{
public string Name { get; set; }

public int Age { get; set; }

public override string ToString()
{
return "Name:" + Name + ", Age:" + Age;
}
}
}

+ 25
- 0
samples/Samples.Redis.SqlServer/Dockerfile 查看文件

@@ -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"]

+ 20
- 0
samples/Samples.Redis.SqlServer/Program.cs 查看文件

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

namespace Samples.Redis.SqlServer
{
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/Samples.Redis.SqlServer/Samples.Redis.SqlServer.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.RedisStreams\DotNetCore.CAP.RedisStreams.csproj" />
<ProjectReference Include="..\..\src\DotNetCore.CAP.SqlServer\DotNetCore.CAP.SqlServer.csproj" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>

</Project>

+ 47
- 0
samples/Samples.Redis.SqlServer/Startup.cs 查看文件

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

namespace Samples.Redis.SqlServer
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Samples.Redis.SqlServer", Version = "v1" });
});


services.AddCap(options =>
{
options.UseRedis("redis-node-0:6379,password=cap");

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

public void Configure(IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Samples.Redis.SqlServer v1"));

app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

+ 9
- 0
samples/Samples.Redis.SqlServer/appsettings.Development.json 查看文件

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

+ 10
- 0
samples/Samples.Redis.SqlServer/appsettings.json 查看文件

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

+ 90
- 0
samples/Samples.Redis.SqlServer/docker-compose.yml 查看文件

@@ -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

+ 1
- 1
src/Directory.Build.props 查看文件

@@ -26,7 +26,7 @@

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

</Project>

+ 255
- 0
src/DotNetCore.CAP.AmazonSQS/AmazonPolicyExtensions.cs 查看文件

@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Amazon.Auth.AccessControlPolicy;
using Amazon.Auth.AccessControlPolicy.ActionIdentifiers;

namespace DotNetCore.CAP.AmazonSQS
{
public static class AmazonPolicyExtensions
{
/// <summary>
/// Check to see if the policy for the queue has already given permission to the topic.
/// </summary>
/// <param name="policy"></param>
/// <param name="topicArn"></param>
/// <param name="sqsQueueArn"></param>
/// <returns></returns>
public static bool HasSqsPermission(this Policy policy, string topicArn, string sqsQueueArn)
{
foreach (var statement in policy.Statements)
{
var containsResource = statement.Resources.Any(r => r.Id.Equals(sqsQueueArn));

if (!containsResource)
{
continue;
}

foreach (var condition in statement.Conditions)
{
if ((string.Equals(condition.Type, ConditionFactory.StringComparisonType.StringLike.ToString(), StringComparison.OrdinalIgnoreCase) ||
string.Equals(condition.Type, ConditionFactory.StringComparisonType.StringEquals.ToString(), StringComparison.OrdinalIgnoreCase) ||
string.Equals(condition.Type, ConditionFactory.ArnComparisonType.ArnEquals.ToString(), StringComparison.OrdinalIgnoreCase) ||
string.Equals(condition.Type, ConditionFactory.ArnComparisonType.ArnLike.ToString(), StringComparison.OrdinalIgnoreCase)) &&
string.Equals(condition.ConditionKey, ConditionFactory.SOURCE_ARN_CONDITION_KEY, StringComparison.OrdinalIgnoreCase) &&
condition.Values.Contains(topicArn))
{
return true;
}
}
}

return false;
}

/// <summary>
/// Add statement to the SQS policy that gives the SNS topics access to send a message to the queue.
/// </summary>
/// <code>
/// {
/// "Version": "2012-10-17",
/// "Statement": [
/// {
/// "Effect": "Allow",
/// "Principal": {
/// "AWS": "*"
/// },
/// "Action": "sqs:SendMessage",
/// "Resource": "arn:aws:sqs:us-east-1:MyQueue",
/// "Condition": {
/// "ArnLike": {
/// "aws:SourceArn": [
/// "arn:aws:sns:us-east-1:FirstTopic",
/// "arn:aws:sns:us-east-1:SecondTopic"
/// ]
/// }
/// }
/// }]
/// }
/// </code>
/// <param name="policy"></param>
/// <param name="topicArns"></param>
/// <param name="sqsQueueArn"></param>
public static void AddSqsPermissions(this Policy policy, IEnumerable<string> topicArns, string sqsQueueArn)
{
var statement = new Statement(Statement.StatementEffect.Allow);
#pragma warning disable CS0618 // Type or member is obsolete
statement.Actions.Add(SQSActionIdentifiers.SendMessage);
#pragma warning restore CS0618 // Type or member is obsolete
statement.Resources.Add(new Resource(sqsQueueArn));
statement.Principals.Add(new Principal("*"));
foreach (var topicArn in topicArns)
{
statement.Conditions.Add(ConditionFactory.NewSourceArnCondition(topicArn));
}

policy.Statements.Add(statement);
}

/// <summary>
/// Compact SQS access policy
/// </summary>
/// <para>
/// Transforms policies with multiple similar statements:
/// <code>
/// {
/// "Version": "2012-10-17",
/// "Statement": [
/// {
/// "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:MyQueue-FirstTopic"
/// }
/// }
/// },
/// {
/// "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: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:
/// <code>
/// {
/// "Version": "2012-10-17",
/// "Statement": [
/// {
/// "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:MyQueue-*",
/// "arn:aws:sns:us-east-1:MyQueue2-FirstTopic"
/// ]
/// }
/// }
/// }]
/// }
/// </code>
/// </para>
/// <param name="policy"></param>
/// <param name="sqsQueueArn"></param>
public static void CompactSqsPermissions(this Policy policy, string sqsQueueArn)
{
var statementsToCompact = policy.Statements
.Where(s => s.Effect == Statement.StatementEffect.Allow)
#pragma warning disable CS0618 // Type or member is obsolete
.Where(s => s.Actions.All(a => string.Equals(a.ActionName, SQSActionIdentifiers.SendMessage.ActionName, StringComparison.OrdinalIgnoreCase)))
#pragma warning restore CS0618 // Type or member is obsolete
.Where(s => s.Resources.All(r => string.Equals(r.Id, sqsQueueArn, StringComparison.OrdinalIgnoreCase)))
.Where(s => s.Principals.All(r => string.Equals(r.Id, "*", StringComparison.OrdinalIgnoreCase)))
.ToList();

var groupName = GetGroupName(sqsQueueArn);
if (groupName != null)
{
groupName = $":{groupName}-";
}
if (statementsToCompact.Count < 2 && groupName == null)
{
return;
}

var topicArns = new HashSet<string>();
foreach (var statement in statementsToCompact)
{
policy.Statements.Remove(statement);
foreach (var topicArn in statement.Conditions.SelectMany(c => c.Values))
{
topicArns.Add(
groupName != null && topicArn.Contains(groupName, StringComparison.InvariantCultureIgnoreCase)
? $"{GetArnGroupPrefix(topicArn)}-*"
: topicArn);
}
}

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);
}
}
}

+ 69
- 11
src/DotNetCore.CAP.AmazonSQS/AmazonSQSConsumerClient.cs 查看文件

@@ -5,8 +5,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Auth.AccessControlPolicy;
using Amazon.SimpleNotificationService;
using Amazon.SimpleNotificationService.Model;
using Amazon.SQS;
@@ -14,7 +16,6 @@ using Amazon.SQS.Model;
using DotNetCore.CAP.Messages;
using DotNetCore.CAP.Transport;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Headers = DotNetCore.CAP.Messages.Headers;

namespace DotNetCore.CAP.AmazonSQS
@@ -42,17 +43,17 @@ namespace DotNetCore.CAP.AmazonSQS

public BrokerAddress BrokerAddress => new BrokerAddress("AmazonSQS", _queueUrl);

public void Subscribe(IEnumerable<string> topics)
public ICollection<string> FetchTopics(IEnumerable<string> topicNames)
{
if (topics == null)
if (topicNames == null)
{
throw new ArgumentNullException(nameof(topics));
throw new ArgumentNullException(nameof(topicNames));
}
Connect(initSNS: true, initSQS: false);
var topicArns = new List<string>();
foreach (var topic in topics)
foreach (var topic in topicNames)
{
var createTopicRequest = new CreateTopicRequest(topic.NormalizeForAws());

@@ -60,11 +61,23 @@ namespace DotNetCore.CAP.AmazonSQS

topicArns.Add(createTopicResponse.TopicArn);
}
GenerateSqsAccessPolicyAsync(topicArns)
.GetAwaiter().GetResult();

Connect(initSNS: false, initSQS: true);
return topicArns;
}

_snsClient.SubscribeQueueToTopicsAsync(topicArns, _sqsClient, _queueUrl)
.GetAwaiter().GetResult();
public void Subscribe(IEnumerable<string> topics)
{
if (topics == null)
{
throw new ArgumentNullException(nameof(topics));
}

Connect();

SubscribeToTopics(topics).GetAwaiter().GetResult();
}

public void Listening(TimeSpan timeout, CancellationToken cancellationToken)
@@ -83,7 +96,7 @@ namespace DotNetCore.CAP.AmazonSQS

if (response.Messages.Count == 1)
{
var messageObj = JsonConvert.DeserializeObject<SQSReceivedMessage>(response.Messages[0].Body);
var messageObj = JsonSerializer.Deserialize<SQSReceivedMessage>(response.Messages[0].Body);

var header = messageObj.MessageAttributes.ToDictionary(x => x.Key, x => x.Value.Value);
var body = messageObj.Message;
@@ -207,6 +220,51 @@ namespace DotNetCore.CAP.AmazonSQS
return Task.CompletedTask;
}

private async Task GenerateSqsAccessPolicyAsync(IEnumerable<string> topicArns)
{
Connect(initSNS: false, initSQS: true);

var queueAttributes = await _sqsClient.GetAttributesAsync(_queueUrl).ConfigureAwait(false);

var sqsQueueArn = queueAttributes["QueueArn"];

var policy = queueAttributes.TryGetValue("Policy", out var policyStr) && !string.IsNullOrEmpty(policyStr)
? Policy.FromJson(policyStr)
: new Policy();

var topicArnsToAllow = topicArns
.Where(a => !policy.HasSqsPermission(a, sqsQueueArn))
.ToList();

if (!topicArnsToAllow.Any())
{
return;
}

policy.AddSqsPermissions(topicArnsToAllow, sqsQueueArn);
policy.CompactSqsPermissions(sqsQueueArn);

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
}
}

+ 5
- 5
src/DotNetCore.CAP.AmazonSQS/DotNetCore.CAP.AmazonSQS.csproj 查看文件

@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DotNetCore.CAP.AmazonSQS</AssemblyName>
<PackageTags>$(PackageTags);AmazonSQS;SQS</PackageTags>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.AmazonSQS.xml</DocumentationFile>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.AmazonSQS.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="3.3.101.182" />
<PackageReference Include="AWSSDK.SQS" Version="3.3.102.125" />
<PackageReference Include="AWSSDK.SimpleNotificationService" Version="3.5.1.50" />
<PackageReference Include="AWSSDK.SQS" Version="3.5.1.27" />
</ItemGroup>

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

+ 50
- 17
src/DotNetCore.CAP.AmazonSQS/ITransport.AmazonSQS.cs 查看文件

@@ -37,9 +37,9 @@ namespace DotNetCore.CAP.AmazonSQS
{
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)
@@ -62,12 +62,19 @@ namespace DotNetCore.CAP.AmazonSQS
await _snsClient.PublishAsync(request);

_logger.LogDebug($"SNS topic message [{message.GetName().NormalizeForAws()}] has been published.");
return OperateResult.Success;
}
else
{
_logger.LogWarning($"Can't be found SNS topics for [{message.GetName().NormalizeForAws()}]");
}
return OperateResult.Success;

var errorMessage = $"Can't be found SNS topics for [{message.GetName().NormalizeForAws()}]";
_logger.LogWarning(errorMessage);

return OperateResult.Failed(
new PublisherSentFailedException(errorMessage),
new OperateError
{
Code = "SNS",
Description = $"Can't be found SNS topics for [{message.GetName().NormalizeForAws()}]"
});
}
catch (Exception ex)
{
@@ -82,11 +89,11 @@ namespace DotNetCore.CAP.AmazonSQS
}
}

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

await _semaphore.WaitAsync();
@@ -100,14 +107,21 @@ namespace DotNetCore.CAP.AmazonSQS
if (_topicArnMaps == null)
{
_topicArnMaps = new Dictionary<string, string>();
var topics = await _snsClient.ListTopicsAsync();
topics.Topics.ForEach(x =>
string nextToken = null;
do
{
var name = x.TopicArn.Split(':').Last();
_topicArnMaps.Add(name, x.TopicArn);
});

return true;
var topics = nextToken == null
? await _snsClient.ListTopicsAsync()
: await _snsClient.ListTopicsAsync(nextToken);
topics.Topics.ForEach(x =>
{
var name = x.TopicArn.Split(':').Last();
_topicArnMaps.Add(name, x.TopicArn);
});
nextToken = topics.NextToken;
}
while (!string.IsNullOrEmpty(nextToken));
}
}
catch (Exception e)
@@ -118,8 +132,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;
}

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

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

+ 52
- 12
src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusConsumerClient.cs 查看文件

@@ -78,13 +78,25 @@ namespace DotNetCore.CAP.AzureServiceBus
{
ConnectAsync().GetAwaiter().GetResult();

_consumerClient.RegisterMessageHandler(OnConsumerReceived,
new MessageHandlerOptions(OnExceptionReceived)
{
AutoComplete = false,
MaxConcurrentCalls = 10,
MaxAutoRenewDuration = TimeSpan.FromSeconds(30)
});
if (_asbOptions.EnableSessions)
{
_consumerClient.RegisterSessionHandler(OnConsumerReceivedWithSession,
new SessionHandlerOptions(OnExceptionReceived)
{
AutoComplete = false,
MaxAutoRenewDuration = TimeSpan.FromSeconds(30)
});
}
else
{
_consumerClient.RegisterMessageHandler(OnConsumerReceived,
new MessageHandlerOptions(OnExceptionReceived)
{
AutoComplete = false,
MaxConcurrentCalls = 10,
MaxAutoRenewDuration = TimeSpan.FromSeconds(30)
});
}

while (true)
{
@@ -96,7 +108,15 @@ namespace DotNetCore.CAP.AzureServiceBus

public void Commit(object sender)
{
_consumerClient.CompleteAsync((string)sender);
var commitInput = (AzureServiceBusConsumerCommitInput) sender;
if (_asbOptions.EnableSessions)
{
commitInput.Session.CompleteAsync(commitInput.LockToken);
}
else
{
_consumerClient.CompleteAsync(commitInput.LockToken);
}
}

public void Reject(object sender)
@@ -141,7 +161,13 @@ namespace DotNetCore.CAP.AzureServiceBus

if (!await mClient.SubscriptionExistsAsync(_asbOptions.TopicPath, _subscriptionName))
{
await mClient.CreateSubscriptionAsync(_asbOptions.TopicPath, _subscriptionName);
var subscriptionDescription =
new SubscriptionDescription(_asbOptions.TopicPath, _subscriptionName)
{
RequiresSession = _asbOptions.EnableSessions
};

await mClient.CreateSubscriptionAsync(subscriptionDescription);
_logger.LogInformation($"Azure Service Bus topic {_asbOptions.TopicPath} created subscription: {_subscriptionName}");
}

@@ -157,14 +183,28 @@ namespace DotNetCore.CAP.AzureServiceBus

#region private methods

private Task OnConsumerReceived(Message message, CancellationToken token)
private TransportMessage ConvertMessage(Message message)
{
var header = message.UserProperties.ToDictionary(x => x.Key, y => y.Value?.ToString());
header.Add(Headers.Group, _subscriptionName);

var context = new TransportMessage(header, message.Body);
return new TransportMessage(header, message.Body);
}
private Task OnConsumerReceivedWithSession(IMessageSession session, Message message, CancellationToken token)
{
var context = ConvertMessage(message);

OnMessageReceived?.Invoke(new AzureServiceBusConsumerCommitInput(message.SystemProperties.LockToken, session), context);
return Task.CompletedTask;
}

private Task OnConsumerReceived(Message message, CancellationToken token)
{
var context = ConvertMessage(message);

OnMessageReceived?.Invoke(message.SystemProperties.LockToken, context);
OnMessageReceived?.Invoke(new AzureServiceBusConsumerCommitInput(message.SystemProperties.LockToken), context);

return Task.CompletedTask;
}


+ 16
- 0
src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusConsumerCommitInput.cs 查看文件

@@ -0,0 +1,16 @@
using Microsoft.Azure.ServiceBus;

namespace DotNetCore.CAP.AzureServiceBus
{
public class AzureServiceBusConsumerCommitInput
{
public AzureServiceBusConsumerCommitInput(string lockToken, IMessageSession session = null)
{
LockToken = lockToken;
Session = session;
}
public IMessageSession Session { get; set; }
public string LockToken { get; set; }
}
}

+ 7
- 0
src/DotNetCore.CAP.AzureServiceBus/AzureServiceBusHeaders.cs 查看文件

@@ -0,0 +1,7 @@
namespace DotNetCore.CAP.AzureServiceBus
{
public static class AzureServiceBusHeaders
{
public const string SessionId = "cap-session-id";
}
}

+ 7
- 0
src/DotNetCore.CAP.AzureServiceBus/CAP.AzureServiceBusOptions.cs 查看文件

@@ -1,6 +1,7 @@
// 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.AzureServiceBus;
using Microsoft.Azure.ServiceBus.Primitives;

// ReSharper disable once CheckNamespace
@@ -21,6 +22,12 @@ namespace DotNetCore.CAP
/// </summary>
public string ConnectionString { get; set; }

/// <summary>
/// Whether Service Bus sessions are enabled. If enabled, all messages must contain a
/// <see cref="AzureServiceBusHeaders.SessionId"/> header. Defaults to false.
/// </summary>
public bool EnableSessions { get; set; } = false;

/// <summary>
/// The name of the topic relative to the service namespace base address.
/// </summary>


+ 4
- 4
src/DotNetCore.CAP.AzureServiceBus/DotNetCore.CAP.AzureServiceBus.csproj 查看文件

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

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>DotNetCore.CAP.AzureServiceBus</AssemblyName>
<PackageTags>$(PackageTags);AzureServiceBus</PackageTags>
</PropertyGroup>
@@ -9,15 +9,15 @@
<PropertyGroup>
<WarningsAsErrors>NU1605;NU1701</WarningsAsErrors>
<NoWarn>NU1701;CS1591</NoWarn>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.AzureServiceBus.xml</DocumentationFile>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\DotNetCore.CAP.AzureServiceBus.xml</DocumentationFile>
</PropertyGroup>

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

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

</Project>

+ 10
- 6
src/DotNetCore.CAP.AzureServiceBus/ITransport.AzureServiceBus.cs 查看文件

@@ -42,9 +42,16 @@ namespace DotNetCore.CAP.AzureServiceBus
{
MessageId = transportMessage.GetId(),
Body = transportMessage.Body,
Label = transportMessage.GetName()
Label = transportMessage.GetName(),
CorrelationId = transportMessage.GetCorrelationId()
};

if (_asbOptions.Value.EnableSessions)
{
transportMessage.Headers.TryGetValue(AzureServiceBusHeaders.SessionId, out var sessionId);
message.SessionId = string.IsNullOrEmpty(sessionId) ? transportMessage.GetId() : sessionId;
}

foreach (var header in transportMessage.Headers)
{
message.UserProperties.Add(header.Key, header.Value);
@@ -75,10 +82,7 @@ namespace DotNetCore.CAP.AzureServiceBus

try
{
if (_topicClient == null)
{
_topicClient = new TopicClient(BrokerAddress.Endpoint, _asbOptions.Value.TopicPath, RetryPolicy.NoRetry);
}
_topicClient ??= new TopicClient(BrokerAddress.Endpoint, _asbOptions.Value.TopicPath, RetryPolicy.NoRetry);
}
finally
{
@@ -86,4 +90,4 @@ namespace DotNetCore.CAP.AzureServiceBus
}
}
}
}
}

+ 0
- 128
src/DotNetCore.CAP.Dashboard/AwaitableInfo.cs 查看文件

@@ -1,128 +0,0 @@
// 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.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
- 37
src/DotNetCore.CAP.Dashboard/BatchCommandDispatcher.cs 查看文件

@@ -1,37 +0,0 @@
// 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.Net;
using System.Threading.Tasks;

namespace DotNetCore.CAP.Dashboard
{
internal class BatchCommandDispatcher : IDashboardDispatcher
{
private readonly Action<DashboardContext, long> _command;

public BatchCommandDispatcher(Action<DashboardContext, long> command)
{
_command = command;
}

public async Task Dispatch(DashboardContext context)
{
var messageIds = await context.Request.GetFormValuesAsync("messages[]");
if (messageIds.Count == 0)
{
context.Response.StatusCode = 422;
return;
}

foreach (var messageId in messageIds)
{
var id = long.Parse(messageId);
_command(context, id);
}

context.Response.StatusCode = (int) HttpStatusCode.NoContent;
}
}
}

+ 140
- 0
src/DotNetCore.CAP.Dashboard/CAP.BuilderExtension.cs 查看文件

@@ -0,0 +1,140 @@
// 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.Reflection;
using System.Threading.Tasks;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.GatewayProxy;
using DotNetCore.CAP.Dashboard.NodeDiscovery;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public static class CapBuilderExtension
{
public static IApplicationBuilder UseCapDashboard(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

var provider = app.ApplicationServices;

var options = provider.GetService<DashboardOptions>();
if (options != null)
{
if (provider.GetService<DiscoveryOptions>() != null)
{
app.UseMiddleware<GatewayProxyMiddleware>();
}

app.UseMiddleware<UiMiddleware>();

app.Map(options.PathMatch + "/api", false, x =>
{

var builder = new RouteBuilder(x);

var methods = typeof(RouteActionProvider).GetMethods(BindingFlags.Instance | BindingFlags.Public);

foreach (var method in methods)
{
var executor = ObjectMethodExecutor.Create(method, typeof(RouteActionProvider).GetTypeInfo());

var getAttr = method.GetCustomAttribute<HttpGetAttribute>();
if (getAttr != null)
{

builder.MapGet(getAttr.Template, async (request, response, data) =>
{
if (!await Authentication(request.HttpContext, options))
{
response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}

var actionProvider = new RouteActionProvider(request, response, data);
try
{
await executor.ExecuteAsync(actionProvider, null);
}
catch (Exception ex)
{
response.StatusCode = StatusCodes.Status500InternalServerError;
await response.WriteAsync(ex.Message);
}
});
}

var postAttr = method.GetCustomAttribute<HttpPostAttribute>();
if (postAttr != null)
{
builder.MapPost(postAttr.Template, async (request, response, data) =>
{
if (!await Authentication(request.HttpContext, options))
{
response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}

var actionProvider = new RouteActionProvider(request, response, data);
try
{
await executor.ExecuteAsync(actionProvider, null);
}
catch (Exception ex)
{
response.StatusCode = StatusCodes.Status500InternalServerError;
await response.WriteAsync(ex.Message);
}
});
}
}

var capRouter = builder.Build();

x.UseRouter(capRouter);
});
}

return app;
}

internal static async Task<bool> Authentication(HttpContext context, DashboardOptions options)
{
if (options.UseAuth)
{
var result = await context.AuthenticateAsync(options.DefaultAuthenticationScheme);

if (result.Succeeded && result.Principal != null)
{
context.User = result.Principal;
}
else
{
return false;
}
}

var isAuthenticated = context.User?.Identity?.IsAuthenticated;

if (isAuthenticated == false && options.UseChallengeOnAuth)
{
await context.ChallengeAsync(options.DefaultChallengeScheme);

return false;
}

return true;
}
}
}

+ 0
- 151
src/DotNetCore.CAP.Dashboard/CAP.DashboardMiddleware.cs 查看文件

@@ -1,151 +0,0 @@
// 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.Linq;
using System.Net;
using System.Threading.Tasks;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.GatewayProxy;
using DotNetCore.CAP.Dashboard.NodeDiscovery;
using DotNetCore.CAP.Persistence;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public static class CapBuilderExtension
{
public static IApplicationBuilder UseCapDashboard(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

CheckRequirement(app);

var provider = app.ApplicationServices;

if (provider.GetService<DashboardOptions>() != null)
{
if (provider.GetService<DiscoveryOptions>() != null)
{
app.UseMiddleware<GatewayProxyMiddleware>();
}

app.UseMiddleware<DashboardMiddleware>();
}

return app;
}

private static void CheckRequirement(IApplicationBuilder app)
{
var marker = app.ApplicationServices.GetService<CapMarkerService>();
if (marker == null)
{
throw new InvalidOperationException(
"AddCap() must be called on the service collection. eg: services.AddCap(...)");
}

var messageQueueMarker = app.ApplicationServices.GetService<CapMessageQueueMakerService>();
if (messageQueueMarker == null)
{
throw new InvalidOperationException(
"You must be config used message queue provider at AddCap() options! eg: services.AddCap(options=>{ options.UseKafka(...) })");
}

var databaseMarker = app.ApplicationServices.GetService<CapStorageMarkerService>();
if (databaseMarker == null)
{
throw new InvalidOperationException(
"You must be config used database provider at AddCap() options! eg: services.AddCap(options=>{ options.UseSqlServer(...) })");
}
}
}

sealed class CapStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseCapDashboard();

next(app);
};
}
}

public class DashboardMiddleware
{
private readonly RequestDelegate _next;
private readonly DashboardOptions _options;
private readonly RouteCollection _routes;
private readonly IDataStorage _storage;

public DashboardMiddleware(RequestDelegate next, DashboardOptions options, IDataStorage storage,
RouteCollection routes)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_options = options ?? throw new ArgumentNullException(nameof(options));
_storage = storage ?? throw new ArgumentNullException(nameof(storage));
_routes = routes ?? throw new ArgumentNullException(nameof(routes));
}

public async Task Invoke(HttpContext context)
{
if (!context.Request.Path.StartsWithSegments(_options.PathMatch,
out var matchedPath, out var remainingPath))
{
await _next(context);
return;
}

// Update the path
var path = context.Request.Path;
var pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matchedPath);
context.Request.Path = remainingPath;

try
{
var dashboardContext = new CapDashboardContext(_storage, _options, context);
var findResult = _routes.FindDispatcher(context.Request.Path.Value);

if (findResult == null)
{
await _next.Invoke(context);
return;
}

foreach (var authorizationFilter in _options.Authorization)
{
var authenticateResult = await authorizationFilter.AuthorizeAsync(dashboardContext);
if (authenticateResult) continue;

var isAuthenticated = context.User?.Identity?.IsAuthenticated;

context.Response.StatusCode = isAuthenticated == true
? (int)HttpStatusCode.Forbidden
: (int)HttpStatusCode.Unauthorized;

return;
}

dashboardContext.UriMatch = findResult.Item2;

await findResult.Item1.Dispatch(dashboardContext);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
}
}
}

+ 26
- 9
src/DotNetCore.CAP.Dashboard/CAP.DashboardOptions.cs 查看文件

@@ -1,34 +1,51 @@
// 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.Collections.Generic;
using DotNetCore.CAP.Dashboard;

// ReSharper disable once CheckNamespace

namespace DotNetCore.CAP
{
public class DashboardOptions
{
public DashboardOptions()
{
AppPath = "/";
PathMatch = "/cap";
Authorization = new[] {new LocalRequestsOnlyAuthorizationFilter()};
StatsPollingInterval = 2000;
}

/// <summary>
/// The path for the Back To Site link. Set to <see langword="null" /> in order to hide the Back To Site link.
/// When behind the proxy, specify the base path to allow spa call prefix.
/// </summary>
public string AppPath { get; set; }
public string PathBase { get; set; }

/// <summary>
/// Path prefix to match from url path.
/// </summary>
public string PathMatch { get; set; }

public IEnumerable<IDashboardAuthorizationFilter> Authorization { get; set; }

/// <summary>
/// The interval the /stats endpoint should be polled with.
/// </summary>
public int StatsPollingInterval { get; set; }

/// <summary>
/// Enable authentication on dashboard request.
/// </summary>
public bool UseAuth { get; set; }

/// <summary>
/// Default scheme used for authentication. If no scheme is set, the DefaultScheme set up in AddAuthentication will be used.
/// </summary>
public string DefaultAuthenticationScheme { get; set; }

/// <summary>
/// Enable authentication challenge on dashboard request.
/// </summary>
public bool UseChallengeOnAuth { get; set; }

/// <summary>
/// Default scheme used for authentication challenge. If no scheme is set, the DefaultChallengeScheme set up in AddAuthentication will be used.
/// </summary>
public string DefaultChallengeScheme { get; set; }
}
}

+ 14
- 3
src/DotNetCore.CAP.Dashboard/CAP.DashboardOptionsExtensions.cs 查看文件

@@ -3,9 +3,9 @@

using System;
using DotNetCore.CAP;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.GatewayProxy;
using DotNetCore.CAP.Dashboard.GatewayProxy.Requester;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

@@ -26,11 +26,22 @@ namespace DotNetCore.CAP
_options?.Invoke(dashboardOptions);
services.AddTransient<IStartupFilter, CapStartupFilter>();
services.AddSingleton(dashboardOptions);
services.AddSingleton(DashboardRoutes.Routes);
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
services.AddSingleton<IHttpClientCache, MemoryHttpClientCache>();
services.AddSingleton<IRequestMapper, RequestMapper>();
}
}

sealed class CapStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
next(app);

app.UseCapDashboard();
};
}
}
}
@@ -43,7 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
return capOptions.UseDashboard(opt => { });
}
public static CapOptions UseDashboard(this CapOptions capOptions, Action<DashboardOptions> options)
{
if (options == null)


+ 7
- 10
src/DotNetCore.CAP.Dashboard/CapCache.cs 查看文件

@@ -1,7 +1,4 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -18,7 +15,7 @@ namespace DotNetCore.CAP.Dashboard
/// </summary>
// ReSharper disable once InheritdocConsiderUsage
// ReSharper disable once InconsistentNaming
internal class Cache<K, T> : IDisposable
public class Cache<K, T> : IDisposable
{
#region Constructor and class members

@@ -124,7 +121,7 @@ namespace DotNetCore.CAP.Dashboard

private void RemoveByTimer(object state)
{
Remove((K) state);
Remove((K)state);
}

#endregion
@@ -254,8 +251,8 @@ namespace DotNetCore.CAP.Dashboard
try
{
var removers = (from k in _cache.Keys.Cast<K>()
where keyPattern(k)
select k).ToList();
where keyPattern(k)
select k).ToList();

foreach (var workKey in removers)
{
@@ -357,7 +354,7 @@ namespace DotNetCore.CAP.Dashboard
/// instance.
/// The <c>.Global</c> member is lazy instanciated.
/// </summary>
internal class CapCache : Cache<string, object>
public class CapCache : Cache<string, object>
{
#region Static Global Cache instance

@@ -375,4 +372,4 @@ namespace DotNetCore.CAP.Dashboard
}

#endregion
}
}

+ 0
- 44
src/DotNetCore.CAP.Dashboard/CoercedAwaitableInfo.cs 查看文件

@@ -1,44 +0,0 @@
// 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.Linq.Expressions;
using Microsoft.Extensions.Internal;

namespace DotNetCore.CAP.Dashboard
{
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;
}

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

+ 0
- 37
src/DotNetCore.CAP.Dashboard/CombinedResourceDispatcher.cs 查看文件

@@ -1,37 +0,0 @@
// 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.Reflection;
using System.Threading.Tasks;

namespace DotNetCore.CAP.Dashboard
{
internal class CombinedResourceDispatcher : EmbeddedResourceDispatcher
{
private readonly Assembly _assembly;
private readonly string _baseNamespace;
private readonly string[] _resourceNames;

public CombinedResourceDispatcher(
string contentType,
Assembly assembly,
string baseNamespace,
params string[] resourceNames) : base(contentType, assembly, null)
{
_assembly = assembly;
_baseNamespace = baseNamespace;
_resourceNames = resourceNames;
}

protected override async Task WriteResponse(DashboardResponse response)
{
foreach (var resourceName in _resourceNames)
{
await WriteResource(
response,
_assembly,
$"{_baseNamespace}.{resourceName}");
}
}
}
}

+ 0
- 42
src/DotNetCore.CAP.Dashboard/CommandDispatcher.cs 查看文件

@@ -1,42 +0,0 @@
// 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.Net;
using System.Threading.Tasks;

namespace DotNetCore.CAP.Dashboard
{
internal class CommandDispatcher : IDashboardDispatcher
{
private readonly Func<DashboardContext, bool> _command;

public CommandDispatcher(Func<DashboardContext, bool> command)
{
_command = command;
}

public Task Dispatch(DashboardContext context)
{
var request = context.Request;
var response = context.Response;

if (!"POST".Equals(request.Method, StringComparison.OrdinalIgnoreCase))
{
response.StatusCode = (int) HttpStatusCode.MethodNotAllowed;
return Task.FromResult(false);
}

if (_command(context))
{
response.StatusCode = (int) HttpStatusCode.NoContent;
}
else
{
response.StatusCode = 422;
}

return Task.FromResult(true);
}
}
}

+ 0
- 5
src/DotNetCore.CAP.Dashboard/Content/css/bootstrap.min.css
文件差异内容过多而无法显示
查看文件


+ 0
- 457
src/DotNetCore.CAP.Dashboard/Content/css/cap.css 查看文件

@@ -1,460 +0,0 @@
/* Sticky footer styles

html, body {
height: 100%;
/* The html and body elements cannot have any padding or margin. */
}

body {
/* 75px to make the container go all the way to the bottom of the topbar */
padding-top: 75px;
}

/* Wrapper for page content to push down footer */

#wrap {
height: auto !important;
height: 100%;
/* Negative indent footer by its height */
margin: 0 auto -60px;
min-height: 100%;
/* Pad bottom by footer height */
padding: 0 0 60px;
}

/* Set the fixed height of the footer here */

#footer { background-color: #f5f5f5; }


/* Custom page CSS

.container .credit { margin: 20px 0; }

.page-header {
margin-top: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.btn-death {
background-color: #777;
border-color: #666;
color: #fff;
}

.btn-death:hover {
background-color: #666;
border-color: #555;
color: #fff;
}

.list-group .list-group-item .glyphicon { margin-right: 3px; }

.breadcrumb {
background-color: inherit;
margin-bottom: 10px;
padding: 0;
}

.btn-toolbar-label {
display: inline-block;
margin-left: 5px;
padding: 7px 0;
vertical-align: middle;
}

.btn-toolbar-label-sm { padding: 5px 0; }

.btn-toolbar-spacer {
display: inline-block;
height: 1px;
width: 5px;
}

a:hover .label-hover {
background-color: #2a6496 !important;
color: #fff !important;
}

.expander { cursor: pointer; }

.expandable { display: none; }

.table-inner {
font-size: 90%;
margin-bottom: 7px;
}

.min-width {
white-space: nowrap;
width: 1%;
}

.align-right { text-align: right; }

.table > tbody > tr.hover:hover > td, .table > tbody > tr.hover:hover > th { background-color: #f9f9f9; }

.table > tbody > tr.highlight > td, .table > tbody > tr.highlight > th {
background-color: #fcf8e3;
border-color: #fbeed5;
}

.table > tbody > tr.highlight:hover > td, .table > tbody > tr.highlight:hover > th {
background-color: #f6f2dd;
border-color: #f5e8ce;
}

.word-break { word-break: break-all; }

/* Statistics widget

#stats .list-group-item {
background-color: #f8f8f8;
border-color: #e7e7e7;
}

#stats a.list-group-item { color: #777; }

#stats a.list-group-item:hover,
#stats a.list-group-item:focus { color: #333; }

#stats .list-group-item.active,
#stats .list-group-item.active:hover,
#stats .list-group-item.active:focus {
background-color: #e7e7e7;
border-color: #e7e7e7;
color: #555;
}

.table td.failed-job-details {
background-color: #f5f5f5;
border-top: none;
padding-bottom: 0;
padding-top: 0;
}

.obsolete-data, .obsolete-data a, .obsolete-data pre, .obsolete-data .label { color: #999; }

.obsolete-data pre, .obsolete-data .label { background-color: #f5f5f5; }

.obsolete-data .glyphicon-question-sign {
color: #999;
font-size: 80%;
}

.stack-trace {
border: none;
padding: 10px;
}

.st-type { font-weight: bold; }

.st-param-name { color: #666; }

.st-file { color: #999; }

.st-method {
color: #00008B;
font-weight: bold;
}

.st-line { color: #8B008B; }

.width-200 { width: 200px; }

.btn-toolbar-top { margin-bottom: 10px; }

.paginator .btn { color: #428bca; }

.paginator .btn.active { color: #333; }

/* Job Snippet styles */

.job-snippet {
-ms-border-radius: 4px;
background-color: #f5f5f5;
border-radius: 4px;
display: table;
margin-bottom: 20px;
padding: 15px;
width: 100%;
}

.job-snippet > * {
display: table-cell;
vertical-align: top;
}

.job-snippet-code { vertical-align: top; }

.job-snippet-code pre {
-ms-border-radius: 0;
background: inherit;
border: none;
border-radius: 0;
font-size: 14px;
margin: 0;
padding: 0;
}

.job-snippet-code code {
background-color: #f5f5f5;
color: black;
display: block;
}

.job-snippet-code pre .comment { color: rgb(0, 128, 0); }

.job-snippet-code pre .keyword { color: rgb(0, 0, 255); }

.job-snippet-code pre .string { color: rgb(163, 21, 21); }

.job-snippet-code pre .type { color: rgb(43, 145, 175); }

.job-snippet-code pre .xmldoc { color: rgb(128, 128, 128); }

.job-snippet-properties {
max-width: 200px;
padding-left: 5px;
}

.job-snippet-properties dl { margin: 0; }

.job-snippet-properties dl dt {
color: #999;
font-weight: normal;
text-shadow: 0 1px white;
}

.job-snippet-properties dl dd {
margin-bottom: 5px;
margin-left: 0;
}

.job-snippet-properties pre {
-ms-box-shadow: none;
-webkit-box-shadow: none;
background-color: white;
border: none;
margin: 0;
padding: 2px 4px;
}

.job-snippet-properties code { color: black; }

.state-card {
-ms-border-radius: 3px;
background-color: #fff;
border: 1px solid #e5e5e5;
border-radius: 3px;
display: block;
margin-bottom: 7px;
padding: 12px;
position: relative;
}

.state-card-title { margin-bottom: 0; }

.state-card-title .pull-right { margin-top: 3px; }

.state-card-text {
margin-bottom: 0;
margin-top: 5px;
}

.state-card h4 { margin-top: 0; }

.state-card-body {
-ms-border-bottom-left-radius: 3px;
-ms-border-bottom-right-radius: 3px;
background-color: #f5f5f5;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
margin: 10px -12px -12px -12px;
padding: 10px;
}

.state-card-body dl {
margin-bottom: 0;
margin-top: 5px;
}

.state-card-body pre {
background: transparent;
padding: 0;
white-space: pre-wrap; /* CSS 3 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}

.state-card-body .stack-trace {
background-color: transparent;
margin-bottom: 0;
padding: 0 20px;
}

.state-card-body .exception-type { margin-top: 0; }

/* Job History styles */

.job-history {
margin-bottom: 10px;
opacity: 0.8;
}

.job-history.job-history-current { opacity: 1.0; }

.job-history-heading {
-ms-border-top-left-radius: 4px;
-ms-border-top-right-radius: 4px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
color: #666;
padding: 5px 10px;
}

.job-history-body {
background-color: #f5f5f5;
padding: 10px;
}

.job-history-title {
margin-bottom: 2px;
margin-top: 0;
}

.job-history dl {
margin-bottom: 5px;
margin-top: 5px;
}

.job-history .stack-trace {
background-color: transparent;
margin-bottom: 5px;
padding: 0 20px;
}

.job-history .exception-type { margin-top: 0; }

.job-history-current .job-history-heading,
.job-history-current small { color: white; }

a.job-method { color: inherit; }

.list-group .glyphicon { top: 2px; }

span.metric {
-moz-transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
-ms-transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
-o-transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
-webkit-transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
background-color: transparent;
border: solid 1px;
border-radius: 10px;
display: inline-block;
font-size: 12px;
line-height: 1;
min-width: 10px;
padding: 2px 6px;
text-align: center;
transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
vertical-align: baseline;
white-space: nowrap;
}

span.metric.highlighted {
color: #fff !important;
font-weight: bold;
}

span.metric-default {
border-color: #777;
color: #777;
}

span.metric-default.highlighted { background-color: #777; }

div.metric {
-ms-border-radius: 4px;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
border: solid 1px transparent;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
margin-bottom: 20px;
transition: color .1s ease-out, background .1s ease-out, border .1s ease-out;
}

div.metric .metric-body {
font-size: 26px;
padding: 15px 15px 0;
text-align: center;
}

div.metric .metric-description {
padding: 0 15px 15px;
text-align: center;
}

div.metric.metric-default { border-color: #ddd; }

div.metric-info,
span.metric-info {
border-color: #5bc0de;
color: #5bc0de;
}

span.metric-info.highlighted { background-color: #5bc0de; }

div.metric-warning,
span.metric-warning {
border-color: #f0ad4e;
color: #f0ad4e;
}

span.metric-warning.highlighted { background-color: #f0ad4e; }

div.metric-success,
span.metric-success {
border-color: #5cb85c;
color: #5cb85c;
}

span.metric-success.highlighted { background-color: #5cb85c; }

div.metric-danger,
span.metric-danger {
border-color: #d9534f;
color: #d9534f;
}

span.metric-danger.highlighted { background-color: #d9534f; }

span.metric-null,
div.metric-null { display: none; }

@media (min-width: 992px) {
#stats {
position: fixed;
width: 220px
}
}

@media (min-width: 1200px) {
#stats { width: 262.5px; }
}

.subscribe-table td { vertical-align: middle !important; }

.subscribe-table td[rowspan] { font-weight: bold; }

#legend {
background: rgba(173, 169, 169, 0.13);
color: #000;
position: absolute;
right: 20px;
top: 110px;
}

+ 0
- 1
src/DotNetCore.CAP.Dashboard/Content/css/jsonview.min.css 查看文件

@@ -1 +0,0 @@
@charset "UTF-8";.jsonview{font-family:monospace;font-size:1.1em;white-space:pre-wrap}.jsonview .prop{font-weight:700;text-decoration:none;color:#000}.jsonview .null,.jsonview .undefined{color:red}.jsonview .bool,.jsonview .num{color:#00f}.jsonview .string{color:green;white-space:pre-wrap}.jsonview .string.multiline{display:inline-block;vertical-align:text-top}.jsonview .collapser{position:absolute;left:-1em;cursor:pointer}.jsonview .collapsible{transition:height 1.2s;transition:width 1.2s}.jsonview .collapsible.collapsed{height:.8em;width:1em;display:inline-block;overflow:hidden;margin:0}.jsonview .collapsible.collapsed:before{content:"…";width:1em;margin-left:.2em}.jsonview .collapser.collapsed{transform:rotate(0)}.jsonview .q{display:inline-block;width:0;color:transparent}.jsonview li{position:relative}.jsonview ul{list-style:none;margin:0 0 0 2em;padding:0}.jsonview h1{font-size:1.2em}

部分文件因为文件数量过多而无法显示

正在加载...
取消
保存