Преглед на файлове

Merge branch 'develop'

# Conflicts:
#	build/version.props
#	src/DotNetCore.CAP.PostgreSql/IStorageConnection.PostgreSql.cs
#	src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs
#	src/DotNetCore.CAP/Dashboard/DashboardRoutes.cs
#	src/DotNetCore.CAP/LoggerExtensions.cs
#	src/DotNetCore.CAP/Models/CapPublishedMessage.cs
#	src/DotNetCore.CAP/Processor/IProcessor.NeedRetry.cs
master
Savorboard преди 6 години
родител
ревизия
1e39e8ae01
променени са 100 файла, в които са добавени 3140 реда и са изтрити 1035 реда
  1. +2
    -0
      .gitignore
  2. +27
    -6
      CAP.sln
  3. +241
    -0
      README.2.2.md
  4. +46
    -59
      README.md
  5. +1
    -1
      README.zh-cn.md
  6. +2
    -1
      appveyor.yml
  7. +2
    -2
      build/version.props
  8. +26
    -11
      samples/Sample.Kafka.MySql/Controllers/ValuesController.cs
  9. +0
    -1
      samples/Sample.Kafka.MySql/Sample.Kafka.MySql.csproj
  10. +0
    -2
      samples/Sample.Kafka.MySql/Startup.cs
  11. +76
    -0
      samples/Sample.RabbitMQ.MongoDB/Controllers/ValuesController.cs
  12. +17
    -0
      samples/Sample.RabbitMQ.MongoDB/Program.cs
  13. +16
    -0
      samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj
  14. +41
    -0
      samples/Sample.RabbitMQ.MongoDB/Startup.cs
  15. +17
    -0
      samples/Sample.RabbitMQ.MongoDB/appsettings.json
  16. +12
    -1
      samples/Sample.RabbitMQ.MySql/AppDbContext.cs
  17. +37
    -15
      samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs
  18. +35
    -0
      samples/Sample.RabbitMQ.MySql/Migrations/20180821021736_init.Designer.cs
  19. +30
    -0
      samples/Sample.RabbitMQ.MySql/Migrations/20180821021736_init.cs
  20. +33
    -0
      samples/Sample.RabbitMQ.MySql/Migrations/AppDbContextModelSnapshot.cs
  21. +1
    -1
      samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj
  22. +0
    -2
      samples/Sample.RabbitMQ.MySql/Startup.cs
  23. +1
    -1
      src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj
  24. +37
    -0
      src/DotNetCore.CAP.MongoDB/CAP.MongoDBCapOptionsExtension.cs
  25. +33
    -0
      src/DotNetCore.CAP.MongoDB/CAP.MongoDBOptions.cs
  26. +35
    -0
      src/DotNetCore.CAP.MongoDB/CAP.Options.Extensions.cs
  27. +24
    -0
      src/DotNetCore.CAP.MongoDB/DotNetCore.CAP.MongoDB.csproj
  28. +49
    -0
      src/DotNetCore.CAP.MongoDB/ICapPublisher.MongoDB.cs
  29. +76
    -0
      src/DotNetCore.CAP.MongoDB/ICapTransaction.MongoDB.cs
  30. +80
    -0
      src/DotNetCore.CAP.MongoDB/IClientSessionHandle.CAP.cs
  31. +51
    -0
      src/DotNetCore.CAP.MongoDB/ICollectProcessor.MongoDB.cs
  32. +226
    -0
      src/DotNetCore.CAP.MongoDB/IMonitoringApi.MongoDB.cs
  33. +65
    -0
      src/DotNetCore.CAP.MongoDB/IStorage.MongoDB.cs
  34. +110
    -0
      src/DotNetCore.CAP.MongoDB/IStorageConnection.MongoDB.cs
  35. +71
    -0
      src/DotNetCore.CAP.MongoDB/IStorageTransaction.MongoDB.cs
  36. +6
    -0
      src/DotNetCore.CAP.MongoDB/Properties/AssemblyInfo.cs
  37. +6
    -3
      src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs
  38. +0
    -87
      src/DotNetCore.CAP.MySql/CapPublisher.cs
  39. +1
    -1
      src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj
  40. +63
    -0
      src/DotNetCore.CAP.MySql/ICapPublisher.MySql.cs
  41. +110
    -0
      src/DotNetCore.CAP.MySql/ICapTransaction.MySql.cs
  42. +1
    -1
      src/DotNetCore.CAP.MySql/ICollectProcessor.MySql.cs
  43. +39
    -0
      src/DotNetCore.CAP.MySql/IDbContextTransaction.CAP.cs
  44. +2
    -2
      src/DotNetCore.CAP.MySql/IMonitoringApi.MySql.cs
  45. +4
    -5
      src/DotNetCore.CAP.MySql/IStorage.MySql.cs
  46. +8
    -12
      src/DotNetCore.CAP.MySql/IStorageConnection.MySql.cs
  47. +0
    -5
      src/DotNetCore.CAP.MySql/IStorageTransaction.MySql.cs
  48. +5
    -2
      src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs
  49. +0
    -85
      src/DotNetCore.CAP.PostgreSql/CapPublisher.cs
  50. +1
    -1
      src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj
  51. +70
    -0
      src/DotNetCore.CAP.PostgreSql/ICapPublisher.PostgreSql.cs
  52. +110
    -0
      src/DotNetCore.CAP.PostgreSql/ICapTransaction.PostgreSql.cs
  53. +38
    -0
      src/DotNetCore.CAP.PostgreSql/IDbContextTransaction.CAP.cs
  54. +2
    -2
      src/DotNetCore.CAP.PostgreSql/IMonitoringApi.PostgreSql.cs
  55. +2
    -4
      src/DotNetCore.CAP.PostgreSql/IStorage.PostgreSql.cs
  56. +12
    -12
      src/DotNetCore.CAP.PostgreSql/IStorageConnection.PostgreSql.cs
  57. +2
    -30
      src/DotNetCore.CAP.PostgreSql/IStorageTransaction.PostgreSql.cs
  58. +1
    -1
      src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj
  59. +8
    -0
      src/DotNetCore.CAP.SqlServer/CAP.EFOptions.cs
  60. +8
    -3
      src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs
  61. +0
    -87
      src/DotNetCore.CAP.SqlServer/CapPublisher.cs
  62. +65
    -0
      src/DotNetCore.CAP.SqlServer/Diagnostics/DiagnosticObserver.cs
  63. +41
    -0
      src/DotNetCore.CAP.SqlServer/Diagnostics/DiagnosticProcessorObserver.cs
  64. +63
    -0
      src/DotNetCore.CAP.SqlServer/ICapPublisher.SqlServer.cs
  65. +170
    -0
      src/DotNetCore.CAP.SqlServer/ICapTransaction.SqlServer.cs
  66. +38
    -0
      src/DotNetCore.CAP.SqlServer/IDbContextTransaction.CAP.cs
  67. +23
    -7
      src/DotNetCore.CAP.SqlServer/IMonitoringApi.SqlServer.cs
  68. +11
    -9
      src/DotNetCore.CAP.SqlServer/IStorage.SqlServer.cs
  69. +8
    -12
      src/DotNetCore.CAP.SqlServer/IStorageConnection.SqlServer.cs
  70. +2
    -30
      src/DotNetCore.CAP.SqlServer/IStorageTransaction.SqlServer.cs
  71. +64
    -186
      src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs
  72. +21
    -0
      src/DotNetCore.CAP/Abstractions/IMongoTransaction.cs
  73. +15
    -1
      src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs
  74. +15
    -13
      src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs
  75. +2
    -2
      src/DotNetCore.CAP/Dashboard/DashboardRoutes.cs
  76. +1
    -1
      src/DotNetCore.CAP/Dashboard/Monitoring/MessageDto.cs
  77. +1
    -5
      src/DotNetCore.CAP/DotNetCore.CAP.csproj
  78. +8
    -30
      src/DotNetCore.CAP/ICapPublisher.cs
  79. +41
    -0
      src/DotNetCore.CAP/ICapTransaction.Base.cs
  80. +33
    -0
      src/DotNetCore.CAP/ICapTransaction.cs
  81. +2
    -4
      src/DotNetCore.CAP/IConsumerHandler.Default.cs
  82. +0
    -2
      src/DotNetCore.CAP/IPublishMessageSender.Base.cs
  83. +6
    -7
      src/DotNetCore.CAP/IStorageConnection.cs
  84. +27
    -253
      src/DotNetCore.CAP/Infrastructure/ObjectId.cs
  85. +97
    -0
      src/DotNetCore.CAP/Infrastructure/SnowflakeId.cs
  86. +12
    -0
      src/DotNetCore.CAP/Infrastructure/StatusName.cs
  87. +1
    -0
      src/DotNetCore.CAP/Internal/ICallbackMessageSender.Default.cs
  88. +7
    -7
      src/DotNetCore.CAP/Internal/LoggerExtensions.cs
  89. +10
    -0
      src/DotNetCore.CAP/Internal/NoopTransaction.cs
  90. +1
    -1
      src/DotNetCore.CAP/Models/CapPublishedMessage.cs
  91. +0
    -15
      src/DotNetCore.CAP/Models/CapQueue.cs
  92. +1
    -1
      src/DotNetCore.CAP/Models/CapReceivedMessage.cs
  93. +2
    -3
      src/DotNetCore.CAP/Processor/IProcessor.NeedRetry.cs
  94. +7
    -0
      test/DotNetCore.CAP.MongoDB.Test/ConnectionUtil.cs
  95. +55
    -0
      test/DotNetCore.CAP.MongoDB.Test/DatabaseTestHost.cs
  96. +22
    -0
      test/DotNetCore.CAP.MongoDB.Test/DotNetCore.CAP.MongoDB.Test.csproj
  97. +71
    -0
      test/DotNetCore.CAP.MongoDB.Test/MongoDBMonitoringApiTest.cs
  98. +81
    -0
      test/DotNetCore.CAP.MongoDB.Test/MongoDBStorageConnectionTest.cs
  99. +21
    -0
      test/DotNetCore.CAP.MongoDB.Test/MongoDBStorageTest.cs
  100. +76
    -0
      test/DotNetCore.CAP.MongoDB.Test/MongoDBTransactionTest.cs

+ 2
- 0
.gitignore Целия файл

