# 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.csmaster
@@ -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 |
@@ -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} | |||
@@ -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) |
@@ -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 => | |||
{ | |||
@@ -10,7 +10,7 @@ CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务 | |||
你可以在这里[CAP Wiki](https://github.com/dotnetcore/CAP/wiki)看到更多详细资料。 | |||
## 预览(OverView) | |||
## 预览(OverView) | |||
在我们构建 SOA 或者 微服务系统的过程中,我们通常需要使用事件来对各个服务进行集成,在这过程中简单的使用消息队列并不能保证数据的最终一致性, | |||
CAP 采用的是和当前数据库集成的本地消息表的方案来解决在分布式系统互相调用的各个环节可能出现的异常,它能够保证任何情况下事件消息都是不会丢失的。 | |||
@@ -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 | |||
@@ -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> | |||
@@ -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) | |||
{ | |||
@@ -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" /> | |||
@@ -21,8 +21,6 @@ namespace Sample.Kafka.MySql | |||
public void Configure(IApplicationBuilder app) | |||
{ | |||
app.UseMvc(); | |||
app.UseCap(); | |||
} | |||
} | |||
} |
@@ -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}"); | |||
} | |||
} | |||
} |
@@ -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>(); | |||
} | |||
} |
@@ -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> |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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": "" | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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}"); | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} | |||
} |
@@ -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" /> | |||
@@ -30,8 +30,6 @@ namespace Sample.RabbitMQ.MySql | |||
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
app.UseMvc(); | |||
app.UseCap(); | |||
} | |||
} | |||
} |
@@ -13,7 +13,7 @@ | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Confluent.Kafka" Version="0.11.4" /> | |||
<PackageReference Include="Confluent.Kafka" Version="0.11.5" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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"; | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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> |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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."); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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")] |
@@ -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; | |||
} | |||
@@ -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 | |||
} | |||
} |
@@ -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> | |||
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -36,7 +36,7 @@ namespace DotNetCore.CAP.MySql | |||
foreach (var table in tables) | |||
{ | |||
_logger.LogDebug($"Collecting expired data from table [{table}]."); | |||
int removedCount; | |||
do | |||
{ | |||
@@ -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; } | |||
} | |||
} |
@@ -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) |
@@ -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; | |||
} | |||
@@ -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() | |||
{ | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
@@ -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 | |||
} | |||
} |
@@ -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> | |||
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -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); | |||
@@ -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, |
@@ -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() | |||
{ | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -12,7 +12,7 @@ | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" /> | |||
<PackageReference Include="RabbitMQ.Client" Version="5.1.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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; } | |||
} | |||
} |
@@ -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) |
@@ -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, |
@@ -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() | |||
{ | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
}; | |||
} | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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; | |||
@@ -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; } | |||
@@ -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> | |||
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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) | |||
@@ -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) | |||
@@ -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); | |||
} | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -54,6 +54,7 @@ namespace DotNetCore.CAP.Internal | |||
var publishedMessage = new CapPublishedMessage | |||
{ | |||
Id = SnowflakeId.Default().NextId(), | |||
Name = topicName, | |||
Content = content, | |||
StatusName = StatusName.Scheduled | |||
@@ -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}"); | |||
} |
@@ -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 | |||
{ | |||
} | |||
} |
@@ -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; } | |||
@@ -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; } | |||
} | |||
} |
@@ -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; } | |||
@@ -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) | |||
@@ -0,0 +1,7 @@ | |||
namespace DotNetCore.CAP.MongoDB.Test | |||
{ | |||
public class ConnectionUtil | |||
{ | |||
public static string ConnectionString = "mongodb://localhost:27017"; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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> |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |