@@ -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 |
@@ -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 |
@@ -1,4 +1,5 @@ | |||||
[Oo]bj/ | |||||
node_modules | |||||
[Oo]bj/ | |||||
[Bb]in/ | [Bb]in/ | ||||
TestResults/ | TestResults/ | ||||
.nuget/ | .nuget/ | ||||
@@ -12,8 +13,6 @@ PublishProfiles/ | |||||
*.docstates | *.docstates | ||||
_ReSharper.* | _ReSharper.* | ||||
nuget.exe | nuget.exe | ||||
*net45.csproj | |||||
*net451.csproj | |||||
*k10.csproj | *k10.csproj | ||||
*.psess | *.psess | ||||
*.vsp | *.vsp | ||||
@@ -24,21 +23,11 @@ nuget.exe | |||||
*.*sdf | *.*sdf | ||||
*.ipch | *.ipch | ||||
*.sln.ide | *.sln.ide | ||||
project.lock.json | |||||
.vs | .vs | ||||
.build/ | .build/ | ||||
.testPublish/ | |||||
obj/ | |||||
bin/ | |||||
/.idea/.idea.CAP | |||||
/.idea/.idea.CAP | /.idea/.idea.CAP | ||||
/.idea | /.idea | ||||
Properties | Properties | ||||
/pack.bat | /pack.bat | ||||
/src/DotNetCore.CAP/project.json | |||||
/src/DotNetCore.CAP/packages.config | |||||
/src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj | |||||
/NuGet.config | |||||
.vscode/* | .vscode/* | ||||
samples/Sample.RabbitMQ.MongoDB/appsettings.Development.json | |||||
site/ | site/ |
@@ -2,7 +2,7 @@ language: csharp | |||||
sudo: required | sudo: required | ||||
dist: xenial | dist: xenial | ||||
solution: CAP.sln | solution: CAP.sln | ||||
dotnet: 3.1.100 | |||||
dotnet: 5.0.100 | |||||
mono: none | mono: none | ||||
env: | env: | ||||
- Cap_MySql_ConnectionString="Server=127.0.0.1;Database=cap_test;Uid=root;Pwd=;Allow User Variables=True;SslMode=none" | - 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: | script: | ||||
- export PATH="$PATH:$HOME/.dotnet/tools" | - 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 | - flubu build tests |
@@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution | |||||
.gitignore = .gitignore | .gitignore = .gitignore | ||||
.travis.yml = .travis.yml | .travis.yml = .travis.yml | ||||
appveyor.yml = appveyor.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 | .github\ISSUE_TEMPLATE = .github\ISSUE_TEMPLATE | ||||
LICENSE.txt = LICENSE.txt | LICENSE.txt = LICENSE.txt | ||||
README.md = README.md | README.md = README.md | ||||
@@ -67,7 +68,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.AmazonSQS", | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.AmazonSQS.InMemory", "samples\Sample.AmazonSQS.InMemory\Sample.AmazonSQS.InMemory.csproj", "{B187DD15-092D-4B72-9807-50856607D237}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.AmazonSQS.InMemory", "samples\Sample.AmazonSQS.InMemory\Sample.AmazonSQS.InMemory.csproj", "{B187DD15-092D-4B72-9807-50856607D237}" | ||||
EndProject | 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 | EndProject | ||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | 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}.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.ActiveCfg = Release|Any CPU | ||||
{B187DD15-092D-4B72-9807-50856607D237}.Release|Any CPU.Build.0 = 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 | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||
@@ -184,7 +215,12 @@ Global | |||||
{2B0F467E-ABBD-4A51-BF38-D4F609DB6266} = {3A6B6931-A123-477A-9469-8B468B5385AF} | {2B0F467E-ABBD-4A51-BF38-D4F609DB6266} = {3A6B6931-A123-477A-9469-8B468B5385AF} | ||||
{43475E00-51B7-443D-BC2D-FC21F9D8A0B4} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} | {43475E00-51B7-443D-BC2D-FC21F9D8A0B4} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} | ||||
{B187DD15-092D-4B72-9807-50856607D237} = {3A6B6931-A123-477A-9469-8B468B5385AF} | {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 | EndGlobalSection | ||||
GlobalSection(ExtensibilityGlobals) = postSolution | GlobalSection(ExtensibilityGlobals) = postSolution | ||||
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} | SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} | ||||
@@ -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"> | <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/=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: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/=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> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> |
@@ -32,12 +32,13 @@ CAP can be installed in your project with the following command. | |||||
PM> Install-Package DotNetCore.CAP | 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.Kafka | ||||
PM> Install-Package DotNetCore.CAP.RabbitMQ | PM> Install-Package DotNetCore.CAP.RabbitMQ | ||||
PM> Install-Package DotNetCore.CAP.AzureServiceBus | PM> Install-Package DotNetCore.CAP.AzureServiceBus | ||||
PM> Install-Package DotNetCore.CAP.AmazonSQS | |||||
``` | ``` | ||||
CAP supports SqlServer, MySql, PostgreSql,MongoDB as event log storage. | CAP supports SqlServer, MySql, PostgreSql,MongoDB as event log storage. | ||||
@@ -80,6 +81,7 @@ public void ConfigureServices(IServiceCollection services) | |||||
x.UseRabbitMQ("ConnectionString"); | x.UseRabbitMQ("ConnectionString"); | ||||
x.UseKafka("ConnectionString"); | x.UseKafka("ConnectionString"); | ||||
x.UseAzureServiceBus("ConnectionString"); | x.UseAzureServiceBus("ConnectionString"); | ||||
x.UseAmazonSQS(); | |||||
}); | }); | ||||
} | } | ||||
@@ -239,7 +241,7 @@ services.AddCap(x => | |||||
### Dashboard | ### 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 | 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"; })`. | 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 | ## Contribute | ||||
@@ -21,7 +21,7 @@ CAP 采用的是和当前数据库集成的本地消息表的方案来解决在 | |||||
你同样可以把 CAP 当做 EventBus 来使用,CAP提供了一种更加简单的方式来实现事件消息的发布和订阅,在订阅以及发布的过程中,你不需要继承或实现任何接口。 | 你同样可以把 CAP 当做 EventBus 来使用,CAP提供了一种更加简单的方式来实现事件消息的发布和订阅,在订阅以及发布的过程中,你不需要继承或实现任何接口。 | ||||
这是CAP集在ASP.NET Core 微服务架构中的一个示意图: | |||||
这是 CAP 集在ASP.NET Core 微服务架构中的一个示意图: | |||||
## 架构预览 | ## 架构预览 | ||||
@@ -39,12 +39,13 @@ CAP 采用的是和当前数据库集成的本地消息表的方案来解决在 | |||||
PM> Install-Package DotNetCore.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.Kafka | ||||
PM> Install-Package DotNetCore.CAP.RabbitMQ | PM> Install-Package DotNetCore.CAP.RabbitMQ | ||||
PM> Install-Package DotNetCore.CAP.AzureServiceBus | PM> Install-Package DotNetCore.CAP.AzureServiceBus | ||||
PM> Install-Package DotNetCore.CAP.AmazonSQS | |||||
``` | ``` | ||||
CAP 提供了 Sql Server, MySql, PostgreSQL,MongoDB 的扩展作为数据库存储: | CAP 提供了 Sql Server, MySql, PostgreSQL,MongoDB 的扩展作为数据库存储: | ||||
@@ -81,10 +82,11 @@ public void ConfigureServices(IServiceCollection services) | |||||
//如果你使用的 MongoDB,你可以添加如下配置: | //如果你使用的 MongoDB,你可以添加如下配置: | ||||
x.UseMongoDB("ConnectionStrings"); //注意,仅支持MongoDB 4.0+集群 | x.UseMongoDB("ConnectionStrings"); //注意,仅支持MongoDB 4.0+集群 | ||||
//CAP支持 RabbitMQ、Kafka、AzureServiceBus 等作为MQ,根据使用选择配置: | |||||
//CAP支持 RabbitMQ、Kafka、AzureServiceBus、AmazonSQS 等作为MQ,根据使用选择配置: | |||||
x.UseRabbitMQ("ConnectionStrings"); | x.UseRabbitMQ("ConnectionStrings"); | ||||
x.UseKafka("ConnectionStrings"); | x.UseKafka("ConnectionStrings"); | ||||
x.UseAzureServiceBus("ConnectionStrings"); | x.UseAzureServiceBus("ConnectionStrings"); | ||||
x.UseAmazonSQS(); | |||||
}); | }); | ||||
} | } | ||||
@@ -237,7 +239,7 @@ services.AddCap(x => | |||||
### Dashboard | ### Dashboard | ||||
CAP 2.1+ 以上版本中提供了仪表盘(Dashboard)功能,你可以很方便的查看发出和接收到的消息。除此之外,你还可以在仪表盘中实时查看发送或者接收到的消息。 | |||||
CAP 同时提供了仪表盘(Dashboard)功能,你可以很方便的查看发出和接收到的消息。 除此之外,你还可以在仪表盘中实时查看发送或者接收到的消息。 | |||||
使用一下命令安装 Dashboard: | 使用一下命令安装 Dashboard: | ||||
@@ -7,7 +7,7 @@ environment: | |||||
services: | services: | ||||
- mysql | - mysql | ||||
before_build: | 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: | build_script: | ||||
- ps: flubu | - ps: flubu | ||||
test: off | test: off | ||||
@@ -18,6 +18,6 @@ deploy: | |||||
on: | on: | ||||
appveyor_repo_tag: true | appveyor_repo_tag: true | ||||
api_key: | api_key: | ||||
secure: PZXRBOGLyhYLP7ulHfrh6MnkqB8CstuitgbLcJr3cZkLJLLzPH0ahvuTtmhWxtR2 | |||||
secure: Q/7aMFCMA363iQv1r2fgW2PyvAFL3H409s9Pq8SgmYAbTH+c6ZQcYC9evHpipuNR | |||||
skip_symbols: false | skip_symbols: false | ||||
artifact: /artifacts\/.+\.s?nupkg/ | artifact: /artifacts\/.+\.s?nupkg/ |
@@ -1,5 +1,4 @@ | |||||
using System; | |||||
using System.IO; | |||||
using System.IO; | |||||
using System.Xml; | using System.Xml; | ||||
using FlubuCore.Context; | using FlubuCore.Context; | ||||
using FlubuCore.Scripting.Attributes; | using FlubuCore.Scripting.Attributes; | ||||
@@ -11,7 +10,7 @@ namespace BuildScript | |||||
{ | { | ||||
public BuildVersion FetchBuildVersion(ITaskContext context) | 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(); | XmlDocument doc = new XmlDocument(); | ||||
doc.LoadXml(content); | doc.LoadXml(content); | ||||
@@ -26,13 +25,12 @@ namespace BuildScript | |||||
bool isCi = false; | bool isCi = false; | ||||
bool isTagged = false; | bool isTagged = false; | ||||
if (!context.BuildSystems().IsLocalBuild) | |||||
if (!context.BuildServers().IsLocalBuild) | |||||
{ | { | ||||
isCi = true; | 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; | isTagged = true; | ||||
} | } | ||||
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
<TargetFramework>netstandard2.1</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -9,7 +9,7 @@ | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="FlubuCore" Version="5.1.8" /> | |||||
<PackageReference Include="FlubuCore" Version="6.1.0" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -1,8 +1,8 @@ | |||||
<Project> | <Project> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<VersionMajor>3</VersionMajor> | |||||
<VersionMajor>5</VersionMajor> | |||||
<VersionMinor>1</VersionMinor> | <VersionMinor>1</VersionMinor> | ||||
<VersionPatch>0</VersionPatch> | |||||
<VersionPatch>4</VersionPatch> | |||||
<VersionQuality></VersionQuality> | <VersionQuality></VersionQuality> | ||||
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> | <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -1,5 +1,154 @@ | |||||
# Release Notes | # 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) | ## Version 3.0.4 (2020-05-27) | ||||
**Bug Fixed:** | **Bug Fixed:** | ||||
@@ -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. | 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} | > 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 [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 [Consumer Group Id](http://kafka.apache.org/documentation/#group.id) in Apache Kafka. | ||||
Map to Subscription Name in Azure Service Bus. | 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 | #### 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. | 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. | 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 | #### ConsumerThreadCount | ||||
> Default : 1 | > Default : 1 | ||||
Number of consumer threads, when this value is greater than 1, the order of message execution cannot be guaranteed | |||||
Number of consumer threads, when this value is greater than 1, the order of message execution cannot be guaranteed. | |||||
#### FailedRetryCount | #### FailedRetryCount | ||||
@@ -87,14 +107,9 @@ Maximum number of retries. When this value is reached, retry will stop and the m | |||||
> Default: NULL | > 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 | #### SucceedMessageExpiredAfter | ||||
@@ -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. |
@@ -2,6 +2,95 @@ | |||||
The data sent by using the `ICapPublisher` interface is called `Message`. | 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 | ## Scheduling | ||||
After CAP receives a message, it sends the message to Transport(RabitMq, Kafka...), which is transported by transport. | 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. | 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. | 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. | 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. |
@@ -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 |
@@ -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: 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 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) | [Article: New features in version 2.6](https://www.cnblogs.com/savorboard/p/cap-2-6.html) | ||||
@@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services) | |||||
public class PublishController : Controller | public class PublishController : Controller | ||||
{ | { | ||||
[Route("~/send")] | [Route("~/send")] | ||||
public IActionResult SendMessage([FromService]ICapPublisher capBus) | |||||
public IActionResult SendMessage([FromServices]ICapPublisher capBus) | |||||
{ | { | ||||
capBus.Publish("test.show.time", DateTime.Now); | 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 | ## Process Message | ||||
```C# | ```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 | ## 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. | 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. |
@@ -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). | 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# | ```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# | ```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; | |||||
}); | }); | ||||
}); | |||||
} | |||||
``` | ``` |
@@ -1,3 +0,0 @@ | |||||
# Health Checks | |||||
TODO |
@@ -2,19 +2,19 @@ | |||||
!!! faq "Any IM group(e.g Tencent QQ group) to learn and chat about CAP?" | !!! 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. | 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# | ```c# | ||||
public void ConfigureServices(IServiceCollection services) | 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. | 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?" | !!! 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. | 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. |
@@ -67,4 +67,15 @@ Timestamp | Message created time | string | |||||
Content | Message content | string | Content | Message content | string | ||||
CallbackName | Consumer callback topic name | 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 |
@@ -24,7 +24,7 @@ public void ConfigureServices(IServiceCollection services) | |||||
services.AddCap(x => | services.AddCap(x => | ||||
{ | { | ||||
x.UsePostgreSql(opt=>{ | |||||
x.UseSqlServer(opt=>{ | |||||
//SqlServerOptions | //SqlServerOptions | ||||
}); | }); | ||||
// x.UseXXX ... | // x.UseXXX ... | ||||
@@ -41,5 +41,24 @@ The AzureServiceBus configuration options provided directly by the CAP: | |||||
NAME | DESCRIPTION | TYPE | DEFAULT | NAME | DESCRIPTION | TYPE | DEFAULT | ||||
:---|:---|---|:--- | :---|:---|---|:--- | ||||
ConnectionString | Endpoint address | string | | 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 | 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. |
@@ -10,7 +10,9 @@ CAP supports several transport methods: | |||||
* [Kafka](kafka.md) | * [Kafka](kafka.md) | ||||
* [Azure Service Bus](azure-service-bus.md) | * [Azure Service Bus](azure-service-bus.md) | ||||
* [Amazon SQS](aws-sqs.md) | * [Amazon SQS](aws-sqs.md) | ||||
* [NATS](nats.md) | |||||
* [In-Memory Queue](in-memory-queue.md) | * [In-Memory Queue](in-memory-queue.md) | ||||
* [Redis Streams](redis-streams.md) | |||||
## How to select a transport | ## How to select a transport | ||||
@@ -28,3 +30,15 @@ CAP supports several transport methods: | |||||
>`Kafka` vs `RabbitMQ` : | >`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 | ||||
## 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 | |||||
@@ -40,6 +40,42 @@ NAME | DESCRIPTION | TYPE | DEFAULT | |||||
:---|:---|---|:--- | :---|:---|---|:--- | ||||
Servers | Broker server address | string | | Servers | Broker server address | string | | ||||
ConnectionPoolSize | connection pool size | int | 10 | 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 | #### Kafka MainConfig Options | ||||
@@ -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) |
@@ -44,8 +44,10 @@ UserName | Broker user name | string | guest | |||||
Password | Broker password | string | guest | Password | Broker password | string | guest | ||||
VirtualHost | Broker virtual host | string | / | VirtualHost | Broker virtual host | string | / | ||||
Port | Port | int | -1 | 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 | #### 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 | #### How to connect cluster | ||||
using comma split connection string, like this: | using comma split connection string, like this: | ||||
@@ -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) |
@@ -30,15 +30,31 @@ services.AddCap(config => | |||||
在 `AddCap` 中 `CapOptions` 对象是用来存储配置相关信息,默认情况下它们都具有一些默认值,有些时候你可能需要自定义。 | 在 `AddCap` 中 `CapOptions` 对象是用来存储配置相关信息,默认情况下它们都具有一些默认值,有些时候你可能需要自定义。 | ||||
#### DefaultGroup | |||||
#### DefaultGroupName | |||||
默认值:cap.queue.{程序集名称} | 默认值:cap.queue.{程序集名称} | ||||
默认的消费者组的名字,在不同的 Transports 中对应不同的名字,可以通过自定义此值来自定义不同 Transports 中的名字,以便于查看。 | 默认的消费者组的名字,在不同的 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 | #### Version | ||||
@@ -76,6 +92,12 @@ services.AddCap(config => | |||||
消费者线程并行处理消息的线程数,当这个值大于1时,将不能保证消息执行的顺序。 | 消费者线程并行处理消息的线程数,当这个值大于1时,将不能保证消息执行的顺序。 | ||||
#### CollectorCleaningInterval | |||||
默认值:300 秒 | |||||
收集器删除已经过期消息的时间间隔。 | |||||
#### FailedRetryCount | #### FailedRetryCount | ||||
默认值:50 | 默认值:50 | ||||
@@ -86,12 +108,7 @@ services.AddCap(config => | |||||
默认值:NULL | 默认值:NULL | ||||
类型:`Action<MessageType, string, string>` | |||||
> | |||||
T1 : Message Type | |||||
T2 : Message Name | |||||
T3 : Message Content | |||||
类型:`Action<FailedInfo>` | |||||
重试阈值的失败回调。当重试达到 FailedRetryCount 设置的值的时候,将调用此 Action 回调,你可以通过指定此回调来接收失败达到最大的通知,以做出人工介入。例如发送邮件或者短信。 | 重试阈值的失败回调。当重试达到 FailedRetryCount 设置的值的时候,将调用此 Action 回调,你可以通过指定此回调来接收失败达到最大的通知,以做出人工介入。例如发送邮件或者短信。 | ||||
@@ -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>(); | |||||
``` | |||||
目前, 我们还不支持同时添加多个过滤器。 |
@@ -6,6 +6,54 @@ | |||||
你可以阅读 [quick-start](../getting-started/quick-start.md#_3) 来学习如何发送和处理消息。 | 你可以阅读 [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 中我们可以做到不需要修改或包装使用者的原始消息数据格式和内容进行发送。 | 在 3.0+ 版本中,我们对消息结构进行了重构,我们利用了消息队列中消息协议中的 Header 来传输一些额外信息,以便于在 Body 中我们可以做到不需要修改或包装使用者的原始消息数据格式和内容进行发送。 | ||||
@@ -45,7 +93,6 @@ channel.basicPublish(exchangeName, routingKey, | |||||
``` | ``` | ||||
## 消息调度 | ## 消息调度 | ||||
CAP 接收到消息之后会将消息发送到 Transport, 由 Transport 进行运输。 | CAP 接收到消息之后会将消息发送到 Transport, 由 Transport 进行运输。 | ||||
@@ -66,7 +113,7 @@ CAP 接收到消息之后会将消息进行 Persistent(持久化), 有关 | |||||
在消息发送过程中,当出现 Broker 宕机或者连接失败的情况亦或者出现异常的情况下,这个时候 CAP 会对发送的重试,第一次重试次数为 3,4分钟后以后每分钟重试一次,进行次数 +1,当总次数达到50次后,CAP将不对其进行重试。 | 在消息发送过程中,当出现 Broker 宕机或者连接失败的情况亦或者出现异常的情况下,这个时候 CAP 会对发送的重试,第一次重试次数为 3,4分钟后以后每分钟重试一次,进行次数 +1,当总次数达到50次后,CAP将不对其进行重试。 | ||||
你可以在 CapOptions 中设置FailedRetryCount来调整默认重试的总次数。 | |||||
你可以在 CapOptions 中设置 [FailedRetryCount](../configuration#failedretrycount) 来调整默认重试的总次数。 | |||||
当失败总次数达到默认失败总次数后,就不会进行重试了,你可以在 Dashboard 中查看消息失败的原因,然后进行人工重试处理。 | 当失败总次数达到默认失败总次数后,就不会进行重试了,你可以在 Dashboard 中查看消息失败的原因,然后进行人工重试处理。 | ||||
@@ -78,4 +125,5 @@ CAP 接收到消息之后会将消息进行 Persistent(持久化), 有关 | |||||
数据库消息表中具有一个 ExpiresAt 字段表示消息的过期时间,当消息发送成功或者消费成功后,CAP会将消息状态为 Successed 的 ExpiresAt 设置为 1天 后过期,会将消息状态为 Failed 的 ExpiresAt 设置为 15天 后过期。 | 数据库消息表中具有一个 ExpiresAt 字段表示消息的过期时间,当消息发送成功或者消费成功后,CAP会将消息状态为 Successed 的 ExpiresAt 设置为 1天 后过期,会将消息状态为 Failed 的 ExpiresAt 设置为 15天 后过期。 | ||||
CAP 默认情况下会每隔一个小时将消息表的数据进行清理删除,避免数据量过多导致性能的降低。清理规则为 ExpiresAt 不为空并且小于当前时间的数据。 也就是说状态为Failed的消息(正常情况他们已经被重试了 50 次),如果你15天没有人工介入处理,同样会被清理掉。 | |||||
CAP 默认情况下会每隔**5分钟**将消息表的数据进行清理删除,避免数据量过多导致性能的降低。清理规则为 ExpiresAt 不为空并且小于当前时间的数据。 也就是说状态为Failed的消息(正常情况他们已经被重试了 50 次),如果你15天没有人工介入处理,同样会被清理掉。你可以通过 [CollectorCleaningInterval](../configuration#collectorcleaninginterval) 配置项来自定义间隔时间。 | |||||
@@ -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 |
@@ -25,6 +25,8 @@ CAP 采用模块化设计,具有高度的可扩展性。你有许多选项可 | |||||
[Article: CAP 介绍及使用](http://www.cnblogs.com/savorboard/p/cap.html) | [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 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) | [Article: CAP 2.6 版本中的新特性](https://www.cnblogs.com/savorboard/p/cap-2-6.html) | ||||
@@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services) | |||||
public class PublishController : Controller | public class PublishController : Controller | ||||
{ | { | ||||
[Route("~/send")] | [Route("~/send")] | ||||
public IActionResult SendMessage([FromService]ICapPublisher capBus) | |||||
public IActionResult SendMessage([FromServices]ICapPublisher capBus) | |||||
{ | { | ||||
capBus.Publish("test.show.time", DateTime.Now); | capBus.Publish("test.show.time", DateTime.Now); | ||||
@@ -26,6 +26,12 @@ services.AddCap(x => | |||||
### Dashboard 配置项 | ### Dashboard 配置项 | ||||
* PathBase | |||||
默认值:N/A | |||||
当位于代理后时,通过配置此参数可以指定代理请求前缀。 | |||||
* PathMatch | * PathMatch | ||||
默认值:'/cap' | 默认值:'/cap' | ||||
@@ -38,39 +44,67 @@ services.AddCap(x => | |||||
此配置项用来配置Dashboard 前端 获取状态接口(/stats)的轮询时间 | 此配置项用来配置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# | ```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# | ```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; | |||||
}); | }); | ||||
}); | |||||
} | |||||
``` | ``` |
@@ -1,3 +0,0 @@ | |||||
# 健康检查 | |||||
TODO |
@@ -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 版本。** |
@@ -2,19 +2,19 @@ | |||||
!!! faq "Any IM group(e.g Tencent QQ group) to learn and chat about CAP?" | !!! 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. | 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# | ```c# | ||||
public void ConfigureServices(IServiceCollection services) | 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. | 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?" | !!! 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. | 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. |
@@ -68,4 +68,10 @@ CallbackName | 回调的订阅者名称 | string | |||||
感谢社区对CAP的支持,以下是社区支持的持久化的实现 | 感谢社区对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 |
@@ -25,7 +25,7 @@ public void ConfigureServices(IServiceCollection services) | |||||
services.AddCap(x => | services.AddCap(x => | ||||
{ | { | ||||
x.UsePostgreSql(opt=>{ | |||||
x.UseSqlServer(opt=>{ | |||||
//SqlServerOptions | //SqlServerOptions | ||||
}); | }); | ||||
// x.UseXXX ... | // x.UseXXX ... | ||||
@@ -45,4 +45,23 @@ NAME | DESCRIPTION | TYPE | DEFAULT | |||||
:---|:---|---|:--- | :---|:---|---|:--- | ||||
ConnectionString | Endpoint 地址 | string | | ConnectionString | Endpoint 地址 | string | | ||||
TopicPath | Topic entity path | string | cap | 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 | 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. |
@@ -10,12 +10,14 @@ CAP 支持以下几种运输方式: | |||||
* [Kafka](kafka.md) | * [Kafka](kafka.md) | ||||
* [Azure Service Bus](azure-service-bus.md) | * [Azure Service Bus](azure-service-bus.md) | ||||
* [Amazon SQS](aws-sqs.md) | * [Amazon SQS](aws-sqs.md) | ||||
* [NATS](nats.md) | |||||
* [In-Memory Queue](in-memory-queue.md) | * [In-Memory Queue](in-memory-queue.md) | ||||
* [Redis Streams](redis-streams.md) | |||||
## 怎么选择运输器 | ## 怎么选择运输器 | ||||
🏳🌈 | RabbitMQ | Kafka | Azure Service Bus | In-Memory | 🏳🌈 | RabbitMQ | Kafka | Azure Service Bus | In-Memory | ||||
:-- | :--: | :--: | :--: | :-- : | |||||
:-- | :--: | :--: | :--: | :--: | |||||
**定位** | 可靠消息传输 | 实时数据处理 | 云 | 内存型,测试 | **定位** | 可靠消息传输 | 实时数据处理 | 云 | 内存型,测试 | ||||
**分布式** | ✔ | ✔ | ✔ |❌ | **分布式** | ✔ | ✔ | ✔ |❌ | ||||
**持久化** | ✔ | ✔ | ✔ | ❌ | **持久化** | ✔ | ✔ | ✔ | ❌ | ||||
@@ -26,4 +28,14 @@ CAP 支持以下几种运输方式: | |||||
> http://geekswithblogs.net/michaelstephenson/archive/2012/08/12/150399.aspx | > http://geekswithblogs.net/michaelstephenson/archive/2012/08/12/150399.aspx | ||||
>`Kafka` vs `RabbitMQ` : | >`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 |
@@ -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)找到更多详细信息。 |
@@ -45,8 +45,10 @@ UserName | 用户名 | string | guest | |||||
Password | 密码 | string | guest | Password | 密码 | string | guest | ||||
VirtualHost | 虚拟主机 | string | / | VirtualHost | 虚拟主机 | string | / | ||||
Port | 端口号 | int | -1 | 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 | #### 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 集群? | #### 如何连接 RabbitMQ 集群? | ||||
使用逗号分隔连接字符串即可,如下: | 使用逗号分隔连接字符串即可,如下: | ||||
@@ -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)找到更多详细信息。 |
@@ -10,7 +10,7 @@ edit_uri: 'edit/master/docs/content' | |||||
docs_dir: 'content' | docs_dir: 'content' | ||||
# Copyright | # Copyright | ||||
copyright: Copyright © 2017 <a href="https://github.com/dotnetcore">NCC</a>, Maintained by the <a href="/about/contact-us/#cap-team">CAP Team</a>. | |||||
copyright: Copyright © 2021 <a href="https://github.com/dotnetcore">NCC</a>, Maintained by the <a href="/about/contact-us/#cap-team">CAP Team</a>. | |||||
#theme: material | #theme: material | ||||
@@ -19,18 +19,26 @@ theme: | |||||
palette: | palette: | ||||
primary: 'deep purple' | primary: 'deep purple' | ||||
accent: 'indigo' | accent: 'indigo' | ||||
language: 'en' | |||||
language: en | |||||
include_sidebar: true | include_sidebar: true | ||||
logo: 'img/logo.svg' | logo: 'img/logo.svg' | ||||
favicon: 'img/favicon.ico' | favicon: 'img/favicon.ico' | ||||
features: | features: | ||||
- tabs | |||||
- navigation.tabs | |||||
- navigation.instant | |||||
i18n: | i18n: | ||||
prev: 'Previous' | prev: 'Previous' | ||||
next: 'Next' | next: 'Next' | ||||
#Customization | #Customization | ||||
extra: | 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: | social: | ||||
- icon: 'fontawesome/brands/github' | - icon: 'fontawesome/brands/github' | ||||
link: 'https://github.com/dotnetcore/CAP' | link: 'https://github.com/dotnetcore/CAP' | ||||
@@ -81,16 +89,18 @@ nav: | |||||
- CAP: | - CAP: | ||||
- Configuration: user-guide/en/cap/configuration.md | - Configuration: user-guide/en/cap/configuration.md | ||||
- Messaging: user-guide/en/cap/messaging.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 | - Serialization: user-guide/en/cap/serialization.md | ||||
- Transactions: user-guide/en/cap/transactions.md | - Transactions: user-guide/en/cap/transactions.md | ||||
- Idempotence: user-guide/en/cap/idempotence.md | - Idempotence: user-guide/en/cap/idempotence.md | ||||
- Transport: | - 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 | - Apache Kafka®: user-guide/en/transport/kafka.md | ||||
- Azure Service Bus: user-guide/en/transport/azure-service-bus.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 | - In-Memory Queue: user-guide/en/transport/in-memory-queue.md | ||||
- Storage: | - Storage: | ||||
- General: user-guide/en/storage/general.md | - General: user-guide/en/storage/general.md | ||||
@@ -115,16 +125,18 @@ nav: | |||||
- CAP: | - CAP: | ||||
- 配置: user-guide/zh/cap/configuration.md | - 配置: user-guide/zh/cap/configuration.md | ||||
- 消息: user-guide/zh/cap/messaging.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/serialization.md | ||||
- 运输: user-guide/zh/cap/transactions.md | |||||
- 事务: user-guide/zh/cap/transactions.md | |||||
- 幂等性: user-guide/zh/cap/idempotence.md | - 幂等性: user-guide/zh/cap/idempotence.md | ||||
- 传输: | - 传输: | ||||
- 简介: user-guide/zh/transport/general.md | |||||
- 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 | - 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 | - In-Memory Queue: user-guide/zh/transport/in-memory-queue.md | ||||
- 存储: | - 存储: | ||||
- 简介: user-guide/zh/storage/general.md | - 简介: user-guide/zh/storage/general.md | ||||
@@ -139,6 +151,7 @@ nav: | |||||
- 性能追踪: user-guide/zh/monitoring/diagnostics.md | - 性能追踪: user-guide/zh/monitoring/diagnostics.md | ||||
- 健康检查: user-guide/zh/monitoring/health-checks.md | - 健康检查: user-guide/zh/monitoring/health-checks.md | ||||
- 示例: | - 示例: | ||||
- Castle DynamicProxy: user-guide/zh/samples/castle.dynamicproxy.md | |||||
- Github: user-guide/zh/samples/github.md | - Github: user-guide/zh/samples/github.md | ||||
- eShopOnContainers: user-guide/zh/samples/eshoponcontainers.md | - eShopOnContainers: user-guide/zh/samples/eshoponcontainers.md | ||||
- FAQ: user-guide/zh/samples/faq.md | - FAQ: user-guide/zh/samples/faq.md | ||||
@@ -146,3 +159,8 @@ nav: | |||||
- Contact Us: about/contact-us.md | - Contact Us: about/contact-us.md | ||||
- Release Notes: about/release-notes.md | - Release Notes: about/release-notes.md | ||||
- License: about/license.md | - License: about/license.md | ||||
# Google Analytics | |||||
google_analytics: | |||||
- !!python/object/apply:os.getenv ["GOOGLE_ANALYTICS_KEY"] | |||||
- auto |
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -2,7 +2,7 @@ | |||||
"Logging": { | "Logging": { | ||||
"IncludeScopes": false, | "IncludeScopes": false, | ||||
"LogLevel": { | "LogLevel": { | ||||
"Default": "Error" | |||||
"Default": "Information" | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -16,7 +16,7 @@ namespace Sample.ConsoleApp | |||||
{ | { | ||||
//console app does not support dashboard | //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 => | x.UseRabbitMQ(z => | ||||
{ | { | ||||
z.HostName = "192.168.3.57"; | z.HostName = "192.168.3.57"; | ||||
@@ -29,7 +29,7 @@ namespace Sample.ConsoleApp | |||||
var sp = container.BuildServiceProvider(); | var sp = container.BuildServiceProvider(); | ||||
sp.GetService<IBootstrapper>().BootstrapAsync(default); | |||||
sp.GetService<IBootstrapper>().BootstrapAsync(); | |||||
Console.ReadLine(); | Console.ReadLine(); | ||||
} | } | ||||
@@ -1,12 +1,12 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
<OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.5" /> | |||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="5.0.0" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -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; } | |||||
} | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} | |||||
} |
@@ -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>(); }); | |||||
} | |||||
} |
@@ -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> |
@@ -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(); | |||||
}); | |||||
} | |||||
} | |||||
} |
@@ -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;" | |||||
} | |||||
} |
@@ -1,13 +1,13 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
<WarningsAsErrors>NU1701</WarningsAsErrors> | <WarningsAsErrors>NU1701</WarningsAsErrors> | ||||
<NoWarn>NU1701</NoWarn> | <NoWarn>NU1701</NoWarn> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="Dapper" Version="2.0.35" /> | |||||
<PackageReference Include="Dapper" Version="2.0.78" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" /> | <ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" /> | ||||
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -20,7 +20,7 @@ namespace Sample.RabbitMQ.MongoDB | |||||
services.AddCap(x => | services.AddCap(x => | ||||
{ | { | ||||
x.UseMongoDB(Configuration.GetConnectionString("MongoDB")); | x.UseMongoDB(Configuration.GetConnectionString("MongoDB")); | ||||
x.UseRabbitMQ("192.168.2.120"); | |||||
x.UseRabbitMQ(""); | |||||
x.UseDashboard(); | x.UseDashboard(); | ||||
}); | }); | ||||
services.AddControllers(); | services.AddControllers(); | ||||
@@ -26,13 +26,13 @@ namespace Sample.RabbitMQ.MySql | |||||
} | } | ||||
public class AppDbContext : DbContext | 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; } | public DbSet<Person> Persons { get; set; } | ||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
{ | { | ||||
optionsBuilder.UseMySql(ConnectionString); | |||||
optionsBuilder.UseMySql(ConnectionString, ServerVersion.FromString("mysql")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -1,12 +1,12 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <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> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" /> | <ProjectReference Include="..\..\src\DotNetCore.CAP.Dashboard\DotNetCore.CAP.Dashboard.csproj" /> | ||||
@@ -12,23 +12,11 @@ namespace Sample.RabbitMQ.SqlServer | |||||
{ | { | ||||
return $"Name:{Name}, Id:{Id}"; | 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 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; } | public DbSet<Person> Persons { get; set; } | ||||
@@ -1,8 +1,8 @@ | |||||
using System; | using System; | ||||
using System.Data; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Dapper; | using Dapper; | ||||
using DotNetCore.CAP; | using DotNetCore.CAP; | ||||
using DotNetCore.CAP.Messages; | |||||
using Microsoft.AspNetCore.Mvc; | using Microsoft.AspNetCore.Mvc; | ||||
using Microsoft.Data.SqlClient; | using Microsoft.Data.SqlClient; | ||||
@@ -21,7 +21,7 @@ namespace Sample.RabbitMQ.SqlServer.Controllers | |||||
[Route("~/without/transaction")] | [Route("~/without/transaction")] | ||||
public async Task<IActionResult> WithoutTransaction() | public async Task<IActionResult> WithoutTransaction() | ||||
{ | { | ||||
await _capBus.PublishAsync("sample.rabbitmq.mysql", new Person() | |||||
await _capBus.PublishAsync("sample.rabbitmq.sqlserver", new Person() | |||||
{ | { | ||||
Id = 123, | Id = 123, | ||||
Name = "Bar" | Name = "Bar" | ||||
@@ -40,7 +40,11 @@ namespace Sample.RabbitMQ.SqlServer.Controllers | |||||
//your business code | //your business code | ||||
connection.Execute("insert into test(name) values('test')", transaction: transaction); | 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" }); | 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(); | return Ok(); | ||||
} | } | ||||
[NonAction] | [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}"); | Console.WriteLine($@"{DateTime.Now} Subscriber invoked, Info: {p}"); | ||||
} | } | ||||
[NonAction] | [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}"); | Console.WriteLine($@"{DateTime.Now} Subscriber invoked, Info: {p}"); | ||||
} | } | ||||
} | } | ||||
@@ -1,16 +1,16 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk.Web"> | <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <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> | <PrivateAssets>all</PrivateAssets> | ||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
</PackageReference> | </PackageReference> | ||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.5" /> | |||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.4" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -1,4 +1,6 @@ | |||||
using System; | using System; | ||||
using System.Text.Encodings.Web; | |||||
using System.Text.Unicode; | |||||
using DotNetCore.CAP.Messages; | using DotNetCore.CAP.Messages; | ||||
using Microsoft.AspNetCore.Builder; | using Microsoft.AspNetCore.Builder; | ||||
using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
@@ -15,7 +17,7 @@ namespace Sample.RabbitMQ.SqlServer | |||||
services.AddCap(x => | services.AddCap(x => | ||||
{ | { | ||||
x.UseEntityFramework<AppDbContext>(); | x.UseEntityFramework<AppDbContext>(); | ||||
x.UseRabbitMQ("192.168.2.120"); | |||||
x.UseRabbitMQ(""); | |||||
x.UseDashboard(); | x.UseDashboard(); | ||||
x.FailedRetryCount = 5; | x.FailedRetryCount = 5; | ||||
x.FailedThresholdCallback = failed => | 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, | logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times, | ||||
requiring manual troubleshooting. Message name: {failed.Message.GetName()}"); | requiring manual troubleshooting. Message name: {failed.Message.GetName()}"); | ||||
}; | }; | ||||
x.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All); | |||||
}); | }); | ||||
services.AddControllers(); | services.AddControllers(); | ||||
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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"] |
@@ -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>(); | |||||
}); | |||||
} | |||||
} |
@@ -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> |
@@ -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(); | |||||
}); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,9 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft": "Warning", | |||||
"Microsoft.Hosting.Lifetime": "Information" | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft": "Warning", | |||||
"Microsoft.Hosting.Lifetime": "Information" | |||||
} | |||||
}, | |||||
"AllowedHosts": "*" | |||||
} |
@@ -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 |
@@ -26,7 +26,7 @@ | |||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> | <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> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -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); | |||||
} | |||||
} | |||||
} |
@@ -5,8 +5,10 @@ using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Text; | using System.Text; | ||||
using System.Text.Json; | |||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Amazon.Auth.AccessControlPolicy; | |||||
using Amazon.SimpleNotificationService; | using Amazon.SimpleNotificationService; | ||||
using Amazon.SimpleNotificationService.Model; | using Amazon.SimpleNotificationService.Model; | ||||
using Amazon.SQS; | using Amazon.SQS; | ||||
@@ -14,7 +16,6 @@ using Amazon.SQS.Model; | |||||
using DotNetCore.CAP.Messages; | using DotNetCore.CAP.Messages; | ||||
using DotNetCore.CAP.Transport; | using DotNetCore.CAP.Transport; | ||||
using Microsoft.Extensions.Options; | using Microsoft.Extensions.Options; | ||||
using Newtonsoft.Json; | |||||
using Headers = DotNetCore.CAP.Messages.Headers; | using Headers = DotNetCore.CAP.Messages.Headers; | ||||
namespace DotNetCore.CAP.AmazonSQS | namespace DotNetCore.CAP.AmazonSQS | ||||
@@ -42,17 +43,17 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
public BrokerAddress BrokerAddress => new BrokerAddress("AmazonSQS", _queueUrl); | 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); | Connect(initSNS: true, initSQS: false); | ||||
var topicArns = new List<string>(); | var topicArns = new List<string>(); | ||||
foreach (var topic in topics) | |||||
foreach (var topic in topicNames) | |||||
{ | { | ||||
var createTopicRequest = new CreateTopicRequest(topic.NormalizeForAws()); | var createTopicRequest = new CreateTopicRequest(topic.NormalizeForAws()); | ||||
@@ -60,11 +61,23 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
topicArns.Add(createTopicResponse.TopicArn); | 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) | public void Listening(TimeSpan timeout, CancellationToken cancellationToken) | ||||
@@ -83,7 +96,7 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
if (response.Messages.Count == 1) | 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 header = messageObj.MessageAttributes.ToDictionary(x => x.Key, x => x.Value.Value); | ||||
var body = messageObj.Message; | var body = messageObj.Message; | ||||
@@ -207,6 +220,51 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
return Task.CompletedTask; | 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 | #endregion | ||||
} | } | ||||
} | } |
@@ -1,23 +1,23 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
<TargetFramework>netstandard2.1</TargetFramework> | |||||
<AssemblyName>DotNetCore.CAP.AmazonSQS</AssemblyName> | <AssemblyName>DotNetCore.CAP.AmazonSQS</AssemblyName> | ||||
<PackageTags>$(PackageTags);AmazonSQS;SQS</PackageTags> | <PackageTags>$(PackageTags);AmazonSQS;SQS</PackageTags> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<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> | <NoWarn>1701;1702;1705;CS1591</NoWarn> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <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> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | <ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
</Project> | </Project> |
@@ -37,9 +37,9 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
{ | { | ||||
try | 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; | string bodyJson = null; | ||||
if (message.Body != null) | if (message.Body != null) | ||||
@@ -62,12 +62,19 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
await _snsClient.PublishAsync(request); | await _snsClient.PublishAsync(request); | ||||
_logger.LogDebug($"SNS topic message [{message.GetName().NormalizeForAws()}] has been published."); | _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) | catch (Exception ex) | ||||
{ | { | ||||
@@ -82,11 +89,11 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
} | } | ||||
} | } | ||||
public async Task<bool> TryAddTopicArns() | |||||
private async Task FetchExistingTopicArns() | |||||
{ | { | ||||
if (_topicArnMaps != null) | if (_topicArnMaps != null) | ||||
{ | { | ||||
return true; | |||||
return; | |||||
} | } | ||||
await _semaphore.WaitAsync(); | await _semaphore.WaitAsync(); | ||||
@@ -100,14 +107,21 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
if (_topicArnMaps == null) | if (_topicArnMaps == null) | ||||
{ | { | ||||
_topicArnMaps = new Dictionary<string, string>(); | _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) | catch (Exception e) | ||||
@@ -118,8 +132,27 @@ namespace DotNetCore.CAP.AmazonSQS | |||||
{ | { | ||||
_semaphore.Release(); | _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; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -78,13 +78,25 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | { | ||||
ConnectAsync().GetAwaiter().GetResult(); | 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) | while (true) | ||||
{ | { | ||||
@@ -96,7 +108,15 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
public void Commit(object sender) | 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) | public void Reject(object sender) | ||||
@@ -141,7 +161,13 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
if (!await mClient.SubscriptionExistsAsync(_asbOptions.TopicPath, _subscriptionName)) | 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}"); | _logger.LogInformation($"Azure Service Bus topic {_asbOptions.TopicPath} created subscription: {_subscriptionName}"); | ||||
} | } | ||||
@@ -157,14 +183,28 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
#region private methods | #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()); | var header = message.UserProperties.ToDictionary(x => x.Key, y => y.Value?.ToString()); | ||||
header.Add(Headers.Group, _subscriptionName); | 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; | return Task.CompletedTask; | ||||
} | } | ||||
@@ -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; } | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | |||||
public static class AzureServiceBusHeaders | |||||
{ | |||||
public const string SessionId = "cap-session-id"; | |||||
} | |||||
} |
@@ -1,6 +1,7 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | // Copyright (c) .NET Core Community. All rights reserved. | ||||
// Licensed under the MIT License. See License.txt in the project root for license information. | // Licensed under the MIT License. See License.txt in the project root for license information. | ||||
using DotNetCore.CAP.AzureServiceBus; | |||||
using Microsoft.Azure.ServiceBus.Primitives; | using Microsoft.Azure.ServiceBus.Primitives; | ||||
// ReSharper disable once CheckNamespace | // ReSharper disable once CheckNamespace | ||||
@@ -21,6 +22,12 @@ namespace DotNetCore.CAP | |||||
/// </summary> | /// </summary> | ||||
public string ConnectionString { get; set; } | 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> | /// <summary> | ||||
/// The name of the topic relative to the service namespace base address. | /// The name of the topic relative to the service namespace base address. | ||||
/// </summary> | /// </summary> | ||||
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netstandard2.0</TargetFramework> | |||||
<TargetFramework>netstandard2.1</TargetFramework> | |||||
<AssemblyName>DotNetCore.CAP.AzureServiceBus</AssemblyName> | <AssemblyName>DotNetCore.CAP.AzureServiceBus</AssemblyName> | ||||
<PackageTags>$(PackageTags);AzureServiceBus</PackageTags> | <PackageTags>$(PackageTags);AzureServiceBus</PackageTags> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -9,15 +9,15 @@ | |||||
<PropertyGroup> | <PropertyGroup> | ||||
<WarningsAsErrors>NU1605;NU1701</WarningsAsErrors> | <WarningsAsErrors>NU1605;NU1701</WarningsAsErrors> | ||||
<NoWarn>NU1701;CS1591</NoWarn> | <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> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="4.1.3" /> | |||||
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="5.1.2" /> | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | <ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | ||||
</ItemGroup> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -42,9 +42,16 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
{ | { | ||||
MessageId = transportMessage.GetId(), | MessageId = transportMessage.GetId(), | ||||
Body = transportMessage.Body, | 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) | foreach (var header in transportMessage.Headers) | ||||
{ | { | ||||
message.UserProperties.Add(header.Key, header.Value); | message.UserProperties.Add(header.Key, header.Value); | ||||
@@ -75,10 +82,7 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
try | 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 | finally | ||||
{ | { | ||||
@@ -86,4 +90,4 @@ namespace DotNetCore.CAP.AzureServiceBus | |||||
} | } | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,34 +1,51 @@ | |||||
// Copyright (c) .NET Core Community. All rights reserved. | // Copyright (c) .NET Core Community. All rights reserved. | ||||
// Licensed under the MIT License. See License.txt in the project root for license information. | // 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 | // ReSharper disable once CheckNamespace | ||||
namespace DotNetCore.CAP | namespace DotNetCore.CAP | ||||
{ | { | ||||
public class DashboardOptions | public class DashboardOptions | ||||
{ | { | ||||
public DashboardOptions() | public DashboardOptions() | ||||
{ | { | ||||
AppPath = "/"; | |||||
PathMatch = "/cap"; | PathMatch = "/cap"; | ||||
Authorization = new[] {new LocalRequestsOnlyAuthorizationFilter()}; | |||||
StatsPollingInterval = 2000; | StatsPollingInterval = 2000; | ||||
} | } | ||||
/// <summary> | /// <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> | /// </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 string PathMatch { get; set; } | ||||
public IEnumerable<IDashboardAuthorizationFilter> Authorization { get; set; } | |||||
/// <summary> | /// <summary> | ||||
/// The interval the /stats endpoint should be polled with. | /// The interval the /stats endpoint should be polled with. | ||||
/// </summary> | /// </summary> | ||||
public int StatsPollingInterval { get; set; } | 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; } | |||||
} | } | ||||
} | } |
@@ -3,9 +3,9 @@ | |||||
using System; | using System; | ||||
using DotNetCore.CAP; | using DotNetCore.CAP; | ||||
using DotNetCore.CAP.Dashboard; | |||||
using DotNetCore.CAP.Dashboard.GatewayProxy; | using DotNetCore.CAP.Dashboard.GatewayProxy; | ||||
using DotNetCore.CAP.Dashboard.GatewayProxy.Requester; | using DotNetCore.CAP.Dashboard.GatewayProxy.Requester; | ||||
using Microsoft.AspNetCore.Builder; | |||||
using Microsoft.AspNetCore.Hosting; | using Microsoft.AspNetCore.Hosting; | ||||
using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
@@ -26,11 +26,22 @@ namespace DotNetCore.CAP | |||||
_options?.Invoke(dashboardOptions); | _options?.Invoke(dashboardOptions); | ||||
services.AddTransient<IStartupFilter, CapStartupFilter>(); | services.AddTransient<IStartupFilter, CapStartupFilter>(); | ||||
services.AddSingleton(dashboardOptions); | services.AddSingleton(dashboardOptions); | ||||
services.AddSingleton(DashboardRoutes.Routes); | |||||
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>(); | services.AddSingleton<IHttpRequester, HttpClientHttpRequester>(); | ||||
services.AddSingleton<IHttpClientCache, MemoryHttpClientCache>(); | services.AddSingleton<IHttpClientCache, MemoryHttpClientCache>(); | ||||
services.AddSingleton<IRequestMapper, RequestMapper>(); | 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 => { }); | return capOptions.UseDashboard(opt => { }); | ||||
} | } | ||||
public static CapOptions UseDashboard(this CapOptions capOptions, Action<DashboardOptions> options) | public static CapOptions UseDashboard(this CapOptions capOptions, Action<DashboardOptions> options) | ||||
{ | { | ||||
if (options == null) | if (options == null) | ||||
@@ -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.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Threading; | using System.Threading; | ||||
@@ -18,7 +15,7 @@ namespace DotNetCore.CAP.Dashboard | |||||
/// </summary> | /// </summary> | ||||
// ReSharper disable once InheritdocConsiderUsage | // ReSharper disable once InheritdocConsiderUsage | ||||
// ReSharper disable once InconsistentNaming | // ReSharper disable once InconsistentNaming | ||||
internal class Cache<K, T> : IDisposable | |||||
public class Cache<K, T> : IDisposable | |||||
{ | { | ||||
#region Constructor and class members | #region Constructor and class members | ||||
@@ -124,7 +121,7 @@ namespace DotNetCore.CAP.Dashboard | |||||
private void RemoveByTimer(object state) | private void RemoveByTimer(object state) | ||||
{ | { | ||||
Remove((K) state); | |||||
Remove((K)state); | |||||
} | } | ||||
#endregion | #endregion | ||||
@@ -254,8 +251,8 @@ namespace DotNetCore.CAP.Dashboard | |||||
try | try | ||||
{ | { | ||||
var removers = (from k in _cache.Keys.Cast<K>() | 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) | foreach (var workKey in removers) | ||||
{ | { | ||||
@@ -357,7 +354,7 @@ namespace DotNetCore.CAP.Dashboard | |||||
/// instance. | /// instance. | ||||
/// The <c>.Global</c> member is lazy instanciated. | /// The <c>.Global</c> member is lazy instanciated. | ||||
/// </summary> | /// </summary> | ||||
internal class CapCache : Cache<string, object> | |||||
public class CapCache : Cache<string, object> | |||||
{ | { | ||||
#region Static Global Cache instance | #region Static Global Cache instance | ||||
@@ -375,4 +372,4 @@ namespace DotNetCore.CAP.Dashboard | |||||
} | } | ||||
#endregion | #endregion | ||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} |
@@ -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}"); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} | |||||
} |
@@ -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; | |||||
} |
@@ -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} |