Browse Source

add docs to master (#284)

* update version to 2.4.0

* Add version options to config file.

* update resource

* add  message version support  for dashboard

* add  message version support  for dashboard

* Support using version to isolate messages. #220

* update mongo unit tests

* update unit tests

* update unit tests

* Set default versions for consumer groups

* solve the problem of issue#181 (#237)

* Issue#235 (#238)

* solve the problem of issue#181

* solve the problem of issue#235

* refactor

* Fix the message persistence bug. #240

* using new CamelCaseNamingStrategy

* update packages to .net core 2.2

* update test framework to netcoreapp2.2

* Update .travis.yml

* update TargetFramework

* Exclude build samples project

* update version to 2.4.1

* add samples project to sln for build

* update version to 2.4.2

* Fixed PostgreSql version isolation feature bug. (#256)

* Fixed spelling errors

* modify cap publish Message to rabbitmq slow (#261)

* Startup the CAP with the BackgroundService.  #265

* update samples

* Fixed SQL query bug. #266

* update travis ci config

* update travis ci config

* adjust dashboard table column width

* adjust the consumer execution time to milliseconds

* update ignore

* add mkdocs.yml

* update version to 2.4.3

* add about.md docs

* add index.md docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* add docs

* Fix resource files

* add docs

* add docs

* add docs

* Create readme.md

* add markdown extensions supports

* update about.md

* add CNAME fiel

* add img

* update docs

* Update README.zh-cn.md
master
Savorboard 5 years ago
committed by GitHub
parent
commit
bf6f7b43a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1407 additions and 154 deletions
  1. +1
    -0
      .gitignore
  2. +1
    -1
      README.zh-cn.md
  3. +1
    -1
      build/version.props
  4. +1
    -0
      docs/CNAME
  5. +5
    -0
      docs/about.md
  6. BIN
     
  7. +29
    -0
      docs/index.md
  8. +8
    -0
      docs/readme.md
  9. +233
    -0
      docs/user-guide-cn/api-interface.md
  10. +156
    -0
      docs/user-guide-cn/configuration.md
  11. +55
    -0
      docs/user-guide-cn/design-principle.md
  12. +13
    -0
      docs/user-guide-cn/distributed-transactions.md
  13. +51
    -0
      docs/user-guide-cn/faq.md
  14. +81
    -0
      docs/user-guide-cn/getting-started.md
  15. +88
    -0
      docs/user-guide-cn/implementation-mechanisms.md
  16. +204
    -0
      docs/user-guide/api-interface.md
  17. +153
    -0
      docs/user-guide/configuration.md
  18. +55
    -0
      docs/user-guide/design-principle.md
  19. +14
    -0
      docs/user-guide/distributed-transactions.md
  20. +40
    -0
      docs/user-guide/faq.md
  21. +83
    -0
      docs/user-guide/getting-started.md
  22. +90
    -0
      docs/user-guide/implementation-mechanisms.md
  23. +24
    -0
      mkdocs.yml
  24. +12
    -102
      src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs
  25. +3
    -24
      src/DotNetCore.CAP/Dashboard/Content/resx/Strings.resx
  26. +5
    -14
      src/DotNetCore.CAP/Dashboard/Content/resx/Strings.zh.resx
  27. +0
    -11
      src/DotNetCore.CAP/Dashboard/HtmlHelper.cs
  28. +1
    -1
      src/DotNetCore.CAP/IConsumerHandler.Default.cs

+ 1
- 0
.gitignore View File

@@ -41,3 +41,4 @@ Properties
/NuGet.config
.vscode/*
samples/Sample.RabbitMQ.MongoDB/appsettings.Development.json
site/

+ 1
- 1
README.zh-cn.md View File

@@ -8,7 +8,7 @@

CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级、易使用、高性能等特点。

你可以在这里[CAP Wiki](https://github.com/dotnetcore/CAP/wiki)看到更多详细资料。
你可以在这里[CAP docs](http://docs.dotnet-china.org/CAP/)看到更多详细资料。

你可以在这里看到[CAP 视频教程](https://www.cnblogs.com/savorboard/p/cap-video-1.html),学习如何在项目中集成CAP。



+ 1
- 1
build/version.props View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<VersionMajor>2</VersionMajor>
<VersionMinor>4</VersionMinor>
<VersionPatch>2</VersionPatch>
<VersionPatch>3</VersionPatch>
<VersionQuality></VersionQuality>
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
</PropertyGroup>


+ 1
- 0
docs/CNAME View File

@@ -0,0 +1 @@
cap.dotnet-china.com

+ 5
- 0
docs/about.md View File

@@ -0,0 +1,5 @@

## Contact Us

* Submit an issue
* Email: yangxiaodong1214@126.com

BIN
View File


+ 29
- 0
docs/index.md View File

@@ -0,0 +1,29 @@
# CAP

CAP is a library based on .Net standard, which is a solution to deal with distributed transactions, also has the function of EventBus, it is lightweight, easy to use, and efficiently.

## Introduction

In the process of building an SOA or MicroService system, we usually need to use the event to integrate each services. In the process, the simple use of message queue does not guarantee the reliability. CAP is adopted the local message table program integrated with the current database to solve the exception may occur in the process of the distributed system calling each other. It can ensure that the event messages are not lost in any case.

You can also use the CAP as an EventBus. The CAP provides a simpler way to implement event publishing and subscriptions. You do not need to inherit or implement any interface during the process of subscription and sending.

This is a diagram of the CAP working in the ASP.NET Core MicroService architecture:

![cap.png](img/architecture.png)

!!! note
CAP implements the Outbox Pattern described in the [eShop ebook](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/subscribe-events#designing-atomicity-and-resiliency-when-publishing-to-the-event-bus).

## Contributing

One of the easiest ways to contribute is to participate in discussions and discuss issues. You can also contribute by submitting pull requests with code changes.

If you have any question or problems, please report them on the CAP repository:

<a href="https://github.com/dotnetcore/cap/issues/new"><button class="btn btn-primary btn-lg" type="submit"><i class="fab fa-github fa-2x"></i> Report Issue</button></a>
<a href="https://github.com/dotnetcore/cap/issues"><button class="btn btn-primary btn-lg" type="submit"> Active Issues <i class="fab fa-github fa-2x"></i></button></a>

## License

CAP is licensed under the [MIT license](https://github.com/dotnetcore/CAP/blob/master/LICENSE.txt).

+ 8
- 0
docs/readme.md View File

@@ -0,0 +1,8 @@
# CAP documentation

The folder contains the documentation for CAP.

We are using [Github Pages](https://github.com/yang-xiaodong/cap-docs) to host the documentation and the rendered version
can be found [here](http://cap.dotnet-china.com).

Doc pages are authored in Markdown - you can find a primer [here](https://help.gamejolt.com/markdown).

+ 233
- 0
docs/user-guide-cn/api-interface.md View File

@@ -0,0 +1,233 @@
CAP 的 API 接口只有一个,就是 `ICapPublisher` 接口,你可以从 DI 容器中获取到该接口的实例进行调用。

### 发布/发送

你可以使用 `ICapPublisher` 接口中的 `Publish<T>` 或者 `PublishAsync<T>` 方法来发送消息:

```cs
public class PublishController : Controller
{
private readonly ICapPublisher _capBus;

public PublishController(ICapPublisher capPublisher)
{
_capBus = capPublisher;
}
//不使用事务
[Route("~/without/transaction")]
public IActionResult WithoutTransaction()
{
_capBus.Publish("xxx.services.show.time", DateTime.Now);
return Ok();
}

//Ado.Net 中使用事务,自动提交
[Route("~/adonet/transaction")]
public IActionResult AdonetWithTransaction()
{
using (var connection = new MySqlConnection(ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true))
{
//业务代码

_capBus.Publish("xxx.services.show.time", DateTime.Now);
}
}
return Ok();
}

//EntityFramework 中使用事务,自动提交
[Route("~/ef/transaction")]
public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
{
//业务代码

_capBus.Publish("xxx.services.show.time", DateTime.Now);
}
return Ok();
}
}

```

下面是PublishAsync这个接口的签名:

**`PublishAsync<T>(string name,T object)`**

默认情况下,在调用此方法的时候 CAP 将在内部创建事务,然后将消息写入到 `Cap.Published` 这个消息表。

#### 消息补偿

有时候当发送一条消息出去之后,希望有一个回调可以获得消费方法的通知,用来补偿发送方做的业务操作,那么可以使用下面这个重载。

**`PublishAsync<T>(string name,T object, string callBackName)`**

这个重载中 `callbackName` 是一个回调的订阅方法名称,当消费端处理完成消息之后CAP会把消费者的处理结果返回并且调用指定的订阅方法。

> 在一些需要业务补偿的场景中,我们可以利用此特性进行一些还原的补偿操作。例如:电商系统中的付款操作,订单在进行支付调用支付服务的过程中如果发生异常,那么支付服务可以通过返回一个结果来告诉调用方此次业务失败,调用方将支付状态标记为失败。 调用方通过订阅 `callbackName`(订阅参数为消费方方法的返回值) 即可接收到支付服务消费者方法的返回结果,从而进行补偿的业务处理。

下面是使用方法:

```C#

// 发送方
_capBus.Publish("xxx.services.show.time",DaateTime.Now,"callback-show-execute-time");

[CapSubscribe("callback-show-execute-time")] //对应发送的 callbackName
public void ShowPublishTimeAndReturnExecuteTime(DateTime time)
{
Console.WriteLine(time); // 这是订阅方返回的时间
}

//--------------------------------------------------------------------------------

//订阅方
[CapSubscribe("xxx.services.show.time")]
public DateTime ShowPublishTimeAndReturnExecuteTime(DateTime time)
{
Console.WriteLine(time); // 这是发送的时间

return DateTime.Now; // 这是消费者返回的时间,CAP会取该方法的返回值用来传递到发送方的回调订阅里面
}

```

#### 事务

事务在 CAP 具有重要作用,它是保证消息可靠性的一个基石。 在发送一条消息到消息队列的过程中,如果不使用事务,我们是没有办法保证我们的业务代码在执行成功后消息已经成功的发送到了消息队列,或者是消息成功的发送到了消息队列,但是业务代码确执行失败。

这里的失败原因可能是多种多样的,比如连接异常,网络故障等等。

*只有业务代码和CAP的Publish代码必须在同一个事务中,才能够保证业务代码和消息代码同时成功或者失败。*

以下是两种使用事务进行Publish的代码:

* EntityFramework

```cs
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: false)
{
//业务代码

_capBus.Publish("xxx.services.show.time", DateTime.Now);

trans.Commit();
}
```

在不使用自动提交的时候,你的业务代码可以位于 Publish 之前或者之后,只需要保证在同一个事务。

当使用自动提交时候,需要确保 `_capBus.Publish` 位于代码的最后。

其中,发送的内容会序列化为Json存储到消息表中。

* Dapper

```cs
using (var connection = new MySqlConnection(ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: false))
{
//your business code
connection.Execute("insert into test(name) values('test')", transaction: (IDbTransaction)transaction.DbTransaction);

_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);

transaction.Commit();
}
}
```

### 订阅/消费

**注意:框架无法做到100%确保消息只执行一次,所以在一些关键场景消息端在方法实现的过程中自己保证幂等性。**

使用 `CapSubscribeAttribute` 来订阅 CAP 发布出去的消息。

```
[CapSubscribe("xxx.services.bar")]
public void BarMessageProcessor()
{
}

```

这里,你也可以使用多个 `CapSubscribe[""]` 来同时订阅多个不同的消息 :

```
[CapSubscribe("xxx.services.bar")]
[CapSubscribe("xxx.services.foo")]
public void BarAndFooMessageProcessor()
{
}

```

其中,`xxx.services.bar` 为订阅的消息名称,内部实现上,这个名称在不同的消息队列具有不同的代表。 在 Kafka 中,这个名称即为 Topic Name。 在RabbitMQ 中,为 RouteKey。

> RabbitMQ 中的 RouteKey 支持绑定键表达式写法,有两种主要的绑定键:
>
> \*(星号)可以代替一个单词.
>
> \# (井号) 可以代替0个或多个单词.
>
> 比如在下面这个图中(P为发送者,X为RabbitMQ中的Exchange,C为消费者,Q为队列)
>
> ![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170807093230268-283915002.png)
>
> 在这个示例中,我们将发送一条关于动物描述的消息,也就是说 Name(routeKey) 字段中的内容包含 3 个单词。第一个单词是描述速度的(celerity),第二个单词是描述颜色的(colour),第三个是描述哪种动物的(species),它们组合起来类似:“<celerity>.<colour>.<species>”。
>
> 然后在使用 `CapSubscribe` 绑定的时候,Q1绑定为 `CapSubscribe["*.orange.*"]`, Q2 绑定为 `CapSubscribe["*.*.rabbit"]` 和 `[CapSubscribe["lazy.#]`。
>
> 那么,当发送一个名为 "quick.orange.rabbit" 消息的时候,这两个队列将会同时收到该消息。同样名为 `lazy.orange.elephant`的消息也会被同时收到。另外,名为 "quick.orange.fox" 的消息将仅会被发送到Q1队列,名为 "lazy.brown.fox" 的消息仅会被发送到Q2。"lazy.pink.rabbit" 仅会被发送到Q2一次,即使它被绑定了2次。"quick.brown.fox" 没有匹配到任何绑定的队列,所以它将会被丢弃。
>
> 另外一种情况,如果你违反约定,比如使用 4个单词进行组合,例如 "quick.orange.male.rabbit",那么它将匹配不到任何的队列,消息将会被丢弃。
>
> 但是,假如你的消息名为 "lazy.orange.male.rabbit",那么他们将会被发送到Q2,因为 #(井号)可以匹配 0 或者多个单词。


在 CAP 中,我们把每一个拥有 `CapSubscribe[]`标记的方法叫做**订阅者**,你可以把订阅者进行分组。

**组(Group)**,是订阅者的一个集合,每一组可以有一个或者多个消费者,但是一个订阅者只能属于某一个组。同一个组内的订阅者订阅的消息只能被消费一次。

如果你在订阅的时候没有指定组,CAP会将订阅者设置到一个默认的组,默认的组名称为 `cap.queue.{程序集名称}`。

以下是使用组进行订阅的示例:

```cs
[CapSubscribe("xxx.services.foo", Group = "moduleA")]
public void FooMessageProcessor()
{
}

```

#### 例外情况

这里有几种情况可能需要知道:

**① 消息发布的时候订阅方还未启动**

Kafka:

当 Kafka 中,发布的消息存储于持久化的日志文件中,所以消息不会丢失,当订阅者所在的程序启动的时候会消费掉这些消息。

RabbitMQ:

在 RabbitMQ 中,应用程序**首次启动**会创建具有持久化的 Exchange 和 Queue,CAP 会针对每一个订阅者Group会新建一个消费者队列,**由于首次启动时候订阅者未启动的所以是没有队列的,消息无法进行持久化,这个时候生产者发的消息会丢失**。

针对RabbitMQ的消息丢失的问题,有两种解决方式:

i. 部署应用程序之前,在RabbitMQ中手动创建具有durable特性的Exchange和Queue,默认情况他们的名字分别是(cap.default.topic, cap.default.group)。

ii. 提前运行一遍所有实例,让Exchange和Queue初始化。

我们建议采用第 ii 种方案,因为很容易做到。

+ 156
- 0
docs/user-guide-cn/configuration.md View File

@@ -0,0 +1,156 @@
CAP 使用 Microsoft.Extensions.DependencyInjection 进行配置的注入,你也可以依赖于 DI 从json文件中读取配置。

### Cap Options

你可以使用如下方式来配置 CAP 中的一些配置项,例如

```cs
services.AddCap(capOptions => {
capOptions.FailedCallback = //...
});

```

`CapOptions` 提供了以下配置项:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
DefaultGroup | 订阅者所属的默认消费者组 | string | cap.queue+程序集名称
SuccessedMessageExpiredAfter | 成功的消息被删除的过期时间 | int | 3600 秒
FailedCallback| 执行失败消息时的回调函数,详情见下文 | Action | NULL
FailedRetryInterval | 失败重试间隔时间 | int | 60 秒
FailedRetryCount | 失败最大重试次数 | int | 50 次

CapOptions 提供了 `FailedCallback` 为处理失败的消息时的回调函数。当消息多次发送失败后,CAP会将消息状态标记为`Failed`,CAP有一个专门的处理者用来处理这种失败的消息,针对失败的消息会重新放入到队列中发送到MQ,在这之前如果`FailedCallback`具有值,那么将首先调用此回调函数来告诉客户端。

FailedCallback 的类型为 `Action<MessageType,string,string>`,第一个参数为消息类型(发送的还是接收的),第二个参数为消息的名称(name),第三个参数为消息的内容(content)。

### RabbitMQ Options

CAP 采用的是针对 CapOptions 进行扩展来实现RabbitMQ的配置功能,所以针对 RabbitMQ 的配置用法如下:

```cs
services.AddCap(capOptions => {
capOptions.UseRabbitMQ(rabbitMQOption=>{
// rabbitmq options.
});
});
```

`RabbitMQOptions` 提供了有关RabbitMQ相关的配置:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
HostName | 宿主地址 | string | localhost
UserName | 用户名 | string | guest
Password | 密码 | string | guest
VirtualHost | 虚拟主机 | string | /
Port | 端口号 | int | -1
TopicExchangeName | CAP默认Exchange名称 | string | cap.default.topic
RequestedConnectionTimeout | RabbitMQ连接超时时间 | int | 30,000 毫秒
SocketReadTimeout | RabbitMQ消息读取超时时间 | int | 30,000 毫秒
SocketWriteTimeout | RabbitMQ消息写入超时时间 | int | 30,000 毫秒
QueueMessageExpires | 队列中消息自动删除时间 | int | (10天) 毫秒

### Kafka Options

CAP 采用的是针对 CapOptions 进行扩展来实现 Kafka 的配置功能,所以针对 Kafka 的配置用法如下:

```cs
services.AddCap(capOptions => {
capOptions.UseKafka(kafkaOption=>{
// kafka options.
// kafkaOptions.MainConfig.Add("", "");
});
});
```

`KafkaOptions` 提供了有关 Kafka 相关的配置,由于Kafka的配置比较多,所以此处使用的是提供的 MainConfig 字典来支持进行自定义配置,你可以查看这里来获取对配置项的支持信息。

[https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md)


### EntityFramework Options

如果使用的 Entityframework 来作为消息持久化存储的话,那么你可以在配置 CAP EntityFramework 配置项的时候来自定义一些配置。

```cs
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>(efOption =>
{
// entityframework options.
});
});

```

注意,如果你使用了 `UseEntityFramework` 的配置项,那么你不需要再次配置下面的章节几个针对不同数据库的配置,CAP 将会自动读取 DbContext 中使用的数据库相关配置信息。

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Schema | Cap表架构 | string | Cap (SQL Server)
Schema | Cap表架构 | string | cap (PostgreSql)
TableNamePrefix | Cap表前缀 | string | cap (MySql)


### SqlServer Options

注意,如果你使用的是 EntityFramewrok,你用不到此配置项。

CAP 采用的是针对 CapOptions 进行扩展来实现 SqlServer 的配置功能,所以针对 SqlServer 的配置用法如下:

```cs
services.AddCap(capOptions => {
capOptions.UseSqlServer(sqlserverOptions => {
// sqlserverOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Schema | Cap表架构 | string | Cap
ConnectionString | 数据库连接字符串 | string | null


### MySql Options

注意,如果你使用的是 EntityFramewrok,你用不到此配置项。

CAP 采用的是针对 CapOptions 进行扩展来实现 MySql 的配置功能,所以针对 MySql 的配置用法如下:

```cs
services.AddCap(capOptions => {
capOptions.UseMySql(mysqlOptions => {
// mysqlOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
TableNamePrefix | Cap表名前缀 | string | cap
ConnectionString | 数据库连接字符串 | string | null

### PostgreSql Options

注意,如果你使用的是 EntityFramewrok,你用不到此配置项。

CAP 采用的是针对 CapOptions 进行扩展来实现 PostgreSql 的配置功能,所以针对 PostgreSql 的配置用法如下:

```cs
services.AddCap(capOptions => {
capOptions.UsePostgreSql(postgreOptions => {
// postgreOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:---
Schema | Cap表名前缀 | string | cap
ConnectionString | 数据库连接字符串 | string | null

+ 55
- 0
docs/user-guide-cn/design-principle.md View File

@@ -0,0 +1,55 @@
### 动机

随着微服务架构的流行,越来越多的人在尝试使用微服务来架构他们的系统,而在这其中我们会遇到例如分布式事务的问题,为了解决这些问题,我没有发现简单并且易于使用的解决方案,所以我决定来打造这样一个库来解决这个问题。

最初 CAP 是为了解决分布式系统中的事务问题,她采用的是 异步确保 这种弱一致性事务机制实现了分布式事务的最终一致性,更多这方面的信息可以查看第6节。

现在 CAP 除了解决分布式事务的问题外,她另外一个重要的功能就是作为 EventBus 来使用,她具有 EventBus 的所有功能,并且提供了更加简化的方式来处理EventBus中的发布/订阅。

### 持久化

CAP 依靠本地数据库实现消息的持久化,CAP 使用这种方式来应对一切环境或者网络异常导致消息丢失的情况,消息的可靠性是分布式事务的基石,所以在任何情况下消息都不能丢失。

对于消息的持久化分为两种:

**① 消息进入消息队列之前的持久化**

在消息进入到消息队列之前,CAP使用本地数据库表对消息进行持久化,这样可以保证当消息队列出现异常或者网络错误时候消息是没有丢失的。

为了保证这种机制的可靠性,CAP使用和业务代码相同的数据库事务来保证业务操作和CAP的消息在持久化的过程中是强一致的。也就是说在进行消息持久化的过程中,任何一方发生异常情况数据库都会进行回滚操作。

**② 消息进入到消息队列之后的持久化**

消息进入到消息队列之后,CAP会启动消息队列的持久化功能,我们需要说明一下在 RabbitMQ 和 Kafka 中CAP的消息是如何持久化的。

针对于 RabbitMQ 中的消息持久化,CAP 使用的是具有消息持久化功能的消费者队列,但是这里面可能有例外情况,参加 2.2.1 章节。

由于 Kafka 天生设计的就是使用文件进行的消息持久化,在所以在消息进入到Kafka之后,Kafka会保证消息能够正确被持久化而不丢失。

### 通讯数据流

CAP 中消息的流转过程大致如下:

**2.2版本以前**

![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170803174645928-1813351415.png)

> “ P ” 代表消息发送者(生产者)。 “ C ” 代表消息消费者(订阅者)。

**2.2版本以后**

在2.2以后的版本中,我们调整了一些消息的流转流程,我们移除了数据库中的 Queue 表使用内存队列来代替,详情见:[Improve the implementation mechanism of queue mode](https://github.com/dotnetcore/CAP/issues/96)

### 一致性

CAP 采用最终一致性作为的一致性方案,此方案是遵循 CAP 理论,以下是CAP理论的描述。

C(一致性)一致性是指数据的原子性,在经典的数据库中通过事务来保障,事务完成时,无论成功或回滚,数据都会处于一致的状态,在分布式环境下,一致性是指多个节点数据是否一致;

A(可用性)服务一直保持可用的状态,当用户发出一个请求,服务能在一定的时间内返回结果;

P(分区容忍性)在分布式应用中,可能因为一些分布式的原因导致系统无法运转,好的分区容忍性,使应用虽然是一个分布式系统,但是好像一个可以正常运转的整体

根据 [“CAP”分布式理论](https://en.wikipedia.org/wiki/CAP_theorem), 在一个分布式系统中,我们往往为了可用性和分区容错性,忍痛放弃强一致支持,转而追求最终一致性。大部分业务场景下,我们是可以接受短暂的不一致的。

第 6 节将对此做进一步介绍。

+ 13
- 0
docs/user-guide-cn/distributed-transactions.md View File

@@ -0,0 +1,13 @@
针对于分布式事务的处理,CAP 采用的是“异步确保”这种方案。

### 异步确保

异步确保这种方案又叫做本地消息表,这是一种经典的方案,方案最初来源于 eBay,参考资料见段末链接。这种方案目前也是企业中使用最多的方案之一。

相对于 TCC 或者 2PC/3PC 来说,这个方案对于分布式事务来说是最简单的,而且它是去中心化的。在TCC 或者 2PC 的方案中,必须具有事务协调器来处理每个不同服务之间的状态,而此种方案不需要事务协调器。
另外 2PC/TCC 这种方案如果服务依赖过多,会带来管理复杂性增加和稳定性风险增大的问题。试想如果我们强依赖 10 个服务,9 个都执行成功了,最后一个执行失败了,那么是不是前面 9 个都要回滚掉?这个成本还是非常高的。

但是,并不是说 2PC 或者 TCC 这种方案不好,因为每一种方案都有其相对优势的使用场景和优缺点,这里就不做过多介绍了。

> 中文:[http://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html](http://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html)
> 英文:[http://queue.acm.org/detail.cfm?id=1394128](http://queue.acm.org/detail.cfm?id=1394128)

+ 51
- 0
docs/user-guide-cn/faq.md View File

@@ -0,0 +1,51 @@
**1、有CAP学习QQ群吗?**

CAP没有群。
原因是我希望培养大家独立思考和遇到问题时候的解决能力。
使用时候遇到问题先尝试看文档和独立解决,如果解决不了,可以提ISSUE或者发邮件。
QQ群有效沟通太低,浪费时间。

**2、CAP要求发送者与接收者必须使用不同的数据库吗?**

没有要求,要求不同的实例使用不同的数据库。不同实例的意思为,不同代码的两套程序。

但是如果你真的需要在不同的实例使用相同的数据库,那么可以参考下面3的答案。


**3、CAP如何在不同的实例中使用相同的数据库?**

如果想在不同的实例(程序)中连接相同的数据库,那么你可以在配置CAP的时候通过指定不同的数据库表名前缀来实现。

你可以通过以下方式来指定数据库表名前缀:

```cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCap(x =>
{
x.UseKafka("");
x.UseMySql(opt =>
{
opt.ConnectionString = "connection string";
opt.TableNamePrefix = "appone"; // 在这里配置不同的实例使用的表名前缀
});
});
}

```
注意:相同的实例不需要指定不同的表名称前缀,他们在接收消息的时候会进行负载均衡。

**4、CAP可以不使用数据库吗? 我仅仅是想通过她来传递消息,我可以接受消息丢失的情况**

目前是不可以的。

CAP 的设计目标即为在不同的微服务或者SOA系统中来保持数据一致性的一种解决方案,保证这种数据一致性方案的前提是利用了传统数据库的 ACID 特性,如果离开了数据库,那么CAP仅仅是一个消息队列的客户端封装,这没有任何意义。


**5、使用CAP时候,业务出现错误怎么回滚?**

不能回滚,CAP是最终一致性的方案。

你可以想象你的场景为在调用第三方支付,假如你在进行一项第三方支付的操作,调用支付宝的接口成功后,而你自己的代码出现错误了,支付宝会回滚吗? 如果不回滚那么是又应该怎么处理呢? 这里也是同理。



+ 81
- 0
docs/user-guide-cn/getting-started.md View File

@@ -0,0 +1,81 @@
### 介绍

CAP 是一个遵循 .NET Standard 标准库的C#库,用来处理分布式事务以及提供EventBus的功能,她具有轻量级,高性能,易使用等特点。

目前 CAP 使用的是 .NET Standard 1.6 的标准进行开发,目前最新预览版本已经支持 .NET Standard 2.0.

### 应用场景

CAP 的应用场景主要有以下两个:

* 1. 分布式事务中的最终一致性(异步确保)的方案。

分布式事务是在分布式系统中不可避免的一个硬性需求,而目前的分布式事务的解决方案也无外乎就那么几种,在了解 CAP 的分布式事务方案前,可以阅读以下 [这篇文章](http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency)。

CAP 没有采用两阶段提交(2PC)这种事务机制,而是采用的 本地消息表+MQ 这种经典的实现方式,这种方式又叫做 异步确保。

* 2. 具有高可用性的 EventBus。

CAP 实现了 EventBus 中的发布/订阅,它具有 EventBus 的所有功能。也就是说你可以像使用 EventBus 一样来使用 CAP,另外 CAP 的 EventBus 是具有高可用性的,这是什么意思呢?

CAP 借助于本地消息表来对 EventBus 中的消息进行了持久化,这样可以保证 EventBus 发出的消息是可靠的,当消息队列出现宕机或者连接失败的情况时,消息也不会丢失。

### Quick Start

* **引用 NuGet 包**

使用一下命令来引用CAP的NuGet包:

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

根据使用的不同类型的消息队列,来引入不同的扩展包:

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

根据使用的不同类型的数据库,来引入不同的扩展包:

```
PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql
PM> Install-Package DotNetCore.CAP.MongoDB
```

* **启动配置**

在 ASP.NET Core 程序中,你可以在 `Startup.cs` 文件 `ConfigureServices()` 中配置 CAP 使用到的服务:

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

services.AddDbContext<AppDbContext>(); //Options, If you are using EF as the ORM
services.AddSingleton<IMongoClient>(new MongoClient("")); //Options, If you are using MongoDB

services.AddCap(x =>
{
// If you are using EF, you need to add the configuration:
x.UseEntityFramework<AppDbContext>(); //Options, Notice: You don't need to config x.UseSqlServer(""") again! CAP can autodiscovery.

// If you are using Ado.Net, you need to add the configuration:
x.UseSqlServer("Your ConnectionStrings");
x.UseMySql("Your ConnectionStrings");
x.UsePostgreSql("Your ConnectionStrings");

// If you are using MongoDB, you need to add the configuration:
x.UseMongoDB("Your ConnectionStrings"); //MongoDB 4.0+ cluster

// If you are using RabbitMQ, you need to add the configuration:
x.UseRabbitMQ("localhost");

// If you are using Kafka, you need to add the configuration:
x.UseKafka("localhost");
});
}
```

+ 88
- 0
docs/user-guide-cn/implementation-mechanisms.md View File

@@ -0,0 +1,88 @@
CAP 封装了在 ASP.NET Core 中的使用依赖注入来获取 Publisher (`ICapPublisher`)的接口。而启动方式类似于 “中间件” 的形式,通过在 Startup.cs 配置 `ConfigureServices` 和 `Configure` 进行启动。

### 消息表

当系统引入CAP之后并首次启动后,CAP会在客户端生成 2 个表,分别是 Cap.Published, Cap.Received 。注意表名可能在不同的数据库具有不同的大小写区分,如果你在运行项目的时候没有显式的指定数据库生成架构(Schema)或者表名前缀(TableNamePrefix)的话,默认情况下就是以上的名字。

**Cap.Published**:这个表主要是用来存储 CAP 发送到MQ(Message Queue)的客户端消息,也就是说你使用 `ICapPublisher` 接口 Publish 的消息内容。

**Cap.Received**:这个表主要是用来存储 CAP 接收到 MQ(Message Queue) 的客户端订阅的消息,也就是使用 `CapSubscribe[]` 订阅的那些消息。

> 2.2 版本以前:
> **Cap.Queue**: 这个表主要是CAP内部用来处理发送和接收消息的一个临时表,通常情况下,如果系统不出现问题,这个表将是空的。

`Published` 和 `Received` 表具有 StatusName 字段,这个字段用来标识当前消息的状态。目前共有 `Scheduled`,`Successed`,`Failed` 等几个状态。

> 在 2.2 版本以前的所有状态为:`Scheduled`,`Enqueued`,`Processing`,`Successed`,`Failed`

CAP 在处理消息的过程中会依次从`Scheduled` 到 `Successed` 来改变这些消息状态的值。如果是状态值为 `Successed`,代表该消息已经成功的发送到了 MQ 中。如果为 Failed 则代表消息发送失败。

CAP 2.2 以上版本中会针对 `Scheduled`,`Failed` 状态的消息 CAP 会于消息持久化过后 4 分钟后开始进行重试,重试的间隔默认为 60 秒,你可以在 `CapOptions` 中配置的`FailedRetryInterval` 来调整默认间隔时间。

> 2.2 版本以前, CAP 会对状态为 `Failed` 的消息默认进行 100 次重试。

### 消息格式

CAP 采用 JSON 格式进行消息传输,以下是消息的对象模型:

NAME | DESCRIPTION | TYPE
:---|:---|:---
Id | 消息编号 | int
Version | 消息版本 | string
Name | 消息名称 | string
Content | 内容 | string
Group | 所属消费组 | string
Added | 创建时间 | DateTime
ExpiresAt | 过期时间 | DateTime
Retries | 重试次数 | int
StatusName | 状态 | string

>对于 Cap.Received 中的消息,会多一个 `Group` 字段来标记所属的消费者组。

对于消息内容 Content 属性里面的内容CAP 使用 Message 对象进行了一次二次包装。一下为Message对象的信息

NAME | DESCRIPTION | TYPE
:---|:---|:---
Id | CAP生成的消息编号 | string
Timestamp | 消息创建时间 | string
Content | 内容 | string
CallbackName | 回调的订阅者名称 | string

其中 Id 字段,CAP 采用的 MongoDB 中的 ObjectId 分布式Id生成算法生成。

### EventBus

EventBus 采用 发布-订阅 风格进行组件之间的通讯,它不需要显式在组件中进行注册。

![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170804153901240-1774287236.png)

上图是EventBus的一个Event的流程,关于 EventBus 的更多信息就不在这里介绍了...

在 CAP 中,为什么说 CAP 实现了 EventBus 中的全部特性,因为 EventBus 具有的两个大功能就是发布和订阅, 在 CAP 中 使用了另外一种优雅的方式来实现的,另外一个 CAP 提供的强大功能就是消息的持久化,以及在任何异常情况下消息的可靠性,这是EventBus不具有的功能。

![](https://camo.githubusercontent.com/452505edb71d41f2c1bd18907275b76291621e46/687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3235303431372f3230313730372f3235303431372d32303137303730353137353832373132382d313230333239313436392e706e67)

CAP 里面发送一个消息可以看做是一个 “Event”,一个使用了CAP的ASP.NET Core 应用程序既可以进行发送也可以进行订阅接收。


### 重试

重试在整个CAP架构设计中具有重要作用,CAP 中会针对发送失败或者执行失败的消息进行重试。在整个 CAP 的设计过程中有以下几处采用的重试策略。

**1、 发送重试**

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

> 你可以在 `CapOptions` 中设置`FailedRetryCount`来调整默认重试的总次数。

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

**2、 消费重试**

当 Consumer 接收到消息时,会执行消费者方法,在执行消费者方法出现异常时,会进行重试。这个重试策略和上面的 `发送重试` 是相同的。

### 数据清理

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

CAP 默认情况下会每隔一个小时将消息表的数据进行清理删除,避免数据量过多导致性能的降低。清理规则为 ExpiresAt 不为空并且小于当前时间的数据。 也就是说状态为`Failed`的消息(正常情况他们已经被重试了 50 次),如果你15天没有人工介入处理,同样会被清理掉。

+ 204
- 0
docs/user-guide/api-interface.md View File

@@ -0,0 +1,204 @@
## Interfaces
CAP only has one interface,It is `ICapPublisher`,You can get its instance from the DI container and then call it.

### Publish/Send

You can use the `Publish<T>` or `PublishAsync<T>` methods defined in the `ICapPublisher` interface to send the event messages.

```cs
public class PublishController : Controller
{
private readonly ICapPublisher _capBus;

public PublishController(ICapPublisher capPublisher)
{
_capBus = capPublisher;
}

[Route("~/adonet/transaction")]
public IActionResult AdonetWithTransaction()
{
using (var connection = new MySqlConnection(ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true))
{
//your business code

_capBus.Publish("xxx.services.show.time", DateTime.Now);
}
}

return Ok();
}

[Route("~/ef/transaction")]
public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
{
//your business code

_capBus.Publish("xxx.services.show.time", DateTime.Now);
}

return Ok();
}
}

```
The following is the signature of the of the PublishAsync method

**`PublishAsync<T>(string name,T object)`**

By default,when this method(PublishAsync<T>) is called,CAP will create a transaction internally,
and then write messages into the `Cap.Published` message table.

In some situations,you may need a callback when a message is sent out, you can use the follwing
overload of the `PublishAsync<T>` method:

**`PublishAsync<T>(string name,T object, string callBackName)`**

In this overload method,`callbackName` is the callback name of the subscription method,when the consumption-side finished processing messages,CAP will return the processed result and also call the specified subscription method

#### Transactions

Transaction plays a very import role in CAP, It is a main factor to ensure the reliability of messaging.

In the process of sending a message to the message queue without transaction we can not ensure that messages are sent to the message queue successfully after we finish dealing the business logic,or messages are send to the message queque successfully but our bussiness logic is failed.

There is a variety of reasons that causing failure,eg:connection errors,network errors,etc.

*Only by putting the business logic and logic in the Publish of CAP in the same transaction so that we can enssure both them to be success or fail*

The following two blocks of code snippet demonstrate how to use transactions in EntityFramework and dapper when publishing messages.


* EntityFramework

```cs
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: false)
{
// Your business logic。

_capBus.Publish("xxx.services.show.time", DateTime.Now);

trans.Commit();
}

```
When you set the `autoCommit: false`, you can put your business logic before or after the Publish logic,the only thing you need to do is to ensure that they are in the same transaction.

If you set the `autoCommit: true`, you need publish message `_capBus.Publish` at the last.

During the course,the message content will be serialized as json and stored in the message table.

* Dapper

```cs
using (var connection = new MySqlConnection(ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: false))
{
//your business code
connection.Execute("insert into test(name) values('test')", transaction: (IDbTransaction)transaction.DbTransaction);

_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);

transaction.Commit();
}
}

```

### Subscribe/Consume

**NOTE: The businsess logics in the subscription side should be keep idempotent.**

Use `CapSubscribe[""]` to decorate a method so that it can subscribe messages published by CAP.

```
[CapSubscribe("xxx.services.bar")]
public void BarMessageProcessor()
{
}

```
You can also use multiple `CapSubscribe[""]` to decorate a method so that you can subscribe messages from different sources accordingly.

```
[CapSubscribe("xxx.services.bar")]
[CapSubscribe("xxx.services.foo")]
public void BarAndFooMessageProcessor()
{
}

```
`xxx.services.bar` is the name of the message to be subscribed.And it has different name in different message queque Clients.for example,in kafka the name is called Topic Name and in RAbbitMQ it is called RouteKey.

>In RabbitMQ you can use regular expression in RouteKey:


>
> \*(Asterisk)stands for a single word.
>
> \# (hash sign) standards for zero or more words.
>
>See the following picture(P for Publisher,X for Exchange,C for consumer and Q for Queue)
>
> ![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170807093230268-283915002.png)

>In this example, we're going to send messages which all describe animals. The messages will be sent with a routing key that consists of three words (two dots). The first word in the routing key will describe a celerity, second a colour and third a species: "<celerity>.<colour>.<species>".

>We created three bindings: Q1 is bound with binding key "*.orange.*" and Q2 with "*.*.rabbit" and "lazy.#".

>These bindings can be summarised as:

>Q1 is interested in all the orange animals.
Q2 wants to hear everything about rabbits, and everything about lazy animals.
A message with a routing key set to "quick.orange.rabbit" will be delivered to both queues. Message "lazy.orange.elephant" also will go to both of them. On the other hand "quick.orange.fox" will only go to the first queue, and "lazy.brown.fox" only to the second. "lazy.pink.rabbit" will be delivered to the second queue only once, even though it matches two bindings. "quick.brown.fox" doesn't match any binding so it will be discarded.

>What happens if we break our contract and send a message with one or four words, like "orange" or "quick.orange.male.rabbit"? Well, these messages won't match any bindings and will be lost.

>On the other hand "lazy.orange.male.rabbit", even though it has four words, will match the last binding and will be delivered to the second queue.

In CAP,we called a method decorated by `CapSubscribe[]` a **subscriber**,you can group different subscribers.

**Group** is a collection of subscribers,each group can have one or multiple consumers,but a subscriber can only belongs to a certain group(you can not put a subscriber into multiple groups).Messages subscribed by members in a certain group can only be consumed once.

If you do not specify any group when subscribing,CAP will put the subscriber into a default group named `cap.default.group`

the following is a demo shows how to use group when subscribing.

```cs
[CapSubscribe("xxx.services.foo", Group = "moduleA")]
public void FooMessageProcessor()
{
}

```

#### Exceptional case

The following situations you shoud be aware of.

**① the subscription side has not started yet when publishing a message**

Kafka:

In Kafka,published messages stored in the Persistent log files,so messages will not lost.when the subscription side started,it can still consume the message.


RabbitMQ:

In RabbitMQ,the application will create Persistent Exchange and Queue at the **first start**,CAP will create a new consumer queue for each consumer group,**because the application started but the subscription side hasn's start yet so there has no queue,thus the message can not be persited,and the published messages will lost**

There are two ways to solve this `message lost` issue in RamitMQ:

i.Before the deployment of your application,you can create durable Exchange and Queue in RabbitMQ by hand,the default names them are (cap.default.topic, cap.default.group).

ii.Run all instances in advance to ensure that both Exchange and Queue are initialized.

It is highly recommanded that users adopt the second way,because it is easier to achieve.

+ 153
- 0
docs/user-guide/configuration.md View File

@@ -0,0 +1,153 @@
CAP uses Microsoft.Extensions.DependencyInjection for configuration injection.

### Cap Options

You can use the following methods to configure some configuration items in the CAP, for example:

```cs
services.AddCap(capOptions => {
capOptions.FailedCallback = //...
});

```

`CapOptions` provides the following configuration items::

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
DefaultGroup | Default consumer group to which the subscriber belongs | string | cap.queue+{assembly name}
SuccessedMessageExpiredAfter | Expiration date after successful message was deleted | int | 3600 seconds
FailedCallback|Callback function when the failed message is executed. See below for details | Action | NULL
FailedRetryInterval | Failed Retry Interval | int | 60 seconds
FailedRetryCount | Failed RetryCount | int | 50th

CapOptions provides a callback function for `FailedCallback` to handle failed messages. When the message fails to be sent multiple times, the CAP will mark the message state as `Failed`. The CAP has a special handler to handle this failed message. The failed message will be put back into the queue and sent to MQ. Prior to this, if `FailedCallback` has a value, this callback function will be called first to tell the client.

The type of FailedCallback is `Action<MessageType,string,string>`. The first parameter is the message type (send or receive), the second parameter is the name of the message, and the third parameter is the content of the message.

### RabbitMQ Options

The CAP uses the CapOptions extension to implement the RabbitMQ configuration function. Therefore, the configuration of the RabbitMQ is used as follows:

```cs
services.AddCap(capOptions => {
capOptions.UseRabbitMQ(rabbitMQOption=>{
// rabbitmq options.
});
});
```
`RabbitMQOptions` provides related RabbitMQ configuration:

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
HostName | Host Address | string | localhost
UserName | username | string | guest
Password | Password | string | guest
VirtualHost | Virtual Host | string | /
Port | Port number | int | -1
TopicExchangeName | CAP Default Exchange Name | string | cap.default.topic
RequestedConnectionTimeout | RabbitMQ Connection Timeout | int | 30,000 milliseconds
SocketReadTimeout | RabbitMQ message read timeout | int | 30,000 milliseconds
SocketWriteTimeout | RabbitMQ message write timeout | int | 30,000 milliseconds
QueueMessageExpires | Automatic deletion of messages in queue | int | (10 days) ms

### Kafka Options

CAP adopts Kafka's configuration function to expand CapOptions, so the configuration usage for Kafka is as follows:

```cs
services.AddCap(capOptions => {
capOptions.UseKafka(kafkaOption=>{
// kafka options.
// kafkaOptions.MainConfig.Add("", "");
});
});
```

`KafkaOptions` provides Kafka-related configurations. Because Kafka has more configurations, the MainConfig dictionary provided here is used to support custom configurations. You can check here to get support information for configuration items.

[https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md)


### EntityFramework Options

If you are using Entityframework as a message persistence store, then you can customize some configuration when configuring the CAP EntityFramework configuration item.

```cs
services.AddCap(x =>
{
x.UseEntityFramework<AppDbContext>(efOption =>
{
// entityframework options.
});
});

```

Note that if you use the `UseEntityFramework` configuration item, then you do not need to reconfigure the following sections for several different database configurations. The CAP will automatically read the database configuration information used in DbContext.

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
Schema | Cap table schema | string | Cap (SQL Server)
Schema | Cap table schema | string | cap (PostgreSql)
TableNamePrefix | Cap table name prefix | string | cap (MySql)

### SqlServer Options

Note that if you are using EntityFramewrok, you do not use this configuration item.

CAP adopts the configuration function of SqlServer for extending CapOptions. Therefore, the configuration usage of SqlServer is as follows:

```cs
services.AddCap(capOptions => {
capOptions.UseSqlServer(sqlserverOptions => {
// sqlserverOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
Schema | Cap Table Schema | string | Cap
ConnectionString | Database connection string | string | null

### MySql Options

Note that if you are using EntityFramewrok, you do not use this configuration item.

CAP uses the configuration function for MySql that extends for CapOptions, so the configuration usage for MySql is as follows:

```cs
services.AddCap(capOptions => {
capOptions.UseMySql(mysqlOptions => {
// mysqlOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
TableNamePrefix | Cap table name prefix | string | cap
ConnectionString | Database connection string | string | null

### PostgreSql Options

Note that if you are using EntityFramewrok, you do not use this configuration item.

CAP uses PostgreSql configuration functions for CapOptions extensions, so the configuration usage for PostgreSql is as follows:

```cs
services.AddCap(capOptions => {
capOptions.UsePostgreSql(postgreOptions => {
// postgreOptions.ConnectionString
});
});

```

NAME | DESCRIPTION | TYPE | DEFAULT
:---|:---|---|:------
Schema | Cap table name prefix | string | cap
ConnectionString | Database connection string | string | null

+ 55
- 0
docs/user-guide/design-principle.md View File

@@ -0,0 +1,55 @@
### Motivation

With the popularity of microservices architecture, more and more people are trying to use microservices to architect their systems. In this we encounter problems such as distributed transactions. To solve these problems, I did not find simplicity and Easy to use solution, so I decided to create such a library to solve this problem.

The original CAP was to solve the transaction problems in the distributed system. She used asynchronous to ensure that this weak consistency transaction mechanism achieved the eventual consistency of the distributed transaction. For more information, see section 6.

Now in addition to solving distributed transaction problems, CAP's other important function is to use it as an EventBus. It has all of the features of EventBus and provides a more simplified way to handle publish/subscribe in EventBus.

### Persistence

The CAP relies on the local database for persistence of messages. The CAP uses this method to deal with situations in which all messages are lost due to environmental or network anomalies. The reliability of messages is the cornerstone of distributed transactions, so messages cannot be lost under any circumstances.

There are two types of persistence for messages:

**1 Persistence before the message enters the message queue**

Before the message enters the message queue, the CAP uses the local database table to persist the message. This ensures that the message is not lost when the message queue is abnormal or the network error occurs.

In order to ensure the reliability of this mechanism, CAP uses database transactions with the same business code to ensure that business operations and CAP messages are strongly consistent throughout the persistence process. That is to say, in the process of message persistence, the database of any abnormal situation will be rolled back.

**2 Persistence after messages enter the message queue**
After the message enters the message queue, the CAP starts the persistence function of the message queue. We need to explain how the message of the CAP in RabbitMQ and Kafka is persistent.

For message persistence in RabbitMQ, CAP uses a consumer queue with message persistence, but there may be exceptions to this and take part in 2.2.1.

Since Kafka is inherently designed to persist messages using files, Kafka ensures that messages are correctly persisted without loss after the message enters Kafka.

### Communication Data Streams

The flow of messages in the CAP is roughly as follows:

 >2.2 version before

![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170803174645928-1813351415.png)

> "P" represents the sender of the message (producer). "C" stands for message consumer (subscriber).

**After version 2.2**

In the 2.2 and later versions, we adjusted the flow of some messages. We removed the Queue table in the database and used the memory queue instead. For details, see: [Improve the implementation mechanism of queue mode](https://github.com/dotnetcore/CAP/issues/96)
### Consistency

The CAP uses the ultimate consistency as a consistent solution. This solution follows the CAP theory. The following is the description of the CAP theory.

C (consistent) consistency refers to the atomicity of data. It is guaranteed by transactions in a classic database. When a transaction completes, the data will be in a consistent state regardless of success or rollback. In a distributed environment, consistency is Indicates whether the data of multiple nodes is consistent;

A (availability) service is always available, when the user sends a request, the service can return the result within a certain time;

P (Partition Tolerance) In distributed applications, the system may not operate due to some distributed reasons. The good partition tolerance makes the application a distributed system but it seems to be a functioning whole.

According to ["CAP" distributed theory](https://en.wikipedia.org/wiki/CAP_theorem), in a distributed system, we often reluctantly give up strong consensus support for availability and partition fault tolerance, and instead pursue Ultimate consistency. In most business scenarios, we can accept short-term inconsistencies.

Section 6 will introduce this further.

+ 14
- 0
docs/user-guide/distributed-transactions.md View File

@@ -0,0 +1,14 @@
For the processing of distributed transactions, this CAP library matches the "Asynchronous recovery events" scenario.

### Asynchronous recovery events

As known as the name "native message table", this is a classic solution, originally from EBay, and referenced links about it are at the end of this section. This is also one of the most popular solutions in the business development.

Compared to TCC or 2pc/3pc, this solution is the simplest one for distributed transactions, and is decentralized. In TCC or 2PC solutions, the common transaction handlers synchronize the state among different services with a transaction coordinator, but it's not much required in this CAP solution. In addition, the deeper references of other conditions these services have, the more management complexity and stability risk may be increased in 2PC/TCC. Imagine that if we have 9 services committed successfully of all 10 whitch relied heavily, though the last one execute fail, should we roll back transactions of those 9 service? In fact, the cost is still very high.

However, it's not mean that 2PC or TCC are at a disadvantage, each has its own suitability and matched scenarios, here won't introduce more.


> cn: [base-an-acid-alternative](http://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html)
>
> en: [Base: An Acid Alternative](http://queue.acm.org/detail.cfm?id=1394128)

+ 40
- 0
docs/user-guide/faq.md View File

@@ -0,0 +1,40 @@
## 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.

### Does it require certain different databases, one each for productor and resumer in CAP?

Not requird differences necessary, a given advice is that using a special database for each program.

Otherwise, look at Q&A below.

### How to use the same database for different programs?

defining a prefix name of table in `ConfigureServices` method。
codes exsample:

```cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCap(x =>
{
x.UseKafka("");
x.UseMySql(opt =>
{
opt.ConnectionString = "connection string";
opt.TableNamePrefix = "appone"; // different table name prefix here
});
});
}
```

NOTE:different prefixed names cause SLB to no effect.

### If don't care about message missing, can message productor exist without any database, for the reason of sending message only.

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.

+ 83
- 0
docs/user-guide/getting-started.md View File

@@ -0,0 +1,83 @@
### Introduction

CAP is a library based on .Net standard, which is a solution to deal with distributed transactions, also has the function of EventBus, it is lightweight, easy to use, and efficiently.

### Usage Scenarios

The usage scenarios of CAP mainly include the following two:

* 1.The scheme of eventual consistency in distributed transactions

In the process of building an SOA or MicroService system, we usually need to use the event to integrate each services. In the process, the simple use of message queue does not guarantee the reliability. CAP is adopted the local message table program integrated with the current database to solve the exception may occur in the process of the distributed system calling each other. It can ensure that the event messages are not lost in any case.

Distributed transactions are an inevitable requirement in a distributed system, and the current solution for distributed transactions is nothing more than just a few. Before understanding the CAP's distributed transaction scenarios, you can read the following [Articles] (http://www.infoq.com/en/articles/solution-of-distributed-system-transaction-consistency).

The CAP does not use the two-phase commit (2PC) transaction mechanism, but uses the classical message implementation of the local message table + MQ, which is also called asynchronous guarantee.

* 2.Highly usable EventBus

You can also use the CAP as an EventBus. The CAP provides a simpler way to implement event publishing and subscriptions. You do not need to inherit or implement any interface during the process of subscription and sending.

CAP implements the publish and subscribe method of EventBus, which has all the features of EventBus. This means that you can use CAPs just like EventBus. In addition, CAP's EventBus is highly available. What does this mean?

The CAP uses the local message table to persist the messages in the EventBus. This ensures that the messages sent by the EventBus are reliable. When the message queue fails or fails, the messages are not lost.

### Quick Start

* **Reference NuGet Package**

Use the following command to reference the CAP NuGet package:

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

According to the different types of message queues used, different extension packages are introduced:

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

According to the different types of databases used, different extension packages are introduced:

```
PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql
PM> Install-Package DotNetCore.CAP.MongoDB
```

* **Startup Configuration**

In an ASP.NET Core program, you can configure the services used by the CAP in the `Startup.cs` file `ConfigureServices()`:

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

services.AddDbContext<AppDbContext>(); //Options, If you are using EF as the ORM
services.AddSingleton<IMongoClient>(new MongoClient("")); //Options, If you are using MongoDB

services.AddCap(x =>
{
// If you are using EF, you need to add the configuration:
x.UseEntityFramework<AppDbContext>(); //Options, Notice: You don't need to config x.UseSqlServer(""") again! CAP can autodiscovery.

// If you are using Ado.Net, you need to add the configuration:
x.UseSqlServer("Your ConnectionStrings");
x.UseMySql("Your ConnectionStrings");
x.UsePostgreSql("Your ConnectionStrings");

// If you are using MongoDB, you need to add the configuration:
x.UseMongoDB("Your ConnectionStrings"); //MongoDB 4.0+ cluster

// If you are using RabbitMQ, you need to add the configuration:
x.UseRabbitMQ("localhost");

// If you are using Kafka, you need to add the configuration:
x.UseKafka("localhost");
});
}
```

+ 90
- 0
docs/user-guide/implementation-mechanisms.md View File

@@ -0,0 +1,90 @@
Users can get a ICapPublisher interface from the ASP.NET Core DI container to publish a message .It is initialized by configurations in the `ConfigureServices` and `configure` method in the Startup.cs file,just like the way to initialize a `MiddleWare` in ASP.NET Core.

### Message Table

After initialized, CAP will create two tables in the client side,they are `Cap.Published` and `Cap.Received`. Please noted that different databases may deal letter case differently,if you do not explicitly specify the Schema or the TableName Prefix before project startup,the default names are the ones mentioned above.

**Cap.Published**:Used to store messages(Published by the `ICapPublisher` service) that CAP published to the MQ(Message Queue)Client side

**Cap.Received**:Used to Store messages(subscribed by the `CapSubscribe[]`) subscribed by the MQ(message Queue) client side that CAP received.


> Before version 2.2 :
**Cap.Queue**:A temporary table that CAP used to deal with published and received messages,under most circumstances(there aren't any errors),it is an empty table.

Both `Published` and `Received` tables have a `StatusName` field,which is used to mark the status of the current message.Until now it has `Scheduled`,`Successed` and `Failed` statuses.

> Statuses before version 2.2:`Scheduled`,`Enqueued`,`Processing`,`Successed` and `Failed`.

In the process of dealing with messages,CAP will change the status from `Scheduled` to `Successed`(or `Failed` ).if the final status is `Successed`,it means that the message is sent to MQ successfully,and `Failed` means the message is failed to sent to MQ.

Version later than 2.2, CAP will retry after 4 minutes if the status is `Scheduled` or `Failed`,the retry interval is default to 60 seconds.You can change it by modify `FailedRetryInterval` in `CapOptions`.

>Before version 2.2,CAP retry 100 times for `Failed` messages by default.

### Message format5

CAP use JSON to transfer message,the following is CAP's messaging object model:

NAME | DESCRIPTION | TYPE
:---|:---|:---
Id | Message Id | int
Version | Message Version | string
Name | Name | string
Content | Content | string
Group | Group a message belongs to | string
Added |add time | DateTime
ExpiresAt | expire time | DateTime
Retries | retry times | int
StatusName | Status Name | string

>for `Cap.Received`,there is an extra `Group` filed to mark which group the mesage belongs to.

>for the `Content` property CAP will use a Messsage object to wrap all the contents.The following shows details of the Message Object:


NAME | DESCRIPTION | TYPE
:---|:---|:---
Id | Generated by CAP | string
Timestamp | message create time | string
Content | content | string
CallbackName | the subscriber which is used to call back | string

CAP use the same algorithms as MongoDB ObjectId's distributed Id generation algorithms.

### EventBus

EventBus adopt the publish-subscribe messaging style to communicate with different components,and there is no need to register it in component explicitly.

![](http://images2017.cnblogs.com/blog/250417/201708/250417-20170804153901240-1774287236.png)

the diagram in the above link shows Eventbus's event flowchart,about EventBus,users can refer to other meterials to learn about it.

We say that CAP implement all the features in Eventbus,EventBus has two features:publish and subscribe,In CAP we implement them in an elegant way.Besides,CAP also has two very robust feature,they are message persistence and messaging reliability under any circumstances,But EventBus don't have such features.


![](https://camo.githubusercontent.com/452505edb71d41f2c1bd18907275b76291621e46/687474703a2f2f696d61676573323031352e636e626c6f67732e636f6d2f626c6f672f3235303431372f3230313730372f3235303431372d32303137303730353137353832373132382d313230333239313436392e706e67)

In CAP,send a message can be regarded as an "Event",When CAP is used in an ASP.NET Core applicaiton,the application has the ablity to publish as well as receive messages.

### Retry

Retry plays a very important role in CAP's infrastructure,CAP will retry for Failed messages.CAP has the following retry strategies:

**1、 Retry on sending**

in the process of sending a message,when the Broker crashed or connection failed or exceptions are thrown,CAP will retry,it will retry 3 times for the first time,if still failed,then it will retry every 1 minute,the retry the retry count +1,when the retry count come to 50,CAP will not retry any more.

>You can modify `FailedRetryCount` in `CapOptions` to change the default retry count.

As metioned above,when the retry count comes to a certain number,CAP will not retry anymore,this time,you can find out the fail reason in the Dashboard and they deal with it manually.

**2、 Retry on Consuming**

When consumer received messages,specified method in the consumer will be executed,if exceptions are thrown during this course,CAP will retry,the retry strategy is the same as above `Retry on sending`.

### Data clean out

table to store messages in database has an `ExpiresAt` field to mark the expiration time of the message. CAP will set `ExpiresAt` value as **1 hour** for `Successed` messages and **15days** for `Failed` messages.

To avoid performance slow down caused by a large amount of data,CAP will delete expired data every hour by default,the deletion rule is that `ExpiresAt` field's value isn't null and samller than current time.That is, `Failed` messages(it has been retried 50 times by default),if you do not deal with it manually,will also be deleted after 15 days as well,you have to pay attention to it.

+ 24
- 0
mkdocs.yml View File

@@ -0,0 +1,24 @@
site_name: CAP
nav:
- Home: index.md
- <img src="https://www.countryflags.io/us/flat/16.png">User Guide:
- Getting Started: user-guide/getting-started.md
- API Interface: user-guide/api-interface.md
- Configuration: user-guide/configuration.md
- Design Principle: user-guide/design-principle.md.md
- Implementation Mechanisms: user-guide/implementation-mechanisms.md
- Distributed Transactions: user-guide/distributed-transactions.md
- FAQ: user-guide/faq.md
- <img src="https://www.countryflags.io/cn/flat/16.png">使用指南:
- 快速开始: user-guide-cn/getting-started.md
- API 接口: user-guide-cn/api-interface.md
- 配置: user-guide-cn/configuration.md
- 设计原理: user-guide-cn/design-principle.md
- 实现: user-guide-cn/implementation-mechanisms.md
- 分布式事务: user-guide-cn/distributed-transactions.md
- FAQ: user-guide-cn/faq.md
- About: about.md
theme: cinder
markdown_extensions:
- admonition
repo_url: "https://github.com/dotnetcore/CAP"

+ 12
- 102
src/DotNetCore.CAP/Dashboard/Content/resx/Strings.Designer.cs View File

@@ -60,33 +60,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Can not find the target method..
/// </summary>
public static string Common_CannotFindTargetMethod {
get {
return ResourceManager.GetString("Common_CannotFindTargetMethod", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Condition.
/// </summary>
public static string Common_Condition {
get {
return ResourceManager.GetString("Common_Condition", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Continuations.
/// </summary>
public static string Common_Continuations {
get {
return ResourceManager.GetString("Common_Continuations", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Created.
/// </summary>
@@ -105,15 +78,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Do you really want to DELETE ALL selected jobs?.
/// </summary>
public static string Common_DeleteConfirm {
get {
return ResourceManager.GetString("Common_DeleteConfirm", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Delete selected.
/// </summary>
@@ -132,15 +96,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Enqueue jobs.
/// </summary>
public static string Common_EnqueueButton_Text {
get {
return ResourceManager.GetString("Common_EnqueueButton_Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enqueued.
/// </summary>
@@ -185,15 +140,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
return ResourceManager.GetString("Common_Id", resourceCulture);
}
}

/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>
public static string Common_Version {
get {
return ResourceManager.GetString("Common_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Less details....
@@ -231,15 +177,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to No state.
/// </summary>
public static string Common_NoState {
get {
return ResourceManager.GetString("Common_NoState", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to N/A.
/// </summary>
@@ -330,6 +267,15 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>
public static string Common_Version {
get {
return ResourceManager.GetString("Common_Version", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Publish Failed.
/// </summary>
@@ -564,33 +510,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Active Connections.
/// </summary>
public static string Metrics_ActiveConnections {
get {
return ResourceManager.GetString("Metrics_ActiveConnections", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Awaiting.
/// </summary>
public static string Metrics_AwaitingCount {
get {
return ResourceManager.GetString("Metrics_AwaitingCount", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Deleted Jobs.
/// </summary>
public static string Metrics_DeletedJobs {
get {
return ResourceManager.GetString("Metrics_DeletedJobs", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Enqueued.
/// </summary>
@@ -610,7 +529,7 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
/// <summary>
/// Looks up a localized string similar to {0} failed job(s) found. Retry or delete them manually..
/// Looks up a localized string similar to {0} failed message(s) found. .
/// </summary>
public static string Metrics_FailedCountOrNull {
get {
@@ -618,15 +537,6 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Retries.
/// </summary>
public static string Metrics_Retries {
get {
return ResourceManager.GetString("Metrics_Retries", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Servers.
/// </summary>
@@ -682,7 +592,7 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
/// <summary>
/// Looks up a localized string similar to There are no config distributed node discory. .
/// Looks up a localized string similar to There are no config distributed node discory.
/// </summary>
public static string NodePage_NoNodes {
get {
@@ -781,7 +691,7 @@ namespace DotNetCore.CAP.Dashboard.Resources {
}
/// <summary>
/// Looks up a localized string similar to Published Jobs.
/// Looks up a localized string similar to Published messages.
/// </summary>
public static string PublishedPage_Title {
get {


+ 3
- 24
src/DotNetCore.CAP/Dashboard/Content/resx/Strings.resx View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -205,8 +205,8 @@
<value>Items per page</value>
</data>
<data name="NodePage_NoNodes" xml:space="preserve">
<value>There are no config distributed node discory. </value>
</data>
<value>There are no config distributed node discory</value>
</data>
<data name="PublishedMessagesPage_Title" xml:space="preserve">
<value>Published Messages</value>
</data>
@@ -228,36 +228,15 @@
<data name="NavigationMenu_Servers" xml:space="preserve">
<value>Servers</value>
</data>
<data name="Common_CannotFindTargetMethod" xml:space="preserve">
<value>Can not find the target method.</value>
</data>
<data name="Common_Enqueued" xml:space="preserve">
<value>Enqueued</value>
</data>
<data name="Common_NoState" xml:space="preserve">
<value>No state</value>
</data>
<data name="Metrics_ActiveConnections" xml:space="preserve">
<value>Active Connections</value>
</data>
<data name="Metrics_Retries" xml:space="preserve">
<value>Retries</value>
</data>
<data name="Metrics_Servers" xml:space="preserve">
<value>Servers</value>
</data>
<data name="Metrics_TotalConnections" xml:space="preserve">
<value>Total Connections</value>
</data>
<data name="Common_Condition" xml:space="preserve">
<value>Condition</value>
</data>
<data name="Common_Continuations" xml:space="preserve">
<value>Continuations</value>
</data>
<data name="Metrics_AwaitingCount" xml:space="preserve">
<value>Awaiting</value>
</data>
<data name="Metrics_EnqueuedCountOrNull" xml:space="preserve">
<value>Enqueued</value>
</data>


+ 5
- 14
src/DotNetCore.CAP/Dashboard/Content/resx/Strings.zh.resx View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@@ -123,18 +123,12 @@
<data name="Common_Delete" xml:space="preserve">
<value>删除</value>
</data>
<data name="Common_DeleteConfirm" xml:space="preserve">
<value>您确定要删除所选的全部作业吗?</value>
</data>
<data name="Common_Deleting" xml:space="preserve">
<value>删除中...</value>
</data>
<data name="Common_DeleteSelected" xml:space="preserve">
<value>删除选中</value>
</data>
<data name="Common_EnqueueButton_Text" xml:space="preserve">
<value>队列作业</value>
</data>
<data name="Common_Enqueueing" xml:space="preserve">
<value>加入队列中...</value>
</data>
@@ -206,7 +200,7 @@
</data>
<data name="PerPageSelector_ItemsPerPage" xml:space="preserve">
<value>每页条数</value>
</data>
</data>
<data name="PublishedMessagesPage_Title" xml:space="preserve">
<value>发送出的消息</value>
</data>
@@ -231,18 +225,12 @@
<data name="Common_Enqueued" xml:space="preserve">
<value>队列</value>
</data>
<data name="Metrics_DeletedJobs" xml:space="preserve">
<value>删除</value>
</data>
<data name="Metrics_Servers" xml:space="preserve">
<value>服务器</value>
</data>
<data name="Metrics_TotalConnections" xml:space="preserve">
<value>总连接数</value>
</data>
<data name="Metrics_AwaitingCount" xml:space="preserve">
<value>等待中</value>
</data>
<data name="Metrics_EnqueuedCountOrNull" xml:space="preserve">
<value>队列</value>
</data>
@@ -357,4 +345,7 @@
<data name="LayoutPage_Footer_NodeCurrent" xml:space="preserve">
<value>节点: {0}</value>
</data>
<data name="Common_NotAvailable" xml:space="preserve">
<value>N/A</value>
</data>
</root>

+ 0
- 11
src/DotNetCore.CAP/Dashboard/HtmlHelper.cs View File

@@ -106,17 +106,6 @@ namespace DotNetCore.CAP.Dashboard
return new NonEscapedString(value);
}

public NonEscapedString StateLabel(string stateName)
{
if (string.IsNullOrWhiteSpace(stateName))
{
return Raw($"<em>{Strings.Common_NoState}</em>");
}

return Raw(
$"<span class=\"label label-default\" style=\"background-color: {MessageHistoryRenderer.GetForegroundStateColor(stateName)};\">{stateName}</span>");
}

public NonEscapedString RelativeTime(DateTime value)
{
return Raw($"<span data-moment=\"{Helper.ToTimestamp(value)}\">{value}</span>");


+ 1
- 1
src/DotNetCore.CAP/IConsumerHandler.Default.cs View File

@@ -82,7 +82,7 @@ namespace DotNetCore.CAP
_cts.Cancel();
try
{
_compositeTask.Wait(TimeSpan.FromSeconds(2));
_compositeTask?.Wait(TimeSpan.FromSeconds(2));
}
catch (AggregateException ex)
{


Loading…
Cancel
Save