@@ -39,3 +39,5 @@ Properties
/src/DotNetCore.CAP/packages.config
/src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj
/NuGet.config
.vscode/*
samples/Sample.RabbitMQ.MongoDB/appsettings.Development.json

+ 27
- 6
CAP.sln Целия файл

@@ -58,7 +58,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql.Test", "test\DotNetCore.CAP.PostgreSql.Test\DotNetCore.CAP.PostgreSql.Test.csproj", "{7CA3625D-1817-4695-881D-7E79A1E1DED2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka.MySql", "samples\Sample.Kafka.MySql\Sample.Kafka.MySql.csproj", "{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.MongoDB.Test", "test\DotNetCore.CAP.MongoDB.Test\DotNetCore.CAP.MongoDB.Test.csproj", "{C143FCDF-E7F3-46F8-987E-A1BA38C1639D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.MongoDB", "src\DotNetCore.CAP.MongoDB\DotNetCore.CAP.MongoDB.csproj", "{77C0AC02-C44B-49D5-B969-7D5305FC20A5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.MongoDB", "samples\Sample.RabbitMQ.MongoDB\Sample.RabbitMQ.MongoDB.csproj", "{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka.MySql", "samples\Sample.Kafka.MySql\Sample.Kafka.MySql.csproj", "{11563D1A-27CC-45CF-8C04-C16BCC21250A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -109,10 +115,22 @@ Global
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Release|Any CPU.Build.0 = Release|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Release|Any CPU.Build.0 = Release|Any CPU
{C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Release|Any CPU.Build.0 = Release|Any CPU
{77C0AC02-C44B-49D5-B969-7D5305FC20A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77C0AC02-C44B-49D5-B969-7D5305FC20A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77C0AC02-C44B-49D5-B969-7D5305FC20A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77C0AC02-C44B-49D5-B969-7D5305FC20A5}.Release|Any CPU.Build.0 = Release|Any CPU
{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F}.Release|Any CPU.Build.0 = Release|Any CPU
{11563D1A-27CC-45CF-8C04-C16BCC21250A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11563D1A-27CC-45CF-8C04-C16BCC21250A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11563D1A-27CC-45CF-8C04-C16BCC21250A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11563D1A-27CC-45CF-8C04-C16BCC21250A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -129,7 +147,10 @@ Global
{9F3F9BFE-7B6A-4A7A-A6E6-8B517D611873} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{82C403AB-ED68-4084-9A1D-11334F9F08F9} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{7CA3625D-1817-4695-881D-7E79A1E1DED2} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0}
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{C143FCDF-E7F3-46F8-987E-A1BA38C1639D} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0}
{77C0AC02-C44B-49D5-B969-7D5305FC20A5} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{4473DE19-E8D2-4B57-80A8-C8AAA2BFA20F} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{11563D1A-27CC-45CF-8C04-C16BCC21250A} = {3A6B6931-A123-477A-9469-8B468B5385AF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB}


+ 241
- 0
README.2.2.md Целия файл

@@ -0,0 +1,241 @@
# CAP                       [中文](https://github.com/dotnetcore/CAP/blob/develop/README.zh-cn.md)
[![Travis branch](https://img.shields.io/travis/dotnetcore/CAP/develop.svg?label=travis-ci)](https://travis-ci.org/dotnetcore/CAP)
[![AppVeyor](https://ci.appveyor.com/api/projects/status/4mpe0tbu7n126vyw?svg=true)](https://ci.appveyor.com/project/yuleyule66/cap)
[![NuGet](https://img.shields.io/nuget/v/DotNetCore.CAP.svg)](https://www.nuget.org/packages/DotNetCore.CAP/)
[![NuGet Preview](https://img.shields.io/nuget/vpre/DotNetCore.CAP.svg?label=nuget-pre)](https://www.nuget.org/packages/DotNetCore.CAP/)
[![Member project of .NET Core Community](https://img.shields.io/badge/member%20project%20of-NCC-9e20c9.svg)](https://github.com/dotnetcore)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dotnetcore/CAP/master/LICENSE.txt)

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.

## OverView

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:

![](http://images2015.cnblogs.com/blog/250417/201707/250417-20170705175827128-1203291469.png)

> The solid line in the figure represents the user code, and the dotted line represents the internal implementation of the CAP.

## Getting Started

### NuGet

You can run the following command to install the CAP in your project.

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

If you want use Kafka to send integrating event, installing by:

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

If you want use RabbitMQ to send integrating event, installing by:

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

CAP supports SqlServer, MySql, PostgreSql as event log storage.

```

// select a database provider you are using, event log table will integrate into.

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

### Configuration

First,You need to config CAP in your Startup.cs:

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

services.AddDbContext<AppDbContext>();

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

// 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 RabbitMQ, you need to add the configuration:
x.UseRabbitMQ("localhost");

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

public void Configure(IApplicationBuilder app)
{
//.....

app.UseCap();
}

```

### Publish

Inject `ICapPublisher` in your Controller, then use the `ICapPublisher` to send message

```c#
public class PublishController : Controller
{
[Route("~/publishWithTransactionUsingEF")]
public async Task<IActionResult> PublishMessageWithTransactionUsingEF([FromServices]AppDbContext dbContext, [FromServices]ICapPublisher publisher)
{
using (var trans = dbContext.Database.BeginTransaction())
{
// your business code

//If you are using EF, CAP will automatic discovery current environment transaction, so you do not need to explicit pass parameters.
//Achieving atomicity between original database operation and the publish event log thanks to a local transaction.
await publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });

trans.Commit();
}
return Ok();
}

[Route("~/publishWithTransactionUsingAdonet")]
public async Task<IActionResult> PublishMessageWithTransactionUsingAdonet([FromServices]ICapPublisher publisher)
{
var connectionString = "";
using (var sqlConnection = new SqlConnection(connectionString))
{
sqlConnection.Open();
using (var sqlTransaction = sqlConnection.BeginTransaction())
{
// your business code

publisher.Publish("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }, sqlTransaction);

sqlTransaction.Commit();
}
}
return Ok();
}
}

```

### Subscribe

**Action Method**

Add the Attribute `[CapSubscribe()]` on Action to subscribe message:

```c#
public class PublishController : Controller
{
[CapSubscribe("xxx.services.account.check")]
public async Task CheckReceivedMessage(Person person)
{
Console.WriteLine(person.Name);
Console.WriteLine(person.Age);
return Task.CompletedTask;
}
}

```

**Service Method**

If your subscribe method is not in the Controller,then your subscribe class need to Inheritance `ICapSubscribe`:

```c#

namespace xxx.Service
{
public interface ISubscriberService
{
public void CheckReceivedMessage(Person person);
}


public class SubscriberService: ISubscriberService, ICapSubscribe
{
[CapSubscribe("xxx.services.account.check")]
public void CheckReceivedMessage(Person person)
{
}
}
}

```

Then inject your `ISubscriberService` class in Startup.cs

```c#
public void ConfigureServices(IServiceCollection services)
{
//Note: The injection of services needs before of `services.AddCap()`
services.AddTransient<ISubscriberService,SubscriberService>();
services.AddCap(x=>{});
}
```

### Dashboard

CAP 2.1 and above provides the dashboard pages, you can easily view the sent and received messages. In addition, you can also view the message status in real time on the dashboard.

In the distributed environment, the dashboard built-in integrated [Consul](http://consul.io) as a node discovery, while the realization of the gateway agent function, you can also easily view the node or other node data, It's like you are visiting local resources.

```c#
services.AddCap(x =>
{
//...
// Register Dashboard
x.UseDashboard();
// Register to Consul
x.UseDiscovery(d =>
{
d.DiscoveryServerHostName = "localhost";
d.DiscoveryServerPort = 8500;
d.CurrentNodeHostName = "localhost";
d.CurrentNodePort = 5800;
d.NodeId = 1;
d.NodeName = "CAP No.1 Node";
});
});
```

The default dashboard address is :[http://localhost:xxx/cap](http://localhost:xxx/cap) , you can also change the `cap` suffix to others with `d.MatchPath` configuration options.

![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

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.

### License

[MIT](https://github.com/dotnetcore/CAP/blob/master/LICENSE.txt)

+ 46
- 59
README.md Целия файл

@@ -16,9 +16,7 @@ You can also use the CAP as an EventBus. The CAP provides a simpler way to imple

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

![](http://images2015.cnblogs.com/blog/250417/201707/250417-20170705175827128-1203291469.png)

> The solid line in the figure represents the user code, and the dotted line represents the internal implementation of the CAP.
![cap.png](http://oowr92l0m.bkt.clouddn.com/cap.png)

## Getting Started

@@ -30,27 +28,22 @@ You can run the following command to install the CAP in your project.
PM> Install-Package DotNetCore.CAP
```

If you want use Kafka to send integrating event, installing by:
CAP supports RabbitMQ and Kafka as message queue, select the packages you need to install:

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

If you want use RabbitMQ to send integrating event, installing by:

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

CAP supports SqlServer, MySql, PostgreSql as event log storage.
CAP supports SqlServer, MySql, PostgreSql,MongoDB as event log storage.

```

// select a database provider you are using, event log table will integrate into.

PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql
PM> Install-Package DotNetCore.CAP.MongoDB //need MongoDB 4.0+ cluster
```

### Configuration
@@ -62,19 +55,22 @@ public void ConfigureServices(IServiceCollection services)
{
//......

services.AddDbContext<AppDbContext>();
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 following configuration:
// Notice: You don't need to config x.UseSqlServer(""") again! CAP can autodiscovery.
x.UseEntityFramework<AppDbContext>();
// 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:
// 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");

@@ -83,13 +79,6 @@ public void ConfigureServices(IServiceCollection services)
});
}

public void Configure(IApplicationBuilder app)
{
//.....

app.UseCap();
}

```

### Publish
@@ -99,38 +88,39 @@ Inject `ICapPublisher` in your Controller, then use the `ICapPublisher` to send
```c#
public class PublishController : Controller
{
[Route("~/publishWithTransactionUsingEF")]
public async Task<IActionResult> PublishMessageWithTransactionUsingEF([FromServices]AppDbContext dbContext, [FromServices]ICapPublisher publisher)
private readonly ICapPublisher _capBus;

public PublishController(ICapPublisher capPublisher)
{
using (var trans = dbContext.Database.BeginTransaction())
{
// your business code
_capBus = capPublisher;
}

//If you are using EF, CAP will automatic discovery current environment transaction, so you do not need to explicit pass parameters.
//Achieving atomicity between original database operation and the publish event log thanks to a local transaction.
await publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 });
[Route("~/adonet/transaction")]
public IActionResult AdonetWithTransaction()
{
using (var connection = new MySqlConnection(ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: true))
{
//your business code

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

return Ok();
}

[Route("~/publishWithTransactionUsingAdonet")]
public async Task<IActionResult> PublishMessageWithTransactionUsingAdonet([FromServices]ICapPublisher publisher)
[Route("~/ef/transaction")]
public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{
var connectionString = "";
using (var sqlConnection = new SqlConnection(connectionString))
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
{
sqlConnection.Open();
using (var sqlTransaction = sqlConnection.BeginTransaction())
{
// your business code
//your business code

publisher.Publish("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }, sqlTransaction);

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

return Ok();
}
}
@@ -146,12 +136,10 @@ Add the Attribute `[CapSubscribe()]` on Action to subscribe message:
```c#
public class PublishController : Controller
{
[CapSubscribe("xxx.services.account.check")]
public async Task CheckReceivedMessage(Person person)
[CapSubscribe("xxx.services.show.time")]
public void CheckReceivedMessage(DateTime datetime)
{
Console.WriteLine(person.Name);
Console.WriteLine(person.Age);
return Task.CompletedTask;
Console.WriteLine(datetime);
}
}

@@ -159,7 +147,7 @@ public class PublishController : Controller

**Service Method**

If your subscribe method is not in the Controller,then your subscribe class need to Inheritance `ICapSubscribe`:
If your subscribe method is not in the Controller,then your subscribe class need to Inheritance `ICapSubscribe`:

```c#

@@ -170,11 +158,10 @@ namespace xxx.Service
public void CheckReceivedMessage(Person person);
}


public class SubscriberService: ISubscriberService, ICapSubscribe
{
[CapSubscribe("xxx.services.account.check")]
public void CheckReceivedMessage(Person person)
[CapSubscribe("xxx.services.show.time")]
public void CheckReceivedMessage(DateTime datetime)
{
}
}
@@ -182,21 +169,21 @@ namespace xxx.Service

```

Then inject your `ISubscriberService` class in Startup.cs
Then inject your `ISubscriberService` class in Startup.cs

```c#
public void ConfigureServices(IServiceCollection services)
{
//Note: The injection of services needs before of `services.AddCap()`
services.AddTransient<ISubscriberService,SubscriberService>();
services.AddCap(x=>{});
}
```

### Dashboard

CAP 2.1 and above provides the dashboard pages, you can easily view the sent and received messages. In addition, you can also view the message status in real time on the dashboard.
CAP v2.1+ provides the dashboard pages, you can easily view the sent and received messages. In addition, you can also view the message status in real time on the dashboard.

In the distributed environment, the dashboard built-in integrated [Consul](http://consul.io) as a node discovery, while the realization of the gateway agent function, you can also easily view the node or other node data, It's like you are visiting local resources.

@@ -204,10 +191,10 @@ In the distributed environment, the dashboard built-in integrated [Consul](http:
services.AddCap(x =>
{
//...
// Register Dashboard
x.UseDashboard();
// Register to Consul
x.UseDiscovery(d =>
{


+ 1
- 1
README.zh-cn.md Целия файл

@@ -10,7 +10,7 @@ CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务

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

## 预览(OverView)
## 预览(OverView)

在我们构建 SOA 或者 微服务系统的过程中,我们通常需要使用事件来对各个服务进行集成,在这过程中简单的使用消息队列并不能保证数据的最终一致性,
CAP 采用的是和当前数据库集成的本地消息表的方案来解决在分布式系统互相调用的各个环节可能出现的异常,它能够保证任何情况下事件消息都是不会丢失的。


+ 2
- 1
appveyor.yml Целия файл

@@ -4,12 +4,13 @@ environment:
BUILDING_ON_PLATFORM: win
BuildEnvironment: appveyor
Cap_SqlServer_ConnectionStringTemplate: Server=(local)\SQL2014;Database={0};User ID=sa;Password=Password12!
Cap_MySql_ConnectionStringTemplate: Server=localhost;Database={0};Uid=root;Pwd=Password12!;Allow User Variables=True
Cap_MySql_ConnectionStringTemplate: Server=localhost;Database={0};Uid=root;Pwd=Password12!;Allow User Variables=True;SslMode=none
Cap_PostgreSql_ConnectionStringTemplate: Server=localhost;Database={0};UserId=postgres;Password=Password12!
services:
- mssql2014
- mysql
- postgresql
- mongodb
build_script:
- ps: ./build.ps1
test: off


+ 2
- 2
build/version.props Целия файл

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


+ 26
- 11
samples/Sample.Kafka.MySql/Controllers/ValuesController.cs Целия файл

@@ -1,5 +1,7 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;
@@ -16,24 +18,37 @@ namespace Sample.Kafka.MySql.Controllers
_capBus = producer;
}

[Route("~/publish")]
public async Task<IActionResult> PublishMessage()
[Route("~/without/transaction")]
public async Task<IActionResult> WithoutTransaction()
{
using (var connection = new MySqlConnection("Server=192.168.10.110;Database=testcap;UserId=root;Password=123123;"))
{
connection.Open();
var transaction = connection.BeginTransaction();

//your business code here
await _capBus.PublishAsync("sample.rabbitmq.mysql", DateTime.Now);

await _capBus.PublishAsync("xxx.xxx.test2", 123456, transaction);
return Ok();
}

transaction.Commit();
[Route("~/adonet/transaction")]
public IActionResult AdonetWithTransaction()
{
using (var connection = new MySqlConnection(""))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: false))
{
//your business code
connection.Execute("insert into test(name) values('test')", transaction: (IDbTransaction)transaction.DbTransaction);

for (int i = 0; i < 5; i++)
{
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}

transaction.Commit();
}
}

return Ok("publish successful!");
return Ok();
}


[CapSubscribe("#.test2")]
public void Test2(int value)
{


+ 0
- 1
samples/Sample.Kafka.MySql/Sample.Kafka.MySql.csproj Целия файл

@@ -9,7 +9,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="MySqlConnector" Version="0.40.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Kafka\DotNetCore.CAP.Kafka.csproj" />


+ 0
- 2
samples/Sample.Kafka.MySql/Startup.cs Целия файл

@@ -21,8 +21,6 @@ namespace Sample.Kafka.MySql
public void Configure(IApplicationBuilder app)
{
app.UseMvc();

app.UseCap();
}
}
}

+ 76
- 0
samples/Sample.RabbitMQ.MongoDB/Controllers/ValuesController.cs Целия файл

@@ -0,0 +1,76 @@
using System;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Bson;
using MongoDB.Driver;

namespace Sample.RabbitMQ.MongoDB.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IMongoClient _client;
private readonly ICapPublisher _capBus;

public ValuesController(IMongoClient client, ICapPublisher capBus)
{
_client = client;
_capBus = capBus;
}

[Route("~/without/transaction")]
public IActionResult WithoutTransaction()
{
_capBus.Publish("sample.rabbitmq.mongodb", DateTime.Now);

return Ok();
}

[Route("~/transaction/not/autocommit")]
public IActionResult PublishNotAutoCommit()
{
//NOTE: before your test, your need to create database and collection at first
//注意:MongoDB 不能在事务中创建数据库和集合,所以你需要单独创建它们,模拟一条记录插入则会自动创建
//var mycollection = _client.GetDatabase("test").GetCollection<BsonDocument>("test.collection");
//mycollection.InsertOne(new BsonDocument { { "test", "test" } });

using (var session = _client.StartTransaction(_capBus, autoCommit: false))
{
var collection = _client.GetDatabase("test").GetCollection<BsonDocument>("test.collection");
collection.InsertOne(session, new BsonDocument { { "hello", "world" } });

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

session.CommitTransaction();
}
return Ok();
}

[Route("~/transaction/autocommit")]
public IActionResult PublishWithoutTrans()
{
//NOTE: before your test, your need to create database and collection at first
//注意:MongoDB 不能在事务中创建数据库和集合,所以你需要单独创建它们,模拟一条记录插入则会自动创建
//var mycollection = _client.GetDatabase("test").GetCollection<BsonDocument>("test.collection");
//mycollection.InsertOne(new BsonDocument { { "test", "test" } });

using (var session = _client.StartTransaction(_capBus, autoCommit: true))
{
var collection = _client.GetDatabase("test").GetCollection<BsonDocument>("test.collection");
collection.InsertOne(session, new BsonDocument { { "hello", "world" } });

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

return Ok();
}

[NonAction]
[CapSubscribe("sample.rabbitmq.mongodb")]
public void ReceiveMessage(DateTime time)
{
Console.WriteLine($@"{DateTime.Now}, Subscriber invoked, Sent time:{time}");
}
}
}

+ 17
- 0
samples/Sample.RabbitMQ.MongoDB/Program.cs Целия файл

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

namespace Sample.RabbitMQ.MongoDB
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

+ 16
- 0
samples/Sample.RabbitMQ.MongoDB/Sample.RabbitMQ.MongoDB.csproj Целия файл

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

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

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

</Project>

+ 41
- 0
samples/Sample.RabbitMQ.MongoDB/Startup.cs Целия файл

@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;

namespace Sample.RabbitMQ.MongoDB
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IMongoClient>(new MongoClient("mongodb://192.168.10.110:27017,192.168.10.110:27018,192.168.10.110:27019/?replicaSet=rs0"));
services.AddCap(x =>
{
x.UseMongoDB("mongodb://192.168.10.110:27017,192.168.10.110:27018,192.168.10.110:27019/?replicaSet=rs0");
x.UseRabbitMQ("localhost");
x.UseDashboard();
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseMvc();
}
}
}

+ 17
- 0
samples/Sample.RabbitMQ.MongoDB/appsettings.json Целия файл

@@ -0,0 +1,17 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MongoDB": "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0"
},
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "",
"Password": ""
}
}

+ 12
- 1
samples/Sample.RabbitMQ.MySql/AppDbContext.cs Целия файл

@@ -2,11 +2,22 @@

namespace Sample.RabbitMQ.MySql
{
public class Person
{
public int Id { get; set; }

public string Name { get; set; }
}

public class AppDbContext : DbContext
{
public const string ConnectionString = "Server=localhost;Database=testcap;UserId=root;Password=123123;";

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

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql("Server=192.168.10.110;Database=testcap;UserId=root;Password=123123;");
optionsBuilder.UseMySql(ConnectionString);
}
}
}

+ 37
- 15
samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs Целия файл

@@ -1,44 +1,66 @@
using System;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;

namespace Sample.RabbitMQ.MySql.Controllers
{
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly AppDbContext _dbContext;
private readonly ICapPublisher _capBus;

public ValuesController(AppDbContext dbContext, ICapPublisher capPublisher)
public ValuesController(ICapPublisher capPublisher)
{
_dbContext = dbContext;
_capBus = capPublisher;
}

[Route("~/publish")]
public IActionResult PublishMessage()
[Route("~/without/transaction")]
public async Task<IActionResult> WithoutTransaction()
{
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
await _capBus.PublishAsync("sample.rabbitmq.mysql", DateTime.Now);

return Ok();
}

[Route("~/publish2")]
public IActionResult PublishMessage2()
[Route("~/adonet/transaction")]
public IActionResult AdonetWithTransaction()
{
_capBus.Publish("sample.kafka.sqlserver4", DateTime.Now);
using (var connection = new MySqlConnection(AppDbContext.ConnectionString))
{
using (var transaction = connection.BeginTransaction(_capBus, autoCommit: false))
{
//your business code
connection.Execute("insert into test(name) values('test')", transaction: (IDbTransaction)transaction.DbTransaction);

for (int i = 0; i < 5; i++)
{
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}

transaction.Commit();
}
}

return Ok();
}

[Route("~/publishWithTrans")]
public async Task<IActionResult> PublishMessageWithTransaction()
[Route("~/ef/transaction")]
public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{
using (var trans = await _dbContext.Database.BeginTransactionAsync())
using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: false))
{
await _capBus.PublishAsync("sample.kafka.sqlserver", "");
dbContext.Persons.Add(new Person() { Name = "ef.transaction" });

for (int i = 0; i < 5; i++)
{
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}

dbContext.SaveChanges();

trans.Commit();
}
@@ -47,9 +69,9 @@ namespace Sample.RabbitMQ.MySql.Controllers

[NonAction]
[CapSubscribe("#.rabbitmq.mysql")]
public void ReceiveMessage(DateTime time)
public void Subscriber(DateTime time)
{
Console.WriteLine("[sample.rabbitmq.mysql] message received: " + DateTime.Now + ",sent time: " + time);
Console.WriteLine($@"{DateTime.Now}, Subscriber invoked, Sent time:{time}");
}
}
}

+ 35
- 0
samples/Sample.RabbitMQ.MySql/Migrations/20180821021736_init.Designer.cs Целия файл

@@ -0,0 +1,35 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sample.RabbitMQ.MySql;

namespace Sample.RabbitMQ.MySql.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20180821021736_init")]
partial class init
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846")
.HasAnnotation("Relational:MaxIdentifierLength", 64);

modelBuilder.Entity("Sample.RabbitMQ.MySql.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();

b.Property<string>("Name");

b.HasKey("Id");

b.ToTable("Persons");
});
#pragma warning restore 612, 618
}
}
}

+ 30
- 0
samples/Sample.RabbitMQ.MySql/Migrations/20180821021736_init.cs Целия файл

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Sample.RabbitMQ.MySql.Migrations
{
public partial class init : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Persons",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Persons", x => x.Id);
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Persons");
}
}
}

+ 33
- 0
samples/Sample.RabbitMQ.MySql/Migrations/AppDbContextModelSnapshot.cs Целия файл

@@ -0,0 +1,33 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Sample.RabbitMQ.MySql;

namespace Sample.RabbitMQ.MySql.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846")
.HasAnnotation("Relational:MaxIdentifierLength", 64);

modelBuilder.Entity("Sample.RabbitMQ.MySql.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();

b.Property<string>("Name");

b.HasKey("Id");

b.ToTable("Persons");
});
#pragma warning restore 612, 618
}
}
}

+ 1
- 1
samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj Целия файл

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

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.0-rc1-final" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.MySql\DotNetCore.CAP.MySql.csproj" />


+ 0
- 2
samples/Sample.RabbitMQ.MySql/Startup.cs Целия файл

@@ -30,8 +30,6 @@ namespace Sample.RabbitMQ.MySql
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc();

app.UseCap();
}
}
}

+ 1
- 1
src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj Целия файл

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

<ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="0.11.4" />
<PackageReference Include="Confluent.Kafka" Version="0.11.5" />
</ItemGroup>

<ItemGroup>


+ 37
- 0
src/DotNetCore.CAP.MongoDB/CAP.MongoDBCapOptionsExtension.cs Целия файл

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

using System;
using DotNetCore.CAP.Processor;
using Microsoft.Extensions.DependencyInjection;

namespace DotNetCore.CAP.MongoDB
{
// ReSharper disable once InconsistentNaming
public class MongoDBCapOptionsExtension : ICapOptionsExtension
{
private readonly Action<MongoDBOptions> _configure;

public MongoDBCapOptionsExtension(Action<MongoDBOptions> configure)
{
_configure = configure;
}

public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, MongoDBStorage>();
services.AddSingleton<IStorageConnection, MongoDBStorageConnection>();

services.AddScoped<ICapPublisher, MongoDBPublisher>();
services.AddScoped<ICallbackPublisher, MongoDBPublisher>();

services.AddTransient<ICollectProcessor, MongoDBCollectProcessor>();
services.AddTransient<CapTransactionBase, MongoDBCapTransaction>();

var options = new MongoDBOptions();
_configure?.Invoke(options);
services.AddSingleton(options);
}
}
}

+ 33
- 0
src/DotNetCore.CAP.MongoDB/CAP.MongoDBOptions.cs Целия файл

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

namespace DotNetCore.CAP.MongoDB
{
// ReSharper disable once InconsistentNaming
public class MongoDBOptions
{
/// <summary>
/// Gets or sets the database name to use when creating database objects.
/// Default value: "cap"
/// </summary>
public string DatabaseName { get; set; } = "cap";

/// <summary>
/// MongoDB database connection string.
/// Default value: "mongodb://localhost:27017"
/// </summary>
public string DatabaseConnection { get; set; } = "mongodb://localhost:27017";

/// <summary>
/// MongoDB received message collection name.
/// Default value: "received"
/// </summary>
public string ReceivedCollection { get; set; } = "cap.received";

/// <summary>
/// MongoDB published message collection name.
/// Default value: "published"
/// </summary>
public string PublishedCollection { get; set; } = "cap.published";
}
}

+ 35
- 0
src/DotNetCore.CAP.MongoDB/CAP.Options.Extensions.cs Целия файл

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

using System;
using DotNetCore.CAP;
using DotNetCore.CAP.MongoDB;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class CapOptionsExtensions
{
public static CapOptions UseMongoDB(this CapOptions options)
{
return options.UseMongoDB(x => { });
}

public static CapOptions UseMongoDB(this CapOptions options, string connectionString)
{
return options.UseMongoDB(x => { x.DatabaseConnection = connectionString; });
}

public static CapOptions UseMongoDB(this CapOptions options, Action<MongoDBOptions> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}

options.RegisterExtension(new MongoDBCapOptionsExtension(configure));

return options;
}
}
}

+ 24
- 0
src/DotNetCore.CAP.MongoDB/DotNetCore.CAP.MongoDB.csproj Целия файл

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

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>DotNetCore.CAP.MongoDB</AssemblyName>
<PackageTags>$(PackageTags);MongoDB</PackageTags>
</PropertyGroup>
<PropertyGroup>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\DotNetCore.CAP.MongoDB.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CS1591</NoWarn>
</PropertyGroup>

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

<ItemGroup>
<PackageReference Include="MongoDB.Bson" Version="2.7.0" />
<PackageReference Include="MongoDB.Driver" Version="2.7.0" />
<PackageReference Include="MongoDB.Driver.Core" Version="2.7.0" />
</ItemGroup>

</Project>

+ 49
- 0
src/DotNetCore.CAP.MongoDB/ICapPublisher.MongoDB.cs Целия файл

@@ -0,0 +1,49 @@
// 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.Threading;
using System.Threading.Tasks;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
public class MongoDBPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly IMongoClient _client;
private readonly MongoDBOptions _options;

public MongoDBPublisher(IServiceProvider provider, MongoDBOptions options)
: base(provider)
{
_options = options;
_client = ServiceProvider.GetRequiredService<IMongoClient>();
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
await PublishAsyncInternal(message);
}

protected override Task ExecuteAsync(CapPublishedMessage message, ICapTransaction transaction,
CancellationToken cancel = default(CancellationToken))
{
var insertOptions = new InsertOneOptions {BypassDocumentValidation = false};

var collection = _client
.GetDatabase(_options.DatabaseName)
.GetCollection<CapPublishedMessage>(_options.PublishedCollection);

if (NotUseTransaction)
{
return collection.InsertOneAsync(message, insertOptions, cancel);
}

var dbTrans = (IClientSessionHandle) transaction.DbTransaction;
return collection.InsertOneAsync(dbTrans, message, insertOptions, cancel);
}
}
}

+ 76
- 0
src/DotNetCore.CAP.MongoDB/ICapTransaction.MongoDB.cs Целия файл

@@ -0,0 +1,76 @@
// 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.Diagnostics;
using MongoDB.Driver;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class MongoDBCapTransaction : CapTransactionBase
{
public MongoDBCapTransaction(IDispatcher dispatcher)
: base(dispatcher)
{
}

public override void Commit()
{
Debug.Assert(DbTransaction != null);

if (DbTransaction is IClientSessionHandle session)
{
session.CommitTransaction();
}

Flush();
}

public override void Rollback()
{
Debug.Assert(DbTransaction != null);

if (DbTransaction is IClientSessionHandle session)
{
session.AbortTransaction();
}
}

public override void Dispose()
{
(DbTransaction as IClientSessionHandle)?.Dispose();
}
}

public static class CapTransactionExtensions
{
public static ICapTransaction Begin(this ICapTransaction transaction,
IClientSessionHandle dbTransaction, bool autoCommit = false)
{
if (!dbTransaction.IsInTransaction)
{
dbTransaction.StartTransaction();
}

transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="client">The <see cref="IMongoClient" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="IClientSessionHandle" /> of MongoDB transaction session object.</returns>
public static IClientSessionHandle StartTransaction(this IMongoClient client,
ICapPublisher publisher, bool autoCommit = false)
{
var clientSessionHandle = client.StartSession();
var capTrans = publisher.Transaction.Begin(clientSessionHandle, autoCommit);
return new CapMongoDbClientSessionHandle(capTrans);
}
}
}

+ 80
- 0
src/DotNetCore.CAP.MongoDB/IClientSessionHandle.CAP.cs Целия файл

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

using System.Threading;
using System.Threading.Tasks;
using DotNetCore.CAP;
using MongoDB.Bson;
using MongoDB.Driver.Core.Bindings;

// ReSharper disable once CheckNamespace
namespace MongoDB.Driver
{
internal class CapMongoDbClientSessionHandle : IClientSessionHandle
{
private readonly IClientSessionHandle _sessionHandle;
private readonly ICapTransaction _transaction;

public CapMongoDbClientSessionHandle(ICapTransaction transaction)
{
_transaction = transaction;
_sessionHandle = (IClientSessionHandle) _transaction.DbTransaction;
}

public void Dispose()
{
_transaction.Dispose();
}

public void AbortTransaction(CancellationToken cancellationToken = default(CancellationToken))
{
_transaction.Rollback();
}

public Task AbortTransactionAsync(CancellationToken cancellationToken = default(CancellationToken))
{
_transaction.Rollback();
return Task.CompletedTask;
}

public void AdvanceClusterTime(BsonDocument newClusterTime)
{
_sessionHandle.AdvanceClusterTime(newClusterTime);
}

public void AdvanceOperationTime(BsonTimestamp newOperationTime)
{
_sessionHandle.AdvanceOperationTime(newOperationTime);
}

public void CommitTransaction(CancellationToken cancellationToken = default(CancellationToken))
{
_transaction.Commit();
}

public Task CommitTransactionAsync(CancellationToken cancellationToken = default(CancellationToken))
{
_transaction.Commit();
return Task.CompletedTask;
}

public void StartTransaction(TransactionOptions transactionOptions = null)
{
_sessionHandle.StartTransaction(transactionOptions);
}

public IMongoClient Client => _sessionHandle.Client;
public BsonDocument ClusterTime => _sessionHandle.ClusterTime;
public bool IsImplicit => _sessionHandle.IsImplicit;
public bool IsInTransaction => _sessionHandle.IsInTransaction;
public BsonTimestamp OperationTime => _sessionHandle.OperationTime;
public ClientSessionOptions Options => _sessionHandle.Options;
public IServerSession ServerSession => _sessionHandle.ServerSession;
public ICoreSessionHandle WrappedCoreSession => _sessionHandle.WrappedCoreSession;

public IClientSessionHandle Fork()
{
return _sessionHandle.Fork();
}
}
}

+ 51
- 0
src/DotNetCore.CAP.MongoDB/ICollectProcessor.MongoDB.cs Целия файл

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

using System;
using System.Threading.Tasks;
using DotNetCore.CAP.Models;
using DotNetCore.CAP.Processor;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
public class MongoDBCollectProcessor : ICollectProcessor
{
private readonly IMongoDatabase _database;
private readonly ILogger _logger;
private readonly MongoDBOptions _options;
private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5);

public MongoDBCollectProcessor(ILogger<MongoDBCollectProcessor> logger,
MongoDBOptions options,
IMongoClient client)
{
_options = options;
_logger = logger;
_database = client.GetDatabase(_options.DatabaseName);
}

public async Task ProcessAsync(ProcessingContext context)
{
_logger.LogDebug(
$"Collecting expired data from collection [{_options.PublishedCollection}].");

var publishedCollection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);
var receivedCollection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

await publishedCollection.BulkWriteAsync(new[]
{
new DeleteManyModel<CapPublishedMessage>(
Builders<CapPublishedMessage>.Filter.Lt(x => x.ExpiresAt, DateTime.Now))
});
await receivedCollection.BulkWriteAsync(new[]
{
new DeleteManyModel<CapReceivedMessage>(
Builders<CapReceivedMessage>.Filter.Lt(x => x.ExpiresAt, DateTime.Now))
});

await context.WaitAsync(_waitingInterval);
}
}
}

+ 226
- 0
src/DotNetCore.CAP.MongoDB/IMonitoringApi.MongoDB.cs Целия файл

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

using System;
using System.Collections.Generic;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.Dashboard.Monitoring;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
using MongoDB.Bson;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
public class MongoDBMonitoringApi : IMonitoringApi
{
private readonly IMongoDatabase _database;
private readonly MongoDBOptions _options;

public MongoDBMonitoringApi(IMongoClient client, MongoDBOptions options)
{
var mongoClient = client ?? throw new ArgumentNullException(nameof(client));
_options = options ?? throw new ArgumentNullException(nameof(options));

_database = mongoClient.GetDatabase(_options.DatabaseName);
}

public StatisticsDto GetStatistics()
{
var publishedCollection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);
var receivedCollection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

var statistics = new StatisticsDto();

{
if (int.TryParse(
publishedCollection.CountDocuments(x => x.StatusName == StatusName.Succeeded).ToString(),
out var count))
{
statistics.PublishedSucceeded = count;
}
}
{
if (int.TryParse(publishedCollection.CountDocuments(x => x.StatusName == StatusName.Failed).ToString(),
out var count))
{
statistics.PublishedFailed = count;
}
}
{
if (int.TryParse(
receivedCollection.CountDocuments(x => x.StatusName == StatusName.Succeeded).ToString(),
out var count))
{
statistics.ReceivedSucceeded = count;
}
}
{
if (int.TryParse(receivedCollection.CountDocuments(x => x.StatusName == StatusName.Failed).ToString(),
out var count))
{
statistics.ReceivedFailed = count;
}
}

return statistics;
}

public IDictionary<DateTime, int> HourlyFailedJobs(MessageType type)
{
return GetHourlyTimelineStats(type, StatusName.Failed);
}

public IDictionary<DateTime, int> HourlySucceededJobs(MessageType type)
{
return GetHourlyTimelineStats(type, StatusName.Succeeded);
}

public IList<MessageDto> Messages(MessageQueryDto queryDto)
{
queryDto.StatusName = StatusName.Standardized(queryDto.StatusName);

var name = queryDto.MessageType == MessageType.Publish
? _options.PublishedCollection
: _options.ReceivedCollection;
var collection = _database.GetCollection<MessageDto>(name);

var builder = Builders<MessageDto>.Filter;
var filter = builder.Empty;
if (!string.IsNullOrEmpty(queryDto.StatusName))
{
filter = filter & builder.Eq(x => x.StatusName, queryDto.StatusName);
}

if (!string.IsNullOrEmpty(queryDto.Name))
{
filter = filter & builder.Eq(x => x.Name, queryDto.Name);
}

if (!string.IsNullOrEmpty(queryDto.Group))
{
filter = filter & builder.Eq(x => x.Group, queryDto.Group);
}

if (!string.IsNullOrEmpty(queryDto.Content))
{
filter = filter & builder.Regex(x => x.Content, ".*" + queryDto.Content + ".*");
}

var result = collection
.Find(filter)
.SortByDescending(x => x.Added)
.Skip(queryDto.PageSize * queryDto.CurrentPage)
.Limit(queryDto.PageSize)
.ToList();

return result;
}

public int PublishedFailedCount()
{
return GetNumberOfMessage(_options.PublishedCollection, StatusName.Failed);
}

public int PublishedSucceededCount()
{
return GetNumberOfMessage(_options.PublishedCollection, StatusName.Succeeded);
}

public int ReceivedFailedCount()
{
return GetNumberOfMessage(_options.ReceivedCollection, StatusName.Failed);
}

public int ReceivedSucceededCount()
{
return GetNumberOfMessage(_options.ReceivedCollection, StatusName.Succeeded);
}

private int GetNumberOfMessage(string collectionName, string statusName)
{
var collection = _database.GetCollection<BsonDocument>(collectionName);
var count = collection.CountDocuments(new BsonDocument {{"StatusName", statusName}});
return int.Parse(count.ToString());
}

private IDictionary<DateTime, int> GetHourlyTimelineStats(MessageType type, string statusName)
{
var collectionName =
type == MessageType.Publish ? _options.PublishedCollection : _options.ReceivedCollection;
var endDate = DateTime.UtcNow;

var groupby = new BsonDocument
{
{
"$group", new BsonDocument
{
{
"_id", new BsonDocument
{
{
"Key", new BsonDocument
{
{
"$dateToString", new BsonDocument
{
{"format", "%Y-%m-%d %H:00:00"},
{"date", "$Added"}
}
}
}
}
}
},
{"Count", new BsonDocument {{"$sum", 1}}}
}
}
};

var match = new BsonDocument
{
{
"$match", new BsonDocument
{
{
"Added", new BsonDocument
{
{"$gt", endDate.AddHours(-24)}
}
},
{
"StatusName",
new BsonDocument
{
{"$eq", statusName}
}
}
}
}
};

var pipeline = new[] {match, groupby};

var collection = _database.GetCollection<BsonDocument>(collectionName);
var result = collection.Aggregate<BsonDocument>(pipeline).ToList();

var dic = new Dictionary<DateTime, int>();
for (var i = 0; i < 24; i++)
{
dic.Add(DateTime.Parse(endDate.ToLocalTime().ToString("yyyy-MM-dd HH:00:00")), 0);
endDate = endDate.AddHours(-1);
}

result.ForEach(d =>
{
var key = d["_id"].AsBsonDocument["Key"].AsString;
if (DateTime.TryParse(key, out var dateTime))
{
dic[dateTime.ToLocalTime()] = d["Count"].AsInt32;
}
});

return dic;
}
}
}

+ 65
- 0
src/DotNetCore.CAP.MongoDB/IStorage.MongoDB.cs Целия файл

@@ -0,0 +1,65 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using DotNetCore.CAP.Dashboard;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
public class MongoDBStorage : IStorage
{
private readonly CapOptions _capOptions;
private readonly IMongoClient _client;
private readonly ILogger<MongoDBStorage> _logger;
private readonly MongoDBOptions _options;

public MongoDBStorage(CapOptions capOptions,
MongoDBOptions options,
IMongoClient client,
ILogger<MongoDBStorage> logger)
{
_capOptions = capOptions;
_options = options;
_client = client;
_logger = logger;
}

public IStorageConnection GetConnection()
{
return new MongoDBStorageConnection(_capOptions, _options, _client);
}

public IMonitoringApi GetMonitoringApi()
{
return new MongoDBMonitoringApi(_client, _options);
}

public async Task InitializeAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}

var database = _client.GetDatabase(_options.DatabaseName);
var names = (await database.ListCollectionNamesAsync(cancellationToken: cancellationToken))?.ToList();

if (names.All(n => n != _options.ReceivedCollection))
{
await database.CreateCollectionAsync(_options.ReceivedCollection, cancellationToken: cancellationToken);
}

if (names.All(n => n != _options.PublishedCollection))
{
await database.CreateCollectionAsync(_options.PublishedCollection,
cancellationToken: cancellationToken);
}

_logger.LogDebug("Ensuring all create database tables script are applied.");
}
}
}

+ 110
- 0
src/DotNetCore.CAP.MongoDB/IStorageConnection.MongoDB.cs Целия файл

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

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
public class MongoDBStorageConnection : IStorageConnection
{
private readonly CapOptions _capOptions;
private readonly IMongoClient _client;
private readonly IMongoDatabase _database;
private readonly MongoDBOptions _options;

public MongoDBStorageConnection(CapOptions capOptions, MongoDBOptions options, IMongoClient client)
{
_capOptions = capOptions;
_options = options;
_client = client;
_database = _client.GetDatabase(_options.DatabaseName);
}

public bool ChangePublishedState(long messageId, string state)
{
var collection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);

var updateDef = Builders<CapPublishedMessage>
.Update.Inc(x => x.Retries, 1)
.Set(x => x.ExpiresAt, null)
.Set(x => x.StatusName, state);

var result =
collection.UpdateOne(x => x.Id == messageId, updateDef);

return result.ModifiedCount > 0;
}

public bool ChangeReceivedState(long messageId, string state)
{
var collection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

var updateDef = Builders<CapReceivedMessage>
.Update.Inc(x => x.Retries, 1)
.Set(x => x.ExpiresAt, null)
.Set(x => x.StatusName, state);

var result =
collection.UpdateOne(x => x.Id == messageId, updateDef);

return result.ModifiedCount > 0;
}

public IStorageTransaction CreateTransaction()
{
return new MongoDBStorageTransaction(_client, _options);
}

public async Task<CapPublishedMessage> GetPublishedMessageAsync(long id)
{
var collection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);
return await collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}

public async Task<IEnumerable<CapPublishedMessage>> GetPublishedMessagesOfNeedRetry()
{
var fourMinsAgo = DateTime.Now.AddMinutes(-4);
var collection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);
return await collection
.Find(x => x.Retries < _capOptions.FailedRetryCount && x.Added < fourMinsAgo &&
(x.StatusName == StatusName.Failed || x.StatusName == StatusName.Scheduled))
.Limit(200)
.ToListAsync();
}

public async Task<CapReceivedMessage> GetReceivedMessageAsync(long id)
{
var collection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);
return await collection.Find(x => x.Id == id).FirstOrDefaultAsync();
}

public async Task<IEnumerable<CapReceivedMessage>> GetReceivedMessagesOfNeedRetry()
{
var fourMinsAgo = DateTime.Now.AddMinutes(-4);
var collection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

return await collection
.Find(x => x.Retries < _capOptions.FailedRetryCount && x.Added < fourMinsAgo &&
(x.StatusName == StatusName.Failed || x.StatusName == StatusName.Scheduled))
.Limit(200)
.ToListAsync();
}

public void StoreReceivedMessage(CapReceivedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var collection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

collection.InsertOne(message);
}
}
}

+ 71
- 0
src/DotNetCore.CAP.MongoDB/IStorageTransaction.MongoDB.cs Целия файл

@@ -0,0 +1,71 @@
// 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.Threading.Tasks;
using DotNetCore.CAP.Models;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB
{
internal class MongoDBStorageTransaction : IStorageTransaction
{
private readonly IMongoDatabase _database;
private readonly MongoDBOptions _options;
private readonly IClientSessionHandle _session;

public MongoDBStorageTransaction(IMongoClient client, MongoDBOptions options)
{
_options = options;
_database = client.GetDatabase(options.DatabaseName);
_session = client.StartSession();
_session.StartTransaction();
}

public async Task CommitAsync()
{
await _session.CommitTransactionAsync();
}

public void Dispose()
{
_session.Dispose();
}

public void UpdateMessage(CapPublishedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var collection = _database.GetCollection<CapPublishedMessage>(_options.PublishedCollection);

var updateDef = Builders<CapPublishedMessage>.Update
.Set(x => x.Retries, message.Retries)
.Set(x => x.Content, message.Content)
.Set(x => x.ExpiresAt, message.ExpiresAt)
.Set(x => x.StatusName, message.StatusName);

collection.FindOneAndUpdate(_session, x => x.Id == message.Id, updateDef);
}

public void UpdateMessage(CapReceivedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var collection = _database.GetCollection<CapReceivedMessage>(_options.ReceivedCollection);

var updateDef = Builders<CapReceivedMessage>.Update
.Set(x => x.Retries, message.Retries)
.Set(x => x.Content, message.Content)
.Set(x => x.ExpiresAt, message.ExpiresAt)
.Set(x => x.StatusName, message.StatusName);

collection.FindOneAndUpdate(_session, x => x.Id == message.Id, updateDef);
}
}
}

+ 6
- 0
src/DotNetCore.CAP.MongoDB/Properties/AssemblyInfo.cs Целия файл

@@ -0,0 +1,6 @@
// 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.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DotNetCore.CAP.MongoDB.Test")]

+ 6
- 3
src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs Целия файл

@@ -24,9 +24,12 @@ namespace DotNetCore.CAP
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, MySqlStorage>();
services.AddSingleton<IStorageConnection, MySqlStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();

services.AddScoped<ICapPublisher, MySqlPublisher>();
services.AddScoped<ICallbackPublisher, MySqlPublisher>();

services.AddTransient<ICollectProcessor, MySqlCollectProcessor>();
services.AddTransient<CapTransactionBase, MySqlCapTransaction>();

AddSingletionMySqlOptions(services);
}
@@ -44,7 +47,7 @@ namespace DotNetCore.CAP
using (var scope = x.CreateScope())
{
var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(mysqlOptions.DbContextType);
var dbContext = (DbContext) provider.GetService(mysqlOptions.DbContextType);
mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return mysqlOptions;
}


+ 0
- 87
src/DotNetCore.CAP.MySql/CapPublisher.cs Целия файл

@@ -1,87 +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.Data;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using MySql.Data.MySqlClient;

namespace DotNetCore.CAP.MySql
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly MySqlOptions _options;

public CapPublisher(ILogger<CapPublisher> logger, IDispatcher dispatcher, IServiceProvider provider,
MySqlOptions options)
: base(logger, dispatcher)
{
ServiceProvider = provider;
_options = options;

if (_options.DbContextType == null)
{
return;
}

IsUsingEF = true;
_dbContext = (DbContext) ServiceProvider.GetService(_options.DbContextType);
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
using (var conn = new MySqlConnection(_options.ConnectionString))
{
var id = await conn.ExecuteScalarAsync<int>(PrepareSql(), message);
message.Id = id;
Enqueue(message);
}
}

protected override void PrepareConnectionForEF()
{
DbConnection = _dbContext.Database.GetDbConnection();
var dbContextTransaction = _dbContext.Database.CurrentTransaction;
var dbTrans = dbContextTransaction?.GetDbTransaction();
//DbTransaction is dispose in original
if (dbTrans?.Connection == null)
{
IsCapOpenedTrans = true;
dbContextTransaction?.Dispose();
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}

DbTransaction = dbTrans;
}

protected override int Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return dbConnection.ExecuteScalar<int>(PrepareSql(), message, dbTransaction);
}

protected override async Task<int> ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return await dbConnection.ExecuteScalarAsync<int>(PrepareSql(), message, dbTransaction);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO `{_options.TableNamePrefix}.published` (`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST_INSERT_ID()";
}

#endregion private methods
}
}

+ 1
- 1
src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj Целия файл

@@ -15,7 +15,7 @@
<PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.1.0" />
<PackageReference Include="MySqlConnector" Version="0.40.4" />
<PackageReference Include="MySqlConnector" Version="0.43.0" />
</ItemGroup>

<ItemGroup>


+ 63
- 0
src/DotNetCore.CAP.MySql/ICapPublisher.MySql.cs Целия файл

@@ -0,0 +1,63 @@
// 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.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using MySql.Data.MySqlClient;

namespace DotNetCore.CAP.MySql
{
public class MySqlPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly MySqlOptions _options;

public MySqlPublisher(IServiceProvider provider) : base(provider)
{
_options = provider.GetService<MySqlOptions>();
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
await PublishAsyncInternal(message);
}

protected override async Task ExecuteAsync(CapPublishedMessage message, ICapTransaction transaction,
CancellationToken cancel = default(CancellationToken))
{
if (NotUseTransaction)
{
using (var connection = new MySqlConnection(_options.ConnectionString))
{
await connection.ExecuteAsync(PrepareSql(), message);
return;
}
}

var dbTrans = transaction.DbTransaction as IDbTransaction;
if (dbTrans == null && transaction.DbTransaction is IDbContextTransaction dbContextTrans)
{
dbTrans = dbContextTrans.GetDbTransaction();
}

var conn = dbTrans?.Connection;
await conn.ExecuteAsync(PrepareSql(), message, dbTrans);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO `{_options.TableNamePrefix}.published` (`Id`,`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
}

#endregion private methods
}
}

+ 110
- 0
src/DotNetCore.CAP.MySql/ICapTransaction.MySql.cs Целия файл

@@ -0,0 +1,110 @@
// 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.Data;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class MySqlCapTransaction : CapTransactionBase
{
public MySqlCapTransaction(IDispatcher dispatcher) : base(dispatcher)
{
}

public override void Commit()
{
Debug.Assert(DbTransaction != null);

switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Commit();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Commit();
break;
}

Flush();
}

public override void Rollback()
{
Debug.Assert(DbTransaction != null);

switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Rollback();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Rollback();
break;
}
}

public override void Dispose()
{
(DbTransaction as IDbTransaction)?.Dispose();
}
}

public static class CapTransactionExtensions
{
public static ICapTransaction Begin(this ICapTransaction transaction,
IDbContextTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

public static ICapTransaction Begin(this ICapTransaction transaction,
IDbTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="database">The <see cref="DatabaseFacade" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="IDbContextTransaction" /> of EF dbcontext transaction object.</returns>
public static IDbContextTransaction BeginTransaction(this DatabaseFacade database,
ICapPublisher publisher, bool autoCommit = false)
{
var trans = database.BeginTransaction();
var capTrans = publisher.Transaction.Begin(trans, autoCommit);
return new CapEFDbTransaction(capTrans);
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="dbConnection">The <see cref="IDbConnection" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="ICapTransaction" /> object.</returns>
public static ICapTransaction BeginTransaction(this IDbConnection dbConnection,
ICapPublisher publisher, bool autoCommit = false)
{
if (dbConnection.State == ConnectionState.Closed)
{
dbConnection.Open();
}

var dbTransaction = dbConnection.BeginTransaction();
return publisher.Transaction.Begin(dbTransaction, autoCommit);
}
}
}

+ 1
- 1
src/DotNetCore.CAP.MySql/ICollectProcessor.MySql.cs Целия файл

@@ -36,7 +36,7 @@ namespace DotNetCore.CAP.MySql
foreach (var table in tables)
{
_logger.LogDebug($"Collecting expired data from table [{table}].");
int removedCount;
do
{


+ 39
- 0
src/DotNetCore.CAP.MySql/IDbContextTransaction.CAP.cs Целия файл

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

using System;
using DotNetCore.CAP;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore.Storage
{
// ReSharper disable once InconsistentNaming
internal class CapEFDbTransaction : IDbContextTransaction
{
private readonly ICapTransaction _transaction;

public CapEFDbTransaction(ICapTransaction transaction)
{
_transaction = transaction;
var dbContextTransaction = (IDbContextTransaction) _transaction.DbTransaction;
TransactionId = dbContextTransaction.TransactionId;
}

public void Dispose()
{
_transaction.Dispose();
}

public void Commit()
{
_transaction.Commit();
}

public void Rollback()
{
_transaction.Rollback();
}

public Guid TransactionId { get; }
}
}

src/DotNetCore.CAP.MySql/MySqlMonitoringApi.cs → src/DotNetCore.CAP.MySql/IMonitoringApi.MySql.cs Целия файл

@@ -126,7 +126,7 @@ select count(Id) from `{0}.received` where StatusName = N'Failed';", _prefix);
{
var sqlQuery = $"select count(Id) from `{_prefix}.{tableName}` where StatusName = @state";

var count = connection.ExecuteScalar<int>(sqlQuery, new { state = statusName });
var count = connection.ExecuteScalar<int>(sqlQuery, new {state = statusName});
return count;
}

@@ -169,7 +169,7 @@ select aggr.* from (

var valuesMap = connection.Query<TimelineCounter>(
sqlQuery,
new { keys = keyMaps.Keys, statusName })
new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => x.Key, x => x.Count);

foreach (var key in keyMaps.Keys)

src/DotNetCore.CAP.MySql/MySqlStorage.cs → src/DotNetCore.CAP.MySql/IStorage.MySql.cs Целия файл

@@ -58,10 +58,8 @@ namespace DotNetCore.CAP.MySql
{
var batchSql =
$@"
DROP TABLE IF EXISTS `{prefix}.queue`;

CREATE TABLE IF NOT EXISTS `{prefix}.received` (
`Id` int(127) NOT NULL AUTO_INCREMENT,
`Id` bigint NOT NULL,
`Name` varchar(400) NOT NULL,
`Group` varchar(200) DEFAULT NULL,
`Content` longtext,
@@ -73,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `{prefix}.received` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `{prefix}.published` (
`Id` int(127) NOT NULL AUTO_INCREMENT,
`Id` bigint NOT NULL,
`Name` varchar(200) NOT NULL,
`Content` longtext,
`Retries` int(11) DEFAULT NULL,
@@ -81,7 +79,8 @@ CREATE TABLE IF NOT EXISTS `{prefix}.published` (
`ExpiresAt` datetime DEFAULT NULL,
`StatusName` varchar(40) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
";
return batchSql;
}


src/DotNetCore.CAP.MySql/MySqlStorageConnection.cs → src/DotNetCore.CAP.MySql/IStorageConnection.MySql.cs Целия файл

@@ -30,7 +30,7 @@ namespace DotNetCore.CAP.MySql
return new MySqlStorageTransaction(this);
}

public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id)
public async Task<CapPublishedMessage> GetPublishedMessageAsync(long id)
{
var sql = $@"SELECT * FROM `{_prefix}.published` WHERE `Id`={id};";

@@ -52,7 +52,7 @@ namespace DotNetCore.CAP.MySql
}
}

public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message)
public void StoreReceivedMessage(CapReceivedMessage message)
{
if (message == null)
{
@@ -60,16 +60,16 @@ namespace DotNetCore.CAP.MySql
}

var sql = $@"
INSERT INTO `{_prefix}.received`(`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST_INSERT_ID();";
INSERT INTO `{_prefix}.received`(`Id`,`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)
VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";

using (var connection = new MySqlConnection(Options.ConnectionString))
{
return await connection.ExecuteScalarAsync<int>(sql, message);
connection.Execute(sql, message);
}
}

public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
public async Task<CapReceivedMessage> GetReceivedMessageAsync(long id)
{
var sql = $@"SELECT * FROM `{_prefix}.received` WHERE Id={id};";
using (var connection = new MySqlConnection(Options.ConnectionString))
@@ -89,7 +89,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST
}
}

public bool ChangePublishedState(int messageId, string state)
public bool ChangePublishedState(long messageId, string state)
{
var sql =
$"UPDATE `{_prefix}.published` SET `Retries`=`Retries`+1,`ExpiresAt`=NULL,`StatusName` = '{state}' WHERE `Id`={messageId}";
@@ -100,7 +100,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST
}
}

public bool ChangeReceivedState(int messageId, string state)
public bool ChangeReceivedState(long messageId, string state)
{
var sql =
$"UPDATE `{_prefix}.received` SET `Retries`=`Retries`+1,`ExpiresAt`=NULL,`StatusName` = '{state}' WHERE `Id`={messageId}";
@@ -110,9 +110,5 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST
return connection.Execute(sql) > 0;
}
}

public void Dispose()
{
}
}
}

src/DotNetCore.CAP.MySql/MySqlStorageTransaction.cs → src/DotNetCore.CAP.MySql/IStorageTransaction.MySql.cs Целия файл

@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.MySql
{
private readonly IDbConnection _dbConnection;

//private readonly IDbTransaction _dbTransaction;
private readonly string _prefix;

public MySqlStorageTransaction(MySqlStorageConnection connection)
@@ -23,8 +22,6 @@ namespace DotNetCore.CAP.MySql
_prefix = options.TableNamePrefix;

_dbConnection = new MySqlConnection(options.ConnectionString);
// _dbConnection.Open(); for performance
// _dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted);
}

public void UpdateMessage(CapPublishedMessage message)
@@ -55,13 +52,11 @@ namespace DotNetCore.CAP.MySql
{
_dbConnection.Close();
_dbConnection.Dispose();
//_dbTransaction.Commit();
return Task.CompletedTask;
}

public void Dispose()
{
//_dbTransaction.Dispose();
_dbConnection.Dispose();
}
}

+ 5
- 2
src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs Целия файл

@@ -24,9 +24,12 @@ namespace DotNetCore.CAP
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, PostgreSqlStorage>();
services.AddSingleton<IStorageConnection, PostgreSqlStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();

services.AddScoped<ICapPublisher, PostgreSqlPublisher>();
services.AddScoped<ICallbackPublisher, PostgreSqlPublisher>();

services.AddTransient<ICollectProcessor, PostgreSqlCollectProcessor>();
services.AddTransient<CapTransactionBase, PostgreSqlCapTransaction>();

AddSingletonPostgreSqlOptions(services);
}


+ 0
- 85
src/DotNetCore.CAP.PostgreSql/CapPublisher.cs Целия файл

@@ -1,85 +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.Data;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using Npgsql;

namespace DotNetCore.CAP.PostgreSql
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly PostgreSqlOptions _options;

public CapPublisher(ILogger<CapPublisher> logger, IDispatcher dispatcher,
IServiceProvider provider, PostgreSqlOptions options)
: base(logger, dispatcher)
{
ServiceProvider = provider;
_options = options;

if (_options.DbContextType != null)
{
IsUsingEF = true;
_dbContext = (DbContext) ServiceProvider.GetService(_options.DbContextType);
}
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
using (var conn = new NpgsqlConnection(_options.ConnectionString))
{
var id = await conn.ExecuteScalarAsync<int>(PrepareSql(), message);
message.Id = id;
Enqueue(message);
}
}

protected override void PrepareConnectionForEF()
{
DbConnection = _dbContext.Database.GetDbConnection();
var dbContextTransaction = _dbContext.Database.CurrentTransaction;
var dbTrans = dbContextTransaction?.GetDbTransaction();
//DbTransaction is dispose in original
if (dbTrans?.Connection == null)
{
IsCapOpenedTrans = true;
dbContextTransaction?.Dispose();
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}

DbTransaction = dbTrans;
}

protected override int Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return dbConnection.ExecuteScalar<int>(PrepareSql(), message, dbTransaction);
}

protected override Task<int> ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return dbConnection.ExecuteScalarAsync<int>(PrepareSql(), message, dbTransaction);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO \"{_options.Schema}\".\"published\" (\"Name\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING \"Id\";";
}

#endregion private methods
}
}

+ 1
- 1
src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj Целия файл

@@ -15,7 +15,7 @@
<PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.1.0" />
<PackageReference Include="Npgsql" Version="4.0.0" />
<PackageReference Include="Npgsql" Version="4.0.2" />
</ItemGroup>

<ItemGroup>


+ 70
- 0
src/DotNetCore.CAP.PostgreSql/ICapPublisher.PostgreSql.cs Целия файл

@@ -0,0 +1,70 @@
// 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.Data;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;

namespace DotNetCore.CAP.PostgreSql
{
public class PostgreSqlPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly PostgreSqlOptions _options;

public PostgreSqlPublisher(IServiceProvider provider) : base(provider)
{
_options = provider.GetService<PostgreSqlOptions>();
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
await PublishAsyncInternal(message);
}

protected override async Task ExecuteAsync(CapPublishedMessage message, ICapTransaction transaction,
CancellationToken cancel = default(CancellationToken))
{
if (NotUseTransaction)
{
using (var connection = InitDbConnection())
{
await connection.ExecuteAsync(PrepareSql(), message);
return;
}
}

var dbTrans = transaction.DbTransaction as IDbTransaction;
if (dbTrans == null && transaction.DbTransaction is IDbContextTransaction dbContextTrans)
{
dbTrans = dbContextTrans.GetDbTransaction();
}

var conn = dbTrans?.Connection;
await conn.ExecuteAsync(PrepareSql(), message, dbTrans);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO \"{_options.Schema}\".\"published\" (\"Id\",\"Name\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
}

private IDbConnection InitDbConnection()
{
var conn = new NpgsqlConnection(_options.ConnectionString);
conn.Open();
return conn;
}

#endregion private methods
}
}

+ 110
- 0
src/DotNetCore.CAP.PostgreSql/ICapTransaction.PostgreSql.cs Целия файл

@@ -0,0 +1,110 @@
// 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.Data;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class PostgreSqlCapTransaction : CapTransactionBase
{
public PostgreSqlCapTransaction(IDispatcher dispatcher) : base(dispatcher)
{
}

public override void Commit()
{
Debug.Assert(DbTransaction != null);

switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Commit();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Commit();
break;
}

Flush();
}

public override void Rollback()
{
Debug.Assert(DbTransaction != null);

switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Rollback();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Rollback();
break;
}
}

public override void Dispose()
{
(DbTransaction as IDbTransaction)?.Dispose();
}
}

public static class CapTransactionExtensions
{
public static ICapTransaction Begin(this ICapTransaction transaction,
IDbTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

public static ICapTransaction Begin(this ICapTransaction transaction,
IDbContextTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="dbConnection">The <see cref="IDbConnection" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="ICapTransaction" /> object.</returns>
public static ICapTransaction BeginTransaction(this IDbConnection dbConnection,
ICapPublisher publisher, bool autoCommit = false)
{
if (dbConnection.State == ConnectionState.Closed)
{
dbConnection.Open();
}

var dbTransaction = dbConnection.BeginTransaction();
return publisher.Transaction.Begin(dbTransaction, autoCommit);
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="database">The <see cref="DatabaseFacade" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="IDbContextTransaction" /> of EF dbcontext transaction object.</returns>
public static IDbContextTransaction BeginTransaction(this DatabaseFacade database,
ICapPublisher publisher, bool autoCommit = false)
{
var trans = database.BeginTransaction();
var capTrans = publisher.Transaction.Begin(trans, autoCommit);
return new CapEFDbTransaction(capTrans);
}
}
}

+ 38
- 0
src/DotNetCore.CAP.PostgreSql/IDbContextTransaction.CAP.cs Целия файл

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

using System;
using DotNetCore.CAP;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore.Storage
{
internal class CapEFDbTransaction : IDbContextTransaction
{
private readonly ICapTransaction _transaction;

public CapEFDbTransaction(ICapTransaction transaction)
{
_transaction = transaction;
var dbContextTransaction = (IDbContextTransaction) _transaction.DbTransaction;
TransactionId = dbContextTransaction.TransactionId;
}

public void Dispose()
{
_transaction.Dispose();
}

public void Commit()
{
_transaction.Commit();
}

public void Rollback()
{
_transaction.Rollback();
}

public Guid TransactionId { get; }
}
}

src/DotNetCore.CAP.PostgreSql/PostgreSqlMonitoringApi.cs → src/DotNetCore.CAP.PostgreSql/IMonitoringApi.PostgreSql.cs Целия файл

@@ -128,7 +128,7 @@ select count(""Id"") from ""{0}"".""received"" where ""StatusName"" = N'Failed'
var sqlQuery =
$"select count(\"Id\") from \"{_options.Schema}\".\"{tableName}\" where Lower(\"StatusName\") = Lower(@state)";

var count = connection.ExecuteScalar<int>(sqlQuery, new { state = statusName });
var count = connection.ExecuteScalar<int>(sqlQuery, new {state = statusName});
return count;
}

@@ -170,7 +170,7 @@ with aggr as (
)
select ""Key"",""Count"" from aggr where ""Key""= Any(@keys);";

var valuesMap = connection.Query<TimelineCounter>(sqlQuery, new { keys = keyMaps.Keys.ToList(), statusName })
var valuesMap = connection.Query<TimelineCounter>(sqlQuery, new {keys = keyMaps.Keys.ToList(), statusName})
.ToList()
.ToDictionary(x => x.Key, x => x.Count);


src/DotNetCore.CAP.PostgreSql/PostgreSqlStorage.cs → src/DotNetCore.CAP.PostgreSql/IStorage.PostgreSql.cs Целия файл

@@ -100,10 +100,8 @@ namespace DotNetCore.CAP.PostgreSql
var batchSql = $@"
CREATE SCHEMA IF NOT EXISTS ""{schema}"";

DROP TABLE IF EXISTS ""{schema}"".""queue"";

CREATE TABLE IF NOT EXISTS ""{schema}"".""received""(
""Id"" SERIAL PRIMARY KEY NOT NULL,
""Id"" BIGINT PRIMARY KEY NOT NULL,
""Name"" VARCHAR(200) NOT NULL,
""Group"" VARCHAR(200) NULL,
""Content"" TEXT NULL,
@@ -114,7 +112,7 @@ CREATE TABLE IF NOT EXISTS ""{schema}"".""received""(
);

CREATE TABLE IF NOT EXISTS ""{schema}"".""published""(
""Id"" SERIAL PRIMARY KEY NOT NULL,
""Id"" BIGINT PRIMARY KEY NOT NULL,
""Name"" VARCHAR(200) NOT NULL,
""Content"" TEXT NULL,
""Retries"" INT NOT NULL,

src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageConnection.cs → src/DotNetCore.CAP.PostgreSql/IStorageConnection.PostgreSql.cs Целия файл

@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.PostgreSql
return new PostgreSqlStorageTransaction(this);
}

public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id)
public async Task<CapPublishedMessage> GetPublishedMessageAsync(long id)
{
var sql = $"SELECT * FROM \"{Options.Schema}\".\"published\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";

@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.PostgreSql
}
}

public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message)
public void StoreReceivedMessage(CapReceivedMessage message)
{
if (message == null)
{
@@ -58,15 +58,15 @@ namespace DotNetCore.CAP.PostgreSql
}

var sql =
$"INSERT INTO \"{Options.Schema}\".\"received\"(\"Name\",\"Group\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING \"Id\";";
$"INSERT INTO \"{Options.Schema}\".\"received\"(\"Id\",\"Name\",\"Group\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING \"Id\";";

using (var connection = new NpgsqlConnection(Options.ConnectionString))
{
return await connection.ExecuteScalarAsync<int>(sql, message);
connection.Execute(sql, message);
}
}

public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
public async Task<CapReceivedMessage> GetReceivedMessageAsync(long id)
{
var sql = $"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
@@ -77,7 +77,7 @@ namespace DotNetCore.CAP.PostgreSql

public async Task<IEnumerable<CapReceivedMessage>> GetReceivedMessagesOfNeedRetry()
{
var fourMinsAgo = DateTime.Now.AddMinutes(-4).ToString("O");
var fourMinsAgo = DateTime.Now.AddMinutes(-4).ToString("O");
var sql =
$"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"Retries\"<{_capOptions.FailedRetryCount} AND \"Added\"<'{fourMinsAgo}' AND (\"StatusName\"='{StatusName.Failed}' OR \"StatusName\"='{StatusName.Scheduled}') LIMIT 200;";
using (var connection = new NpgsqlConnection(Options.ConnectionString))
@@ -86,11 +86,7 @@ namespace DotNetCore.CAP.PostgreSql
}
}

public void Dispose()
{
}

public bool ChangePublishedState(int messageId, string state)
public bool ChangePublishedState(long messageId, string state)
{
var sql =
$"UPDATE \"{Options.Schema}\".\"published\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
@@ -101,7 +97,7 @@ namespace DotNetCore.CAP.PostgreSql
}
}

public bool ChangeReceivedState(int messageId, string state)
public bool ChangeReceivedState(long messageId, string state)
{
var sql =
$"UPDATE \"{Options.Schema}\".\"received\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
@@ -111,5 +107,9 @@ namespace DotNetCore.CAP.PostgreSql
return connection.Execute(sql) > 0;
}
}

public void Dispose()
{
}
}
}

src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageTransaction.cs → src/DotNetCore.CAP.PostgreSql/IStorageTransaction.PostgreSql.cs Целия файл

@@ -35,9 +35,7 @@ namespace DotNetCore.CAP.PostgreSql
}

var sql =
$@"UPDATE ""{
_schema
}"".""published"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
$@"UPDATE ""{_schema}"".""published"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}

@@ -49,9 +47,7 @@ namespace DotNetCore.CAP.PostgreSql
}

var sql =
$@"UPDATE ""{
_schema
}"".""received"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
$@"UPDATE ""{_schema}"".""received"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
}

@@ -66,29 +62,5 @@ namespace DotNetCore.CAP.PostgreSql
_dbTransaction.Dispose();
_dbConnection.Dispose();
}

public void EnqueueMessage(CapPublishedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var sql = $@"INSERT INTO ""{_schema}"".""queue"" values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Publish},
_dbTransaction);
}

public void EnqueueMessage(CapReceivedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var sql = $@"INSERT INTO ""{_schema}"".""queue"" values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Subscribe},
_dbTransaction);
}
}
}

+ 1
- 1
src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj Целия файл

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

<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.0" />
</ItemGroup>

<ItemGroup>


+ 8
- 0
src/DotNetCore.CAP.SqlServer/CAP.EFOptions.cs Целия файл

@@ -20,5 +20,13 @@ namespace DotNetCore.CAP
/// EF dbcontext type.
/// </summary>
internal Type DbContextType { get; set; }

internal bool IsSqlServer2008 { get; set; }

public EFOptions UseSqlServer2008()
{
IsSqlServer2008 = true;
return this;
}
}
}

+ 8
- 3
src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs Целия файл

@@ -4,6 +4,7 @@
using System;
using DotNetCore.CAP.Processor;
using DotNetCore.CAP.SqlServer;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

@@ -22,11 +23,15 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services)
{
services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<DiagnosticProcessorObserver>();
services.AddSingleton<IStorage, SqlServerStorage>();
services.AddSingleton<IStorageConnection, SqlServerStorageConnection>();
services.AddScoped<ICapPublisher, CapPublisher>();
services.AddScoped<ICallbackPublisher, CapPublisher>();

services.AddScoped<ICapPublisher, SqlServerPublisher>();
services.AddScoped<ICallbackPublisher, SqlServerPublisher>();

services.AddTransient<ICollectProcessor, SqlServerCollectProcessor>();
services.AddTransient<CapTransactionBase, SqlServerCapTransaction>();

AddSqlServerOptions(services);
}
@@ -44,7 +49,7 @@ namespace DotNetCore.CAP
using (var scope = x.CreateScope())
{
var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(sqlServerOptions.DbContextType);
var dbContext = (DbContext) provider.GetService(sqlServerOptions.DbContextType);
sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return sqlServerOptions;
}


+ 0
- 87
src/DotNetCore.CAP.SqlServer/CapPublisher.cs Целия файл

@@ -1,87 +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.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;

namespace DotNetCore.CAP.SqlServer
{
public class CapPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly DbContext _dbContext;
private readonly SqlServerOptions _options;

public CapPublisher(ILogger<CapPublisher> logger, IDispatcher dispatcher,
IServiceProvider provider, SqlServerOptions options)
: base(logger, dispatcher)
{
ServiceProvider = provider;
_options = options;

if (_options.DbContextType == null)
{
return;
}

IsUsingEF = true;
_dbContext = (DbContext) ServiceProvider.GetService(_options.DbContextType);
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
using (var conn = new SqlConnection(_options.ConnectionString))
{
var id = await conn.ExecuteScalarAsync<int>(PrepareSql(), message);
message.Id = id;
Enqueue(message);
}
}

protected override void PrepareConnectionForEF()
{
DbConnection = _dbContext.Database.GetDbConnection();
var dbContextTransaction = _dbContext.Database.CurrentTransaction;
var dbTrans = dbContextTransaction?.GetDbTransaction();
//DbTransaction is dispose in original
if (dbTrans?.Connection == null)
{
IsCapOpenedTrans = true;
dbContextTransaction?.Dispose();
dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted);
dbTrans = dbContextTransaction.GetDbTransaction();
}

DbTransaction = dbTrans;
}

protected override int Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return dbConnection.ExecuteScalar<int>(PrepareSql(), message, dbTransaction);
}

protected override Task<int> ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message)
{
return dbConnection.ExecuteScalarAsync<int>(PrepareSql(), message, dbTransaction);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO {_options.Schema}.[Published] ([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName])VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOPE_IDENTITY();";
}

#endregion private methods
}
}

+ 65
- 0
src/DotNetCore.CAP.SqlServer/Diagnostics/DiagnosticObserver.cs Целия файл

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

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Reflection;
using DotNetCore.CAP.Models;

namespace DotNetCore.CAP.SqlServer.Diagnostics
{
internal class DiagnosticObserver : IObserver<KeyValuePair<string, object>>
{
private const string SqlClientPrefix = "System.Data.SqlClient.";

public const string SqlAfterCommitTransaction = SqlClientPrefix + "WriteTransactionCommitAfter";
public const string SqlErrorCommitTransaction = SqlClientPrefix + "WriteTransactionCommitError";
private readonly ConcurrentDictionary<Guid, List<CapPublishedMessage>> _bufferList;
private readonly IDispatcher _dispatcher;

public DiagnosticObserver(IDispatcher dispatcher,
ConcurrentDictionary<Guid, List<CapPublishedMessage>> bufferList)
{
_dispatcher = dispatcher;
_bufferList = bufferList;
}

public void OnCompleted()
{
}

public void OnError(Exception error)
{
}

public void OnNext(KeyValuePair<string, object> evt)
{
if (evt.Key == SqlAfterCommitTransaction)
{
var sqlConnection = (SqlConnection) GetProperty(evt.Value, "Connection");
var transactionKey = sqlConnection.ClientConnectionId;
if (_bufferList.TryRemove(transactionKey, out var msgList))
{
foreach (var message in msgList)
{
_dispatcher.EnqueueToPublish(message);
}
}
}
else if (evt.Key == SqlErrorCommitTransaction)
{
var sqlConnection = (SqlConnection) GetProperty(evt.Value, "Connection");
var transactionKey = sqlConnection.ClientConnectionId;

_bufferList.TryRemove(transactionKey, out _);
}
}

private static object GetProperty(object _this, string propertyName)
{
return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this);
}
}
}

+ 41
- 0
src/DotNetCore.CAP.SqlServer/Diagnostics/DiagnosticProcessorObserver.cs Целия файл

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

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using DotNetCore.CAP.Models;

namespace DotNetCore.CAP.SqlServer.Diagnostics
{
public class DiagnosticProcessorObserver : IObserver<DiagnosticListener>
{
public const string DiagnosticListenerName = "SqlClientDiagnosticListener";
private readonly IDispatcher _dispatcher;

public DiagnosticProcessorObserver(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
BufferList = new ConcurrentDictionary<Guid, List<CapPublishedMessage>>();
}

public ConcurrentDictionary<Guid, List<CapPublishedMessage>> BufferList { get; }

public void OnCompleted()
{
}

public void OnError(Exception error)
{
}

public void OnNext(DiagnosticListener listener)
{
if (listener.Name == DiagnosticListenerName)
{
listener.Subscribe(new DiagnosticObserver(_dispatcher, BufferList));
}
}
}
}

+ 63
- 0
src/DotNetCore.CAP.SqlServer/ICapPublisher.SqlServer.cs Целия файл

@@ -0,0 +1,63 @@
// 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.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;

namespace DotNetCore.CAP.SqlServer
{
public class SqlServerPublisher : CapPublisherBase, ICallbackPublisher
{
private readonly SqlServerOptions _options;

public SqlServerPublisher(IServiceProvider provider) : base(provider)
{
_options = ServiceProvider.GetService<SqlServerOptions>();
}

public async Task PublishCallbackAsync(CapPublishedMessage message)
{
await PublishAsyncInternal(message);
}

protected override async Task ExecuteAsync(CapPublishedMessage message, ICapTransaction transaction,
CancellationToken cancel = default(CancellationToken))
{
if (NotUseTransaction)
{
using (var connection = new SqlConnection(_options.ConnectionString))
{
await connection.ExecuteAsync(PrepareSql(), message);
return;
}
}

var dbTrans = transaction.DbTransaction as IDbTransaction;
if (dbTrans == null && transaction.DbTransaction is IDbContextTransaction dbContextTrans)
{
dbTrans = dbContextTrans.GetDbTransaction();
}

var conn = dbTrans?.Connection;
await conn.ExecuteAsync(PrepareSql(), message, dbTrans);
}

#region private methods

private string PrepareSql()
{
return
$"INSERT INTO {_options.Schema}.[Published] ([Id],[Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName])VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
}

#endregion private methods
}
}

+ 170
- 0
src/DotNetCore.CAP.SqlServer/ICapTransaction.SqlServer.cs Целия файл

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

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Models;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
namespace DotNetCore.CAP
{
public class SqlServerCapTransaction : CapTransactionBase
{
private readonly DbContext _dbContext;
private readonly DiagnosticProcessorObserver _diagnosticProcessor;

public SqlServerCapTransaction(
IDispatcher dispatcher,
SqlServerOptions sqlServerOptions,
IServiceProvider serviceProvider) : base(dispatcher)
{
if (sqlServerOptions.DbContextType != null)
{
_dbContext = serviceProvider.GetService(sqlServerOptions.DbContextType) as DbContext;
}

_diagnosticProcessor = serviceProvider.GetRequiredService<DiagnosticProcessorObserver>();
}

protected override void AddToSent(CapPublishedMessage msg)
{
if (DbTransaction is NoopTransaction)
{
base.AddToSent(msg);
return;
}

var dbTransaction = DbTransaction as IDbTransaction;
if (dbTransaction == null)
{
if (DbTransaction is IDbContextTransaction dbContextTransaction)
{
dbTransaction = dbContextTransaction.GetDbTransaction();
}

if (dbTransaction == null)
{
throw new ArgumentNullException(nameof(DbTransaction));
}
}

var transactionKey = ((SqlConnection) dbTransaction.Connection).ClientConnectionId;
if (_diagnosticProcessor.BufferList.TryGetValue(transactionKey, out var list))
{
list.Add(msg);
}
else
{
var msgList = new List<CapPublishedMessage>(1) {msg};
_diagnosticProcessor.BufferList.TryAdd(transactionKey, msgList);
}
}

public override void Commit()
{
switch (DbTransaction)
{
case NoopTransaction _:
Flush();
break;
case IDbTransaction dbTransaction:
dbTransaction.Commit();
break;
case IDbContextTransaction dbContextTransaction:
_dbContext?.SaveChanges();
dbContextTransaction.Commit();
break;
}
}

public override void Rollback()
{
switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Rollback();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Rollback();
break;
}
}

public override void Dispose()
{
switch (DbTransaction)
{
case IDbTransaction dbTransaction:
dbTransaction.Dispose();
break;
case IDbContextTransaction dbContextTransaction:
dbContextTransaction.Dispose();
break;
}
}
}

public static class CapTransactionExtensions
{
public static ICapTransaction Begin(this ICapTransaction transaction,
IDbTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

public static ICapTransaction Begin(this ICapTransaction transaction,
IDbContextTransaction dbTransaction, bool autoCommit = false)
{
transaction.DbTransaction = dbTransaction;
transaction.AutoCommit = autoCommit;

return transaction;
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="dbConnection">The <see cref="IDbConnection" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="ICapTransaction" /> object.</returns>
public static IDbTransaction BeginTransaction(this IDbConnection dbConnection,
ICapPublisher publisher, bool autoCommit = false)
{
if (dbConnection.State == ConnectionState.Closed)
{
dbConnection.Open();
}

var dbTransaction = dbConnection.BeginTransaction();
var capTransaction = publisher.Transaction.Begin(dbTransaction, autoCommit);
return (IDbTransaction) capTransaction.DbTransaction;
}

/// <summary>
/// Start the CAP transaction
/// </summary>
/// <param name="database">The <see cref="DatabaseFacade" />.</param>
/// <param name="publisher">The <see cref="ICapPublisher" />.</param>
/// <param name="autoCommit">Whether the transaction is automatically committed when the message is published</param>
/// <returns>The <see cref="IDbContextTransaction" /> of EF dbcontext transaction object.</returns>
public static IDbContextTransaction BeginTransaction(this DatabaseFacade database,
ICapPublisher publisher, bool autoCommit = false)
{
var trans = database.BeginTransaction();
var capTrans = publisher.Transaction.Begin(trans, autoCommit);
return new CapEFDbTransaction(capTrans);
}
}
}

+ 38
- 0
src/DotNetCore.CAP.SqlServer/IDbContextTransaction.CAP.cs Целия файл

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

using System;
using DotNetCore.CAP;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore.Storage
{
internal class CapEFDbTransaction : IDbContextTransaction
{
private readonly ICapTransaction _transaction;

public CapEFDbTransaction(ICapTransaction transaction)
{
_transaction = transaction;
var dbContextTransaction = (IDbContextTransaction) _transaction.DbTransaction;
TransactionId = dbContextTransaction.TransactionId;
}

public void Dispose()
{
_transaction.Dispose();
}

public void Commit()
{
_transaction.Commit();
}

public void Rollback()
{
_transaction.Rollback();
}

public Guid TransactionId { get; }
}
}

src/DotNetCore.CAP.SqlServer/SqlServerMonitoringApi.cs → src/DotNetCore.CAP.SqlServer/IMonitoringApi.SqlServer.cs Целия файл

@@ -89,10 +89,17 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
where += " and content like '%@Content%'";
}

var sqlQuery2008 =
$@"select * from
(SELECT t.*, ROW_NUMBER() OVER(order by t.Added desc) AS rownumber
from [{_options.Schema}].{tableName} as t
where 1=1 {where}) as tbl
where tbl.rownumber between @offset and @offset + @limit";

var sqlQuery =
$"select * from [{_options.Schema}].{tableName} where 1=1 {where} order by Added desc offset @Offset rows fetch next @Limit rows only";

return UseConnection(conn => conn.Query<MessageDto>(sqlQuery, new
return UseConnection(conn => conn.Query<MessageDto>(_options.IsSqlServer2008 ? sqlQuery2008 : sqlQuery, new
{
queryDto.StatusName,
queryDto.Group,
@@ -128,7 +135,7 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
var sqlQuery =
$"select count(Id) from [{_options.Schema}].{tableName} with (nolock) where StatusName = @state";

var count = connection.ExecuteScalar<int>(sqlQuery, new { state = statusName });
var count = connection.ExecuteScalar<int>(sqlQuery, new {state = statusName});
return count;
}

@@ -159,9 +166,18 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
string statusName,
IDictionary<string, DateTime> keyMaps)
{
//SQL Server 2012+
var sqlQuery =
$@"
var sqlQuery2008 = $@"
with aggr as (
select replace(convert(varchar, Added, 111), '/','-') + '-' + CONVERT(varchar, DATEPART(hh, Added)) as [Key],
count(id) [Count]
from [{_options.Schema}].{tableName}
where StatusName = @statusName
group by replace(convert(varchar, Added, 111), '/','-') + '-' + CONVERT(varchar, DATEPART(hh, Added))
)
select [Key], [Count] from aggr with (nolock) where [Key] in @keys;";

//SQL Server 2012+
var sqlQuery = $@"
with aggr as (
select FORMAT(Added,'yyyy-MM-dd-HH') as [Key],
count(id) [Count]
@@ -172,8 +188,8 @@ with aggr as (
select [Key], [Count] from aggr with (nolock) where [Key] in @keys;";

var valuesMap = connection.Query<TimelineCounter>(
sqlQuery,
new { keys = keyMaps.Keys, statusName })
_options.IsSqlServer2008 ? sqlQuery2008 : sqlQuery,
new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => x.Key, x => x.Count);

foreach (var key in keyMaps.Keys)

src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs → src/DotNetCore.CAP.SqlServer/IStorage.SqlServer.cs Целия файл

@@ -4,10 +4,12 @@
using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.Extensions.Logging;

namespace DotNetCore.CAP.SqlServer
@@ -15,15 +17,18 @@ namespace DotNetCore.CAP.SqlServer
public class SqlServerStorage : IStorage
{
private readonly CapOptions _capOptions;
private readonly DiagnosticProcessorObserver _diagnosticProcessorObserver;
private readonly IDbConnection _existingConnection = null;
private readonly ILogger _logger;
private readonly SqlServerOptions _options;

public SqlServerStorage(ILogger<SqlServerStorage> logger,
CapOptions capOptions,
SqlServerOptions options)
SqlServerOptions options,
DiagnosticProcessorObserver diagnosticProcessorObserver)
{
_options = options;
_diagnosticProcessorObserver = diagnosticProcessorObserver;
_logger = logger;
_capOptions = capOptions;
}
@@ -38,7 +43,7 @@ namespace DotNetCore.CAP.SqlServer
return new SqlServerMonitoringApi(this, _options);
}

public async Task InitializeAsync(CancellationToken cancellationToken)
public async Task InitializeAsync(CancellationToken cancellationToken = default(CancellationToken))
{
if (cancellationToken.IsCancellationRequested)
{
@@ -53,6 +58,8 @@ namespace DotNetCore.CAP.SqlServer
}

_logger.LogDebug("Ensuring all create database tables script are applied.");

DiagnosticListener.AllListeners.Subscribe(_diagnosticProcessorObserver);
}

protected virtual string CreateDbTablesScript(string schema)
@@ -64,15 +71,10 @@ BEGIN
EXEC('CREATE SCHEMA [{schema}]')
END;

IF OBJECT_ID(N'[{schema}].[Queue]',N'U') IS NOT NULL
BEGIN
DROP TABLE [{schema}].[Queue];
END;

IF OBJECT_ID(N'[{schema}].[Received]',N'U') IS NULL
BEGIN
CREATE TABLE [{schema}].[Received](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id] [bigint] NOT NULL,
[Name] [nvarchar](200) NOT NULL,
[Group] [nvarchar](200) NULL,
[Content] [nvarchar](max) NULL,
@@ -90,7 +92,7 @@ END;
IF OBJECT_ID(N'[{schema}].[Published]',N'U') IS NULL
BEGIN
CREATE TABLE [{schema}].[Published](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id] [bigint] NOT NULL,
[Name] [nvarchar](200) NOT NULL,
[Content] [nvarchar](max) NULL,
[Retries] [int] NOT NULL,

src/DotNetCore.CAP.SqlServer/SqlServerStorageConnection.cs → src/DotNetCore.CAP.SqlServer/IStorageConnection.SqlServer.cs Целия файл

@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.SqlServer
return new SqlServerStorageTransaction(this);
}

public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id)
public async Task<CapPublishedMessage> GetPublishedMessageAsync(long id)
{
var sql = $@"SELECT * FROM [{Options.Schema}].[Published] WITH (readpast) WHERE Id={id}";

@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.SqlServer
}
}

public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message)
public void StoreReceivedMessage(CapReceivedMessage message)
{
if (message == null)
{
@@ -58,16 +58,16 @@ namespace DotNetCore.CAP.SqlServer
}

var sql = $@"
INSERT INTO [{Options.Schema}].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName])
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOPE_IDENTITY();";
INSERT INTO [{Options.Schema}].[Received]([Id],[Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName])
VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";

using (var connection = new SqlConnection(Options.ConnectionString))
{
return await connection.ExecuteScalarAsync<int>(sql, message);
connection.Execute(sql, message);
}
}

public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id)
public async Task<CapReceivedMessage> GetReceivedMessageAsync(long id)
{
var sql = $@"SELECT * FROM [{Options.Schema}].[Received] WITH (readpast) WHERE Id={id}";
using (var connection = new SqlConnection(Options.ConnectionString))
@@ -87,7 +87,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOP
}
}

public bool ChangePublishedState(int messageId, string state)
public bool ChangePublishedState(long messageId, string state)
{
var sql =
$"UPDATE [{Options.Schema}].[Published] SET Retries=Retries+1,ExpiresAt=NULL,StatusName = '{state}' WHERE Id={messageId}";
@@ -98,7 +98,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOP
}
}

public bool ChangeReceivedState(int messageId, string state)
public bool ChangeReceivedState(long messageId, string state)
{
var sql =
$"UPDATE [{Options.Schema}].[Received] SET Retries=Retries+1,ExpiresAt=NULL,StatusName = '{state}' WHERE Id={messageId}";
@@ -108,9 +108,5 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOP
return connection.Execute(sql) > 0;
}
}

public void Dispose()
{
}
}
}

src/DotNetCore.CAP.SqlServer/SqlServerStorageTransaction.cs → src/DotNetCore.CAP.SqlServer/IStorageTransaction.SqlServer.cs Целия файл

@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.SqlServer
{
private readonly IDbConnection _dbConnection;

private readonly IDbTransaction _dbTransaction;
private readonly string _schema;

public SqlServerStorageTransaction(SqlServerStorageConnection connection)
@@ -24,7 +23,6 @@ namespace DotNetCore.CAP.SqlServer

_dbConnection = new SqlConnection(options.ConnectionString);
_dbConnection.Open();
_dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted);
}

public void UpdateMessage(CapPublishedMessage message)
@@ -36,7 +34,7 @@ namespace DotNetCore.CAP.SqlServer

var sql =
$"UPDATE [{_schema}].[Published] SET [Retries] = @Retries,[Content] = @Content,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
_dbConnection.Execute(sql, message);
}

public void UpdateMessage(CapReceivedMessage message)
@@ -48,43 +46,17 @@ namespace DotNetCore.CAP.SqlServer

var sql =
$"UPDATE [{_schema}].[Received] SET [Retries] = @Retries,[Content] = @Content,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction);
_dbConnection.Execute(sql, message);
}

public Task CommitAsync()
{
_dbTransaction.Commit();
return Task.CompletedTask;
}

public void Dispose()
{
_dbTransaction.Dispose();
_dbConnection.Dispose();
}

public void EnqueueMessage(CapPublishedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Publish},
_dbTransaction);
}

public void EnqueueMessage(CapReceivedMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}

var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);";
_dbConnection.Execute(sql, new CapQueue {MessageId = message.Id, MessageType = MessageType.Subscribe},
_dbTransaction);
}
}
}

+ 64
- 186
src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs Целия файл

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

using System;
using System.Data;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using DotNetCore.CAP.Diagnostics;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;

namespace DotNetCore.CAP.Abstractions
{
public abstract class CapPublisherBase : ICapPublisher, IDisposable
public abstract class CapPublisherBase : ICapPublisher
{
private readonly IDispatcher _dispatcher;
private readonly ILogger _logger;
private readonly CapTransactionBase _transaction;
private readonly IMessagePacker _msgPacker;
private readonly IContentSerializer _serializer;

protected bool NotUseTransaction;

// diagnostics listener
// ReSharper disable once InconsistentNaming
private static readonly DiagnosticListener s_diagnosticListener =
protected static readonly DiagnosticListener s_diagnosticListener =
new DiagnosticListener(CapDiagnosticListenerExtensions.DiagnosticListenerName);

protected CapPublisherBase(ILogger<CapPublisherBase> logger, IDispatcher dispatcher)
{
_logger = logger;
_dispatcher = dispatcher;
}

protected IDbConnection DbConnection { get; set; }
protected IDbTransaction DbTransaction { get; set; }
protected bool IsCapOpenedTrans { get; set; }
protected bool IsCapOpenedConn { get; set; }
protected bool IsUsingEF { get; set; }
protected IServiceProvider ServiceProvider { get; set; }

public void Publish<T>(string name, T contentObj, string callbackName = null)
{
CheckIsUsingEF(name);
PrepareConnectionForEF();

PublishWithTrans(name, contentObj, callbackName);
}

public Task PublishAsync<T>(string name, T contentObj, string callbackName = null)
protected CapPublisherBase(IServiceProvider service)
{
CheckIsUsingEF(name);
PrepareConnectionForEF();
return PublishWithTransAsync(name, contentObj, callbackName);
ServiceProvider = service;
_transaction = service.GetRequiredService<CapTransactionBase>();
_msgPacker = service.GetRequiredService<IMessagePacker>();
_serializer = service.GetRequiredService<IContentSerializer>();
}

public void Publish<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{
CheckIsAdoNet(name);
PrepareConnectionForAdo(dbTransaction);
protected IServiceProvider ServiceProvider { get; }

PublishWithTrans(name, contentObj, callbackName);
}
public ICapTransaction Transaction => _transaction;

public Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{
CheckIsAdoNet(name);
PrepareConnectionForAdo(dbTransaction);

return PublishWithTransAsync(name, contentObj, callbackName);
}

protected void Enqueue(CapPublishedMessage message)
{
_dispatcher.EnqueueToPublish(message);
}

protected abstract void PrepareConnectionForEF();

protected abstract int Execute(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message);

protected abstract Task<int> ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction,
CapPublishedMessage message);

protected virtual string Serialize<T>(T obj, string callbackName = null)
public void Publish<T>(string name, T contentObj, string callbackName = null)
{
var packer = (IMessagePacker)ServiceProvider.GetService(typeof(IMessagePacker));
string content;
if (obj != null)
{
if (Helper.IsComplexType(obj.GetType()))
{
var serializer = (IContentSerializer)ServiceProvider.GetService(typeof(IContentSerializer));
content = serializer.Serialize(obj);
}
else
{
content = obj.ToString();
}
}
else
{
content = string.Empty;
}

var message = new CapMessageDto(content)
var message = new CapPublishedMessage
{
CallbackName = callbackName
Id = SnowflakeId.Default().NextId(),
Name = name,
Content = Serialize(contentObj, callbackName),
StatusName = StatusName.Scheduled
};
return packer.Pack(message);
}

#region private methods

private void PrepareConnectionForAdo(IDbTransaction dbTransaction)
{
DbTransaction = dbTransaction ?? throw new ArgumentNullException(nameof(dbTransaction));
DbConnection = DbTransaction.Connection;
if (DbConnection.State != ConnectionState.Open)
{
IsCapOpenedConn = true;
DbConnection.Open();
}
PublishAsyncInternal(message).GetAwaiter().GetResult();
}

private void CheckIsUsingEF(string name)
public async Task PublishAsync<T>(string name, T contentObj, string callbackName = null,
CancellationToken cancellationToken = default(CancellationToken))
{
if (name == null)
var message = new CapPublishedMessage
{
throw new ArgumentNullException(nameof(name));
}
Id = SnowflakeId.Default().NextId(),
Name = name,
Content = Serialize(contentObj, callbackName),
StatusName = StatusName.Scheduled
};

if (!IsUsingEF)
{
throw new InvalidOperationException(
"If you are using the EntityFramework, you need to configure the DbContextType first." +
" otherwise you need to use overloaded method with IDbTransaction.");
}
await PublishAsyncInternal(message);
}

private void CheckIsAdoNet(string name)
protected async Task PublishAsyncInternal(CapPublishedMessage message)
{
if (name == null)
if (Transaction.DbTransaction == null)
{
throw new ArgumentNullException(nameof(name));
NotUseTransaction = true;
Transaction.DbTransaction = new NoopTransaction();
}

if (IsUsingEF)
{
throw new InvalidOperationException(
"If you are using the EntityFramework, you do not need to use this overloaded.");
}
}

private async Task PublishWithTransAsync<T>(string name, T contentObj, string callbackName = null)
{
Guid operationId = default(Guid);
var content = Serialize(contentObj, callbackName);

var message = new CapPublishedMessage
{
Name = name,
Content = content,
StatusName = StatusName.Scheduled
};

try
{
operationId = s_diagnosticListener.WritePublishMessageStoreBefore(message);

var id = await ExecuteAsync(DbConnection, DbTransaction, message);
await ExecuteAsync(message, Transaction);

ClosedCap();
_transaction.AddToSent(message);

if (id > 0)
{
_logger.LogInformation($"message [{message}] has been persisted in the database.");
s_diagnosticListener.WritePublishMessageStoreAfter(operationId, message);

message.Id = id;
s_diagnosticListener.WritePublishMessageStoreAfter(operationId, message);

Enqueue(message);
if (NotUseTransaction || Transaction.AutoCommit)
{
_transaction.Commit();
}
}
catch (Exception e)
{
_logger.LogError(e, "An exception was occurred when publish message async. exception message:" + name);
s_diagnosticListener.WritePublishMessageStoreError(operationId, message, e);
Console.WriteLine(e);
throw;
}
}

private void PublishWithTrans<T>(string name, T contentObj, string callbackName = null)
{
Guid operationId = default(Guid);

var content = Serialize(contentObj, callbackName);

var message = new CapPublishedMessage
{
Name = name,
Content = content,
StatusName = StatusName.Scheduled
};

try
finally
{
Console.WriteLine("================22222222222222=====================");
operationId = s_diagnosticListener.WritePublishMessageStoreBefore(message);

var id = Execute(DbConnection, DbTransaction, message);
Console.WriteLine("================777777777777777777777=====================");
ClosedCap();

if (id > 0)
if (NotUseTransaction || Transaction.AutoCommit)
{
_logger.LogInformation($"message [{message}] has been persisted in the database.");
s_diagnosticListener.WritePublishMessageStoreAfter(operationId, message);
message.Id = id;
Enqueue(message);
_transaction.Dispose();
}
}
catch (Exception e)
{
_logger.LogError(e, "An exception was occurred when publish message. message:" + name);
s_diagnosticListener.WritePublishMessageStoreError(operationId, message, e);
Console.WriteLine(e);
throw;
}
}

private void ClosedCap()
protected abstract Task ExecuteAsync(CapPublishedMessage message,
ICapTransaction transaction,
CancellationToken cancel = default(CancellationToken));

protected virtual string Serialize<T>(T obj, string callbackName = null)
{
if (IsCapOpenedTrans)
string content;
if (obj != null)
{
DbTransaction.Commit();
DbTransaction.Dispose();
content = Helper.IsComplexType(obj.GetType())
? _serializer.Serialize(obj)
: obj.ToString();
}

if (IsCapOpenedConn)
else
{
DbConnection.Dispose();
content = string.Empty;
}
var message = new CapMessageDto(content)
{
CallbackName = callbackName
};
return _msgPacker.Pack(message);
}

public void Dispose()
{
DbTransaction?.Dispose();
DbConnection?.Dispose();
}

#endregion private methods
}
}

+ 21
- 0
src/DotNetCore.CAP/Abstractions/IMongoTransaction.cs Целия файл

@@ -0,0 +1,21 @@
// 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.Threading.Tasks;

namespace DotNetCore.CAP.Abstractions
{
public interface IMongoTransaction : IDisposable
{
/// <summary>
/// If set true, the session.CommitTransaction() will be called automatically.
/// </summary>
/// <value></value>
bool AutoCommit { get; set; }

Task<IMongoTransaction> BegeinAsync(bool autoCommit = true);

IMongoTransaction Begein(bool autoCommit = true);
}
}

+ 15
- 1
src/DotNetCore.CAP/CAP.AppBuilderExtensions.cs Целия файл

@@ -4,6 +4,7 @@
using System;
using DotNetCore.CAP;
using DotNetCore.CAP.Dashboard.GatewayProxy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

// ReSharper disable once CheckNamespace
@@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Builder
/// <summary>
/// app extensions for <see cref="IApplicationBuilder" />
/// </summary>
public static class AppBuilderExtensions
internal static class AppBuilderExtensions
{
/// <summary>
/// Enables cap for the current application
@@ -70,4 +71,17 @@ namespace Microsoft.AspNetCore.Builder
}
}
}

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

next(app);
};
}
}
}

+ 15
- 13
src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs Целия файл

@@ -8,6 +8,8 @@ using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Processor;
using DotNetCore.CAP.Processor.States;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;

// ReSharper disable once CheckNamespace
@@ -24,9 +26,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// <param name="services">The services available in the application.</param>
/// <param name="setupAction">An action to configure the <see cref="CapOptions" />.</param>
/// <returns>An <see cref="CapBuilder" /> for application services.</returns>
public static CapBuilder AddCap(
this IServiceCollection services,
Action<CapOptions> setupAction)
public static CapBuilder AddCap(this IServiceCollection services, Action<CapOptions> setupAction)
{
if (setupAction == null)
{
@@ -34,8 +34,8 @@ namespace Microsoft.Extensions.DependencyInjection
}

services.TryAddSingleton<CapMarkerService>();
services.Configure(setupAction);

//Consumer service
AddSubscribeServices(services);

//Serializer and model binder
@@ -49,18 +49,18 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<MethodMatcherCache>();

//Bootstrapper and Processors
services.AddSingleton<IProcessingServer, ConsumerHandler>();
services.AddSingleton<IProcessingServer, CapProcessingServer>();
services.AddSingleton<IBootstrapper, DefaultBootstrapper>();
services.AddSingleton<IStateChanger, StateChanger>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IProcessingServer, ConsumerHandler>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IProcessingServer, CapProcessingServer>());
services.TryAddSingleton<IBootstrapper, DefaultBootstrapper>();
services.TryAddSingleton<IStateChanger, StateChanger>();

//Queue's message processor
services.AddTransient<NeedRetryMessageProcessor>();
services.TryAddSingleton<NeedRetryMessageProcessor>();

//Sender and Executors
services.AddSingleton<IDispatcher, Dispatcher>();
services.TryAddSingleton<IDispatcher, Dispatcher>();
// Warning: IPublishMessageSender need to inject at extension project.
services.AddSingleton<ISubscriberExecutor, DefaultSubscriberExecutor>();
services.TryAddSingleton<ISubscriberExecutor, DefaultSubscriberExecutor>();

//Options and extension service
var options = new CapOptions();
@@ -69,9 +69,11 @@ namespace Microsoft.Extensions.DependencyInjection
{
serviceExtension.AddServices(services);
}

services.AddSingleton(options);

//Startup and Middleware
services.AddTransient<IStartupFilter, CapStartupFilter>();

return new CapBuilder(services);
}

@@ -90,7 +92,7 @@ namespace Microsoft.Extensions.DependencyInjection

foreach (var service in consumerListenerServices)
{
services.AddTransient(service.Key, service.Value);
services.TryAddEnumerable(ServiceDescriptor.Transient(service.Key, service.Value));
}
}
}

+ 2
- 2
src/DotNetCore.CAP/Dashboard/DashboardRoutes.cs Целия файл

@@ -82,14 +82,14 @@ namespace DotNetCore.CAP.Dashboard

Routes.AddJsonResult("/published/message/(?<Id>.+)", x =>
{
var id = int.Parse(x.UriMatch.Groups["Id"].Value);
var id = long.Parse(x.UriMatch.Groups["Id"].Value);
var message = x.Storage.GetConnection().GetPublishedMessageAsync(id)
.GetAwaiter().GetResult();
return message.Content;
});
Routes.AddJsonResult("/received/message/(?<Id>.+)", x =>
{
var id = int.Parse(x.UriMatch.Groups["Id"].Value);
var id = long.Parse(x.UriMatch.Groups["Id"].Value);
var message = x.Storage.GetConnection().GetReceivedMessageAsync(id)
.GetAwaiter().GetResult();
return message.Content;


+ 1
- 1
src/DotNetCore.CAP/Dashboard/Monitoring/MessageDto.cs Целия файл

@@ -7,7 +7,7 @@ namespace DotNetCore.CAP.Dashboard.Monitoring
{
public class MessageDto
{
public int Id { get; set; }
public long Id { get; set; }

public string Group { get; set; }



+ 1
- 5
src/DotNetCore.CAP/DotNetCore.CAP.csproj Целия файл

@@ -31,16 +31,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Consul" Version="0.7.2.4" />
<PackageReference Include="Consul" Version="0.7.2.6" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Data.Common" Version="4.3.0" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.5.0" />
<PackageReference Include="System.Threading.ThreadPool" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
</ItemGroup>


+ 8
- 30
src/DotNetCore.CAP/ICapPublisher.cs Целия файл

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

using System.Data;
using System.Threading;
using System.Threading.Tasks;

namespace DotNetCore.CAP
@@ -12,47 +12,25 @@ namespace DotNetCore.CAP
public interface ICapPublisher
{
/// <summary>
/// (EntityFramework) Asynchronous publish a object message.
/// <para>
/// If you are using the EntityFramework, you need to configure the DbContextType first.
/// otherwise you need to use overloaded method with IDbTransaction.
/// </para>
/// CAP transaction context object
/// </summary>
/// <typeparam name="T">The type of content object.</typeparam>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="contentObj">message body content, that will be serialized of json.</param>
/// <param name="callbackName">callback subscriber name</param>
Task PublishAsync<T>(string name, T contentObj, string callbackName = null);
ICapTransaction Transaction { get; }

/// <summary>
/// (EntityFramework) Publish a object message.
/// <para>
/// If you are using the EntityFramework, you need to configure the DbContextType first.
/// otherwise you need to use overloaded method with IDbTransaction.
/// </para>
/// Asynchronous publish an object message.
/// </summary>
/// <typeparam name="T">The type of content object.</typeparam>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="contentObj">message body content, that will be serialized of json.</param>
/// <param name="callbackName">callback subscriber name</param>
void Publish<T>(string name, T contentObj, string callbackName = null);
/// <param name="cancellationToken"></param>
Task PublishAsync<T>(string name, T contentObj, string callbackName = null, CancellationToken cancellationToken = default(CancellationToken));

/// <summary>
/// (ado.net) Asynchronous publish a object message.
/// Publish an object message.
/// </summary>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="contentObj">message body content, that will be serialized of json.</param>
/// <param name="dbTransaction">the transaction of <see cref="IDbTransaction" /></param>
/// <param name="callbackName">callback subscriber name</param>
Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null);

/// <summary>
/// (ado.net) Publish a object message.
/// </summary>
/// <param name="name">the topic name or exchange router key.</param>
/// <param name="contentObj">message body content, that will be serialized of json.</param>
/// <param name="dbTransaction">the transaction of <see cref="IDbTransaction" /></param>
/// <param name="callbackName">callback subscriber name</param>
void Publish<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null);
void Publish<T>(string name, T contentObj, string callbackName = null);
}
}

+ 41
- 0
src/DotNetCore.CAP/ICapTransaction.Base.cs Целия файл

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using DotNetCore.CAP.Models;

namespace DotNetCore.CAP
{
public abstract class CapTransactionBase : ICapTransaction
{
private readonly IDispatcher _dispatcher;

private readonly IList<CapPublishedMessage> _bufferList;

protected CapTransactionBase(IDispatcher dispatcher)
{
_dispatcher = dispatcher;
_bufferList = new List<CapPublishedMessage>(1);
}

public bool AutoCommit { get; set; }

public object DbTransaction { get; set; }

protected internal virtual void AddToSent(CapPublishedMessage msg)
{
_bufferList.Add(msg);
}
protected void Flush()
{
foreach (var message in _bufferList)
{
_dispatcher.EnqueueToPublish(message);
}
}

public abstract void Commit();

public abstract void Rollback();

public abstract void Dispose();
}
}

+ 33
- 0
src/DotNetCore.CAP/ICapTransaction.cs Целия файл

@@ -0,0 +1,33 @@
// 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;

namespace DotNetCore.CAP
{
/// <summary>
/// CAP transaction wrapper, used to wrap database transactions, provides a consistent user interface
/// </summary>
public interface ICapTransaction : IDisposable
{
/// <summary>
/// A flag is used to indicate whether the transaction is automatically committed after the message is published
/// </summary>
bool AutoCommit { get; set; }

/// <summary>
/// Database transaction object, can be converted to a specific database transaction object or IDBTransaction when used
/// </summary>
object DbTransaction { get; set; }

/// <summary>
/// Submit the transaction context of the CAP, we will send the message to the message queue at the time of submission
/// </summary>
void Commit();

/// <summary>
/// We will delete the message data that has not been sstore in the buffer data of current transaction context.
/// </summary>
void Rollback();
}
}

+ 2
- 4
src/DotNetCore.CAP/IConsumerHandler.Default.cs Целия файл

@@ -112,6 +112,7 @@ namespace DotNetCore.CAP

var receivedMessage = new CapReceivedMessage(messageContext)
{
Id = SnowflakeId.Default().NextId(),
StatusName = StatusName.Scheduled,
Content = messageBody
};
@@ -170,10 +171,7 @@ namespace DotNetCore.CAP

private void StoreMessage(CapReceivedMessage receivedMessage)
{
var id = _connection.StoreReceivedMessageAsync(receivedMessage)
.GetAwaiter().GetResult();

receivedMessage.Id = id;
_connection.StoreReceivedMessage(receivedMessage);
}

private (Guid, string) TracingBefore(string topic, string values)


+ 0
- 2
src/DotNetCore.CAP/IPublishMessageSender.Base.cs Целия файл

@@ -173,8 +173,6 @@ namespace DotNetCore.CAP
du);

s_diagnosticListener.WritePublishAfter(eventData);

_logger.MessageHasBeenSent(du.TotalSeconds);
}

private void TracingError(Guid operationId, CapPublishedMessage message, OperateResult result, DateTimeOffset startTime, TimeSpan du)


+ 6
- 7
src/DotNetCore.CAP/IStorageConnection.cs Целия файл

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

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DotNetCore.CAP.Models;
@@ -11,7 +10,7 @@ namespace DotNetCore.CAP
/// <summary>
/// Represents a connection to the storage.
/// </summary>
public interface IStorageConnection : IDisposable
public interface IStorageConnection
{
//Sent messages

@@ -19,7 +18,7 @@ namespace DotNetCore.CAP
/// Returns the message with the given id.
/// </summary>
/// <param name="id">The message's id.</param>
Task<CapPublishedMessage> GetPublishedMessageAsync(int id);
Task<CapPublishedMessage> GetPublishedMessageAsync(long id);

/// <summary>
/// Returns executed failed messages.
@@ -32,13 +31,13 @@ namespace DotNetCore.CAP
/// Stores the message.
/// </summary>
/// <param name="message">The message to store.</param>
Task<int> StoreReceivedMessageAsync(CapReceivedMessage message);
void StoreReceivedMessage(CapReceivedMessage message);

/// <summary>
/// Returns the message with the given id.
/// </summary>
/// <param name="id">The message's id.</param>
Task<CapReceivedMessage> GetReceivedMessageAsync(int id);
Task<CapReceivedMessage> GetReceivedMessageAsync(long id);

/// <summary>
/// Returns executed failed message.
@@ -55,13 +54,13 @@ namespace DotNetCore.CAP
/// </summary>
/// <param name="messageId">Message id</param>
/// <param name="state">State name</param>
bool ChangePublishedState(int messageId, string state);
bool ChangePublishedState(long messageId, string state);

/// <summary>
/// Change specified message's state of received message
/// </summary>
/// <param name="messageId">Message id</param>
/// <param name="state">State name</param>
bool ChangeReceivedState(int messageId, string state);
bool ChangeReceivedState(long messageId, string state);
}
}

+ 27
- 253
src/DotNetCore.CAP/Infrastructure/ObjectId.cs Целия файл

@@ -18,15 +18,13 @@ namespace DotNetCore.CAP.Infrastructure
public struct ObjectId : IComparable<ObjectId>, IEquatable<ObjectId>
{
// private static fields
private static readonly DateTime __unixEpoch;
private static readonly DateTime UnixEpoch;

private static readonly long __dateTimeMaxValueMillisecondsSinceEpoch;
private static readonly long __dateTimeMinValueMillisecondsSinceEpoch;
private static readonly int __staticMachine;
private static readonly short __staticPid;
private static int __staticIncrement; // high byte will be masked out when generating new ObjectId
private static readonly int StaticMachine;
private static readonly short StaticPid;
private static int _staticIncrement; // high byte will be masked out when generating new ObjectId

private static readonly uint[] _lookup32 = Enumerable.Range(0, 256).Select(i =>
private static readonly uint[] Lookup32 = Enumerable.Range(0, 256).Select(i =>
{
var s = i.ToString("x2");
return (uint) s[0] + ((uint) s[1] << 16);
@@ -44,40 +42,13 @@ namespace DotNetCore.CAP.Infrastructure
// static constructor
static ObjectId()
{
__unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
__dateTimeMaxValueMillisecondsSinceEpoch = (DateTime.MaxValue - __unixEpoch).Ticks / 10000;
__dateTimeMinValueMillisecondsSinceEpoch = (DateTime.MinValue - __unixEpoch).Ticks / 10000;
__staticMachine = GetMachineHash();
__staticIncrement = new Random().Next();
__staticPid = (short) GetCurrentProcessId();
UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
StaticMachine = GetMachineHash();
_staticIncrement = new Random().Next();
StaticPid = (short) GetCurrentProcessId();
}

// constructors
/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="bytes">The bytes.</param>
public ObjectId(byte[] bytes)
{
if (bytes == null)
{
throw new ArgumentNullException("bytes");
}

Unpack(bytes, out _timestamp, out _machine, out _pid, out _increment);
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="timestamp">The timestamp (expressed as a DateTime).</param>
/// <param name="machine">The machine hash.</param>
/// <param name="pid">The PID.</param>
/// <param name="increment">The increment.</param>
public ObjectId(DateTime timestamp, int machine, short pid, int increment)
: this(GetTimestampFromDateTime(timestamp), machine, pid, increment)
{
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
@@ -90,14 +61,14 @@ namespace DotNetCore.CAP.Infrastructure
{
if ((machine & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("machine",
"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
throw new ArgumentOutOfRangeException(nameof(machine),
@"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
}

if ((increment & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("increment",
"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
throw new ArgumentOutOfRangeException(nameof(increment),
@"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
}

_timestamp = timestamp;
@@ -105,76 +76,7 @@ namespace DotNetCore.CAP.Infrastructure
_pid = pid;
_increment = increment;
}

/// <summary>
/// Initializes a new instance of the ObjectId class.
/// </summary>
/// <param name="value">The value.</param>
public ObjectId(string value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}

Unpack(ParseHexString(value), out _timestamp, out _machine, out _pid, out _increment);
}

// public static properties
/// <summary>
/// Gets an instance of ObjectId where the value is empty.
/// </summary>
public static ObjectId Empty { get; } = default(ObjectId);

// public properties
/// <summary>
/// Gets the timestamp.
/// </summary>
public int Timestamp => _timestamp;

/// <summary>
/// Gets the machine.
/// </summary>
public int Machine => _machine;

/// <summary>
/// Gets the PID.
/// </summary>
public short Pid => _pid;

/// <summary>
/// Gets the increment.
/// </summary>
public int Increment => _increment;

/// <summary>
/// Gets the creation time (derived from the timestamp).
/// </summary>
public DateTime CreationTime => __unixEpoch.AddSeconds(_timestamp);

// public operators
/// <summary>
/// Compares two ObjectIds.
/// </summary>
/// <param name="lhs">The first ObjectId.</param>
/// <param name="rhs">The other ObjectId</param>
/// <returns>True if the first ObjectId is less than the second ObjectId.</returns>
public static bool operator <(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) < 0;
}

/// <summary>
/// Compares two ObjectIds.
/// </summary>
/// <param name="lhs">The first ObjectId.</param>
/// <param name="rhs">The other ObjectId</param>
/// <returns>True if the first ObjectId is less than or equal to the second ObjectId.</returns>
public static bool operator <=(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) <= 0;
}

/// <summary>
/// Compares two ObjectIds.
/// </summary>
@@ -196,29 +98,7 @@ namespace DotNetCore.CAP.Infrastructure
{
return !(lhs == rhs);
}

/// <summary>
/// Compares two ObjectIds.
/// </summary>
/// <param name="lhs">The first ObjectId.</param>
/// <param name="rhs">The other ObjectId</param>
/// <returns>True if the first ObjectId is greather than or equal to the second ObjectId.</returns>
public static bool operator >=(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) >= 0;
}

/// <summary>
/// Compares two ObjectIds.
/// </summary>
/// <param name="lhs">The first ObjectId.</param>
/// <param name="rhs">The other ObjectId</param>
/// <returns>True if the first ObjectId is greather than the second ObjectId.</returns>
public static bool operator >(ObjectId lhs, ObjectId rhs)
{
return lhs.CompareTo(rhs) > 0;
}

// public static methods
/// <summary>
/// Generates a new ObjectId with a unique value.
@@ -228,17 +108,7 @@ namespace DotNetCore.CAP.Infrastructure
{
return GenerateNewId(GetTimestampFromDateTime(DateTime.UtcNow));
}

/// <summary>
/// Generates a new ObjectId with a unique value (with the timestamp component based on a given DateTime).
/// </summary>
/// <param name="timestamp">The timestamp component (expressed as a DateTime).</param>
/// <returns>An ObjectId.</returns>
public static ObjectId GenerateNewId(DateTime timestamp)
{
return GenerateNewId(GetTimestampFromDateTime(timestamp));
}

/// <summary>
/// Generates a new ObjectId with a unique value (with the given timestamp).
/// </summary>
@@ -246,8 +116,8 @@ namespace DotNetCore.CAP.Infrastructure
/// <returns>An ObjectId.</returns>
public static ObjectId GenerateNewId(int timestamp)
{
var increment = Interlocked.Increment(ref __staticIncrement) & 0x00ffffff; // only use low order 3 bytes
return new ObjectId(timestamp, __staticMachine, __staticPid, increment);
var increment = Interlocked.Increment(ref _staticIncrement) & 0x00ffffff; // only use low order 3 bytes
return new ObjectId(timestamp, StaticMachine, StaticPid, increment);
}

/// <summary>
@@ -271,14 +141,14 @@ namespace DotNetCore.CAP.Infrastructure
{
if ((machine & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("machine",
"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
throw new ArgumentOutOfRangeException(nameof(machine),
@"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
}

if ((increment & 0xff000000) != 0)
{
throw new ArgumentOutOfRangeException("increment",
"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
throw new ArgumentOutOfRangeException(nameof(increment),
@"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
}

var bytes = new byte[12];
@@ -297,53 +167,6 @@ namespace DotNetCore.CAP.Infrastructure
return bytes;
}

/// <summary>
/// Parses a string and creates a new ObjectId.
/// </summary>
/// <param name="s">The string value.</param>
/// <returns>A ObjectId.</returns>
public static ObjectId Parse(string s)
{
if (s == null)
{
throw new ArgumentNullException("s");
}

if (s.Length != 24)
{
throw new ArgumentOutOfRangeException("s", "ObjectId string value must be 24 characters.");
}

return new ObjectId(ParseHexString(s));
}

/// <summary>
/// Unpacks a byte array into the components of an ObjectId.
/// </summary>
/// <param name="bytes">A byte array.</param>
/// <param name="timestamp">The timestamp.</param>
/// <param name="machine">The machine hash.</param>
/// <param name="pid">The PID.</param>
/// <param name="increment">The increment.</param>
public static void Unpack(byte[] bytes, out int timestamp, out int machine, out short pid, out int increment)
{
if (bytes == null)
{
throw new ArgumentNullException("bytes");
}

if (bytes.Length != 12)
{
throw new ArgumentOutOfRangeException("bytes", "Byte array must be 12 bytes long.");
}

timestamp = (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
machine = (bytes[4] << 16) + (bytes[5] << 8) + bytes[6];
pid = (short) ((bytes[7] << 8) + bytes[8]);
increment = (bytes[9] << 16) + (bytes[10] << 8) + bytes[11];
}

// private static methods
/// <summary>
/// Gets the current process id. This method exists because of how CAS operates on the call stack, checking
/// for permissions before executing the method. Hence, if we inlined this call, the calling method would not execute
@@ -365,7 +188,7 @@ namespace DotNetCore.CAP.Infrastructure

private static int GetTimestampFromDateTime(DateTime timestamp)
{
return (int) Math.Floor((ToUniversalTime(timestamp) - __unixEpoch).TotalSeconds);
return (int) Math.Floor((ToUniversalTime(timestamp) - UnixEpoch).TotalSeconds);
}

// public methods
@@ -421,9 +244,9 @@ namespace DotNetCore.CAP.Infrastructure
/// <returns>True if the other object is an ObjectId and equal to this one.</returns>
public override bool Equals(object obj)
{
if (obj is ObjectId)
if (obj is ObjectId id)
{
return Equals((ObjectId) obj);
return Equals(id);
}

return false;
@@ -461,33 +284,6 @@ namespace DotNetCore.CAP.Infrastructure
return ToHexString(ToByteArray());
}

/// <summary>
/// Parses a hex string into its equivalent byte array.
/// </summary>
/// <param name="s">The hex string to parse.</param>
/// <returns>The byte equivalent of the hex string.</returns>
public static byte[] ParseHexString(string s)
{
if (s == null)
{
throw new ArgumentNullException("s");
}

if (s.Length % 2 == 1)
{
throw new Exception("The binary key cannot have an odd number of digits");
}

var arr = new byte[s.Length >> 1];

for (var i = 0; i < s.Length >> 1; ++i)
{
arr[i] = (byte) ((GetHexVal(s[i << 1]) << 4) + GetHexVal(s[(i << 1) + 1]));
}

return arr;
}

/// <summary>
/// Converts a byte array to a hex string.
/// </summary>
@@ -497,13 +293,13 @@ namespace DotNetCore.CAP.Infrastructure
{
if (bytes == null)
{
throw new ArgumentNullException("bytes");
throw new ArgumentNullException(nameof(bytes));
}

var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++)
{
var val = _lookup32[bytes[i]];
var val = Lookup32[bytes[i]];
result[2 * i] = (char) val;
result[2 * i + 1] = (char) (val >> 16);
}
@@ -511,17 +307,6 @@ namespace DotNetCore.CAP.Infrastructure
return new string(result);
}

/// <summary>
/// Converts a DateTime to number of milliseconds since Unix epoch.
/// </summary>
/// <param name="dateTime">A DateTime.</param>
/// <returns>Number of seconds since Unix epoch.</returns>
public static long ToMillisecondsSinceEpoch(DateTime dateTime)
{
var utcDateTime = ToUniversalTime(dateTime);
return (utcDateTime - __unixEpoch).Ticks / 10000;
}

/// <summary>
/// Converts a DateTime to UTC (with special handling for MinValue and MaxValue).
/// </summary>
@@ -541,16 +326,5 @@ namespace DotNetCore.CAP.Infrastructure

return dateTime.ToUniversalTime();
}

private static int GetHexVal(char hex)
{
int val = hex;
//For uppercase A-F letters:
//return val - (val < 58 ? 48 : 55);
//For lowercase a-f letters:
//return val - (val < 58 ? 48 : 87);
//Or the two combined, but a bit slower:
return val - (val < 58 ? 48 : (val < 97 ? 55 : 87));
}
}
}

+ 97
- 0
src/DotNetCore.CAP/Infrastructure/SnowflakeId.cs Целия файл

@@ -0,0 +1,97 @@
// Copyright 2010-2012 Twitter, Inc.
// An object that generates IDs. This is broken into a separate class in case we ever want to support multiple worker threads per process

using System;

namespace DotNetCore.CAP.Infrastructure
{
public class SnowflakeId
{
public const long Twepoch = 1288834974657L;

private const int WorkerIdBits = 5;
private const int DatacenterIdBits = 5;
private const int SequenceBits = 12;
private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits);
private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits);

private const int WorkerIdShift = SequenceBits;
private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
private const long SequenceMask = -1L ^ (-1L << SequenceBits);

private static SnowflakeId _snowflakeId;

private readonly object _lock = new object();
private static readonly object s_lock = new object();
private long _lastTimestamp = -1L;

private SnowflakeId(long workerId, long datacenterId, long sequence = 0L)
{
WorkerId = workerId;
DatacenterId = datacenterId;
Sequence = sequence;

// sanity check for workerId
if (workerId > MaxWorkerId || workerId < 0)
throw new ArgumentException($"worker Id can't be greater than {MaxWorkerId} or less than 0");

if (datacenterId > MaxDatacenterId || datacenterId < 0)
throw new ArgumentException($"datacenter Id can't be greater than {MaxDatacenterId} or less than 0");
}

public long WorkerId { get; protected set; }
public long DatacenterId { get; protected set; }

public long Sequence { get; internal set; }

public static SnowflakeId Default(long datacenterId = 0)
{
lock (s_lock)
{
return _snowflakeId ?? (_snowflakeId = new SnowflakeId(AppDomain.CurrentDomain.Id, datacenterId));
}
}

public virtual long NextId()
{
lock (_lock)
{
var timestamp = TimeGen();

if (timestamp < _lastTimestamp)
throw new Exception(
$"InvalidSystemClock: Clock moved backwards, Refusing to generate id for {_lastTimestamp - timestamp} milliseconds");

if (_lastTimestamp == timestamp)
{
Sequence = (Sequence + 1) & SequenceMask;
if (Sequence == 0) timestamp = TilNextMillis(_lastTimestamp);
}
else
{
Sequence = 0;
}

_lastTimestamp = timestamp;
var id = ((timestamp - Twepoch) << TimestampLeftShift) |
(DatacenterId << DatacenterIdShift) |
(WorkerId << WorkerIdShift) | Sequence;

return id;
}
}

protected virtual long TilNextMillis(long lastTimestamp)
{
var timestamp = TimeGen();
while (timestamp <= lastTimestamp) timestamp = TimeGen();
return timestamp;
}

protected virtual long TimeGen()
{
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
}
}

+ 12
- 0
src/DotNetCore.CAP/Infrastructure/StatusName.cs Целия файл

@@ -11,5 +11,17 @@ namespace DotNetCore.CAP.Infrastructure
public const string Scheduled = nameof(Scheduled);
public const string Succeeded = nameof(Succeeded);
public const string Failed = nameof(Failed);

public static string Standardized(string input)
{
foreach (var item in typeof(StatusName).GetFields())
{
if (item.Name.ToLower() == input.ToLower())
{
return item.Name;
}
}
return string.Empty;
}
}
}

+ 1
- 0
src/DotNetCore.CAP/Internal/ICallbackMessageSender.Default.cs Целия файл

@@ -54,6 +54,7 @@ namespace DotNetCore.CAP.Internal

var publishedMessage = new CapPublishedMessage
{
Id = SnowflakeId.Default().NextId(),
Name = topicName,
Content = content,
StatusName = StatusName.Scheduled


src/DotNetCore.CAP/LoggerExtensions.cs → src/DotNetCore.CAP/Internal/LoggerExtensions.cs Целия файл

@@ -10,12 +10,12 @@ namespace DotNetCore.CAP
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal static class LoggerExtensions
{
public static void ConsumerExecutedAfterThreshold(this ILogger logger, int messageId, int retries)
public static void ConsumerExecutedAfterThreshold(this ILogger logger, long messageId, int retries)
{
logger.LogWarning($"The Subscriber of the message({messageId}) still fails after {retries}th executions and we will stop retrying.");
}

public static void SenderAfterThreshold(this ILogger logger, int messageId, int retries)
public static void SenderAfterThreshold(this ILogger logger, long messageId, int retries)
{
logger.LogWarning($"The Publisher of the message({messageId}) still fails after {retries}th sends and we will stop retrying.");
}
@@ -25,22 +25,22 @@ namespace DotNetCore.CAP
logger.LogWarning(ex, "FailedThresholdCallback action raised an exception:" + ex.Message);
}

public static void ConsumerExecutionRetrying(this ILogger logger, int messageId, int retries)
public static void ConsumerExecutionRetrying(this ILogger logger, long messageId, int retries)
{
logger.LogWarning($"The {retries}th retrying consume a message failed. message id: {messageId}");
}

public static void SenderRetrying(this ILogger logger, int messageId, int retries)
public static void SenderRetrying(this ILogger logger, long messageId, int retries)
{
logger.LogWarning($"The {retries}th retrying send a message failed. message id: {messageId} ");
}

public static void MessageHasBeenSent(this ILogger logger, double seconds)
public static void MessageHasBeenSent(this ILogger logger, string name, string content)
{
logger.LogDebug($"Message published. Took: {seconds} secs.");
logger.LogDebug($"Message published. name: {name}, content:{content}.");
}

public static void MessagePublishException(this ILogger logger, int messageId, string reason, Exception ex)
public static void MessagePublishException(this ILogger logger, long messageId, string reason, Exception ex)
{
logger.LogError(ex, $"An exception occured while publishing a message, reason:{reason}. message id:{messageId}");
}

+ 10
- 0
src/DotNetCore.CAP/Internal/NoopTransaction.cs Целия файл

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

namespace DotNetCore.CAP.Internal
{
public class NoopTransaction
{
}
}

+ 1
- 1
src/DotNetCore.CAP/Models/CapPublishedMessage.cs Целия файл

@@ -15,7 +15,7 @@ namespace DotNetCore.CAP.Models
Added = DateTime.Now;
}

public int Id { get; set; }
public long Id { get; set; }

public string Name { get; set; }



+ 0
- 15
src/DotNetCore.CAP/Models/CapQueue.cs Целия файл

@@ -1,15 +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.

namespace DotNetCore.CAP.Models
{
public class CapQueue
{
public int MessageId { get; set; }

/// <summary>
/// 0 is CapSentMessage, 1 is CapReceivedMessage
/// </summary>
public MessageType MessageType { get; set; }
}
}

+ 1
- 1
src/DotNetCore.CAP/Models/CapReceivedMessage.cs Целия файл

@@ -22,7 +22,7 @@ namespace DotNetCore.CAP.Models
Content = message.Content;
}

public int Id { get; set; }
public long Id { get; set; }

public string Group { get; set; }



+ 2
- 3
src/DotNetCore.CAP/Processor/IProcessor.NeedRetry.cs Целия файл

@@ -4,7 +4,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace DotNetCore.CAP.Processor
{
@@ -16,13 +15,13 @@ namespace DotNetCore.CAP.Processor
private readonly TimeSpan _waitingInterval;

public NeedRetryMessageProcessor(
IOptions<CapOptions> options,
CapOptions options,
ISubscriberExecutor subscriberExecutor,
IPublishMessageSender publishMessageSender)
{
_subscriberExecutor = subscriberExecutor;
_publishMessageSender = publishMessageSender;
_waitingInterval = TimeSpan.FromSeconds(options.Value.FailedRetryInterval);
_waitingInterval = TimeSpan.FromSeconds(options.FailedRetryInterval);
}

public async Task ProcessAsync(ProcessingContext context)


+ 7
- 0
test/DotNetCore.CAP.MongoDB.Test/ConnectionUtil.cs Целия файл

@@ -0,0 +1,7 @@
namespace DotNetCore.CAP.MongoDB.Test
{
public class ConnectionUtil
{
public static string ConnectionString = "mongodb://localhost:27017";
}
}

+ 55
- 0
test/DotNetCore.CAP.MongoDB.Test/DatabaseTestHost.cs Целия файл

@@ -0,0 +1,55 @@
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;

namespace DotNetCore.CAP.MongoDB.Test
{
public abstract class DatabaseTestHost : IDisposable
{
private string _connectionString;

protected IServiceProvider Provider { get; private set; }
protected IMongoClient MongoClient => Provider.GetService<IMongoClient>();
protected IMongoDatabase Database => MongoClient.GetDatabase(MongoDBOptions.DatabaseName);
protected CapOptions CapOptions => Provider.GetService<CapOptions>();
protected MongoDBOptions MongoDBOptions => Provider.GetService<MongoDBOptions>();

protected DatabaseTestHost()
{
CreateServiceCollection();
CreateDatabase();
}

private void CreateDatabase()
{
Provider.GetService<MongoDBStorage>().InitializeAsync(CancellationToken.None).GetAwaiter().GetResult();
}

protected virtual void AddService(ServiceCollection serviceCollection)
{

}

private void CreateServiceCollection()
{
var services = new ServiceCollection();
services.AddOptions();
services.AddLogging();
_connectionString = ConnectionUtil.ConnectionString;

services.AddSingleton(new MongoDBOptions() { DatabaseConnection = _connectionString });
services.AddSingleton(new CapOptions());
services.AddSingleton<IMongoClient>(x => new MongoClient(_connectionString));
services.AddSingleton<MongoDBStorage>();

AddService(services);
Provider = services.BuildServiceProvider();
}

public void Dispose()
{
MongoClient.DropDatabase(MongoDBOptions.DatabaseName);
}
}
}

+ 22
- 0
test/DotNetCore.CAP.MongoDB.Test/DotNetCore.CAP.MongoDB.Test.csproj Целия файл

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

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
<PackageReference Include="FluentAssertions" Version="5.4.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

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

</Project>

+ 71
- 0
test/DotNetCore.CAP.MongoDB.Test/MongoDBMonitoringApiTest.cs Целия файл

@@ -0,0 +1,71 @@
using System;
using System.Linq;
using DotNetCore.CAP.Dashboard.Monitoring;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
using FluentAssertions;
using Xunit;

namespace DotNetCore.CAP.MongoDB.Test
{
[Collection("MongoDB")]
public class MongoDBMonitoringApiTest : DatabaseTestHost
{
private readonly MongoDBMonitoringApi _api;

public MongoDBMonitoringApiTest()
{
_api = new MongoDBMonitoringApi(MongoClient, MongoDBOptions);

var collection = Database.GetCollection<CapPublishedMessage>(MongoDBOptions.PublishedCollection);
collection.InsertMany(new[]
{
new CapPublishedMessage
{
Id = SnowflakeId.Default().NextId(),
Added = DateTime.Now.AddHours(-1),
StatusName = "Failed",
Content = "abc"
},
new CapPublishedMessage
{
Id = SnowflakeId.Default().NextId(),
Added = DateTime.Now,
StatusName = "Failed",
Content = "bbc"
}
});
}

[Fact]
public void HourlyFailedJobs_Test()
{
var result = _api.HourlyFailedJobs(MessageType.Publish);
result.Should().HaveCount(24);
}

[Fact]
public void Messages_Test()
{
var messages =
_api.Messages(new MessageQueryDto
{
MessageType = MessageType.Publish,
StatusName = StatusName.Failed,
Content = "b",
CurrentPage = 1,
PageSize = 1
});

messages.Should().HaveCount(1);
messages.First().Content.Should().Contain("b");
}

[Fact]
public void PublishedFailedCount_Test()
{
var count = _api.PublishedFailedCount();
count.Should().BeGreaterThan(1);
}
}
}

+ 81
- 0
test/DotNetCore.CAP.MongoDB.Test/MongoDBStorageConnectionTest.cs Целия файл

@@ -0,0 +1,81 @@
using System;
using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Models;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Xunit;

namespace DotNetCore.CAP.MongoDB.Test
{
[Collection("MongoDB")]
public class MongoDBStorageConnectionTest : DatabaseTestHost
{
private IStorageConnection _connection =>
Provider.GetService<MongoDBStorage>().GetConnection();

[Fact]
public void StoreReceivedMessageAsync_TestAsync()
{
var messageContext = new MessageContext
{
Group = "test",
Name = "test",
Content = "test-content"
};

_connection.StoreReceivedMessage(new CapReceivedMessage(messageContext)
{
Id = SnowflakeId.Default().NextId()
});
}

[Fact]
public void ChangeReceivedState_Test()
{
StoreReceivedMessageAsync_TestAsync();
var collection = Database.GetCollection<CapReceivedMessage>(MongoDBOptions.ReceivedCollection);

var msg = collection.Find(x => true).FirstOrDefault();
_connection.ChangeReceivedState(msg.Id, StatusName.Scheduled).Should().BeTrue();
collection.Find(x => x.Id == msg.Id).FirstOrDefault()?.StatusName.Should().Be(StatusName.Scheduled);
}

[Fact]
public async void GetReceivedMessagesOfNeedRetry_TestAsync()
{
var msgs = await _connection.GetReceivedMessagesOfNeedRetry();

msgs.Should().BeEmpty();

var id = SnowflakeId.Default().NextId();

var msg = new CapReceivedMessage
{
Id = id,
Group = "test",
Name = "test",
Content = "test-content",
StatusName = StatusName.Failed
};
_connection.StoreReceivedMessage(msg);

var collection = Database.GetCollection<CapReceivedMessage>(MongoDBOptions.ReceivedCollection);

var updateDef = Builders<CapReceivedMessage>
.Update.Set(x => x.Added, DateTime.Now.AddMinutes(-5));

await collection.UpdateOneAsync(x => x.Id == id, updateDef);

msgs = await _connection.GetReceivedMessagesOfNeedRetry();
msgs.Should().HaveCountGreaterThan(0);
}

[Fact]
public void GetReceivedMessageAsync_Test()
{
var msg = _connection.GetReceivedMessageAsync(1);
msg.Should().NotBeNull();
}
}
}

+ 21
- 0
test/DotNetCore.CAP.MongoDB.Test/MongoDBStorageTest.cs Целия файл

@@ -0,0 +1,21 @@
using FluentAssertions;
using MongoDB.Driver;
using Xunit;

namespace DotNetCore.CAP.MongoDB.Test
{
[Collection("MongoDB")]
public class MongoDBStorageTest : DatabaseTestHost
{
[Fact]
public void InitializeAsync_Test()
{
var names = MongoClient.ListDatabaseNames()?.ToList();
names.Should().Contain(MongoDBOptions.DatabaseName);

var collections = Database.ListCollectionNames()?.ToList();
collections.Should().Contain(MongoDBOptions.PublishedCollection);
collections.Should().Contain(MongoDBOptions.ReceivedCollection);
}
}
}

+ 76
- 0
test/DotNetCore.CAP.MongoDB.Test/MongoDBTransactionTest.cs Целия файл

@@ -0,0 +1,76 @@
using System;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Driver;
using Xunit;

namespace DotNetCore.CAP.MongoDB.Test
{
[Collection("MongoDB")]
public class MongoDBTransactionTest : DatabaseTestHost
{
[Fact]
public void MongoDB_Connection_Test()
{
var names = MongoClient.ListDatabaseNames();
names.ToList().Should().NotBeNullOrEmpty();
}

[Fact(Skip = "Because of Appveyor dose not support MongoDB 4.0, so we skip this test for now.")]
public void Transaction_Test()
{
var document = new BsonDocument
{
{ "name", "MongoDB" },
{ "type", "Database" },
{ "count", 1 },
{ "info", new BsonDocument
{
{ "x", 203 },
{ "y", 102 }
}}
};
var db = MongoClient.GetDatabase("test");
var collection1 = db.GetCollection<BsonDocument>("test1");
var collection2 = db.GetCollection<BsonDocument>("test2");
using (var sesstion = MongoClient.StartSession())
{
sesstion.StartTransaction();
collection1.InsertOne(document);
collection2.InsertOne(document);
sesstion.CommitTransaction();
}
var filter = new BsonDocument("name", "MongoDB");
collection1.CountDocuments(filter).Should().BeGreaterThan(0);
collection2.CountDocuments(filter).Should().BeGreaterThan(0);
}

[Fact(Skip = "Because of Appveyor dose not support MongoDB 4.0, so we skip this test for now.")]
public void Transaction_Rollback_Test()
{
var document = new BsonDocument
{
{"name", "MongoDB"},
{"date", DateTimeOffset.Now.ToString()}
};
var db = MongoClient.GetDatabase("test");

var collection = db.GetCollection<BsonDocument>("test3");
var collection4 = db.GetCollection<BsonDocument>("test4");

using (var session = MongoClient.StartSession())
{
session.IsInTransaction.Should().BeFalse();
session.StartTransaction();
session.IsInTransaction.Should().BeTrue();
collection.InsertOne(session, document);
collection4.InsertOne(session, new BsonDocument { { "name", "MongoDB" } });

session.AbortTransaction();
}
var filter = new BsonDocument("name", "MongoDB");
collection.CountDocuments(filter).Should().Be(0);
collection4.CountDocuments(filter).Should().Be(0);
}
}
}

Някои файлове не бяха показани, защото твърде много файлове са промени

Зареждане…
Отказ
Запис