Commit 1e39e8ae authored by Savorboard's avatar Savorboard

Merge branch 'develop'

# Conflicts:
#	build/version.props
#	src/DotNetCore.CAP.PostgreSql/IStorageConnection.PostgreSql.cs
#	src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs
#	src/DotNetCore.CAP/Dashboard/DashboardRoutes.cs
#	src/DotNetCore.CAP/LoggerExtensions.cs
#	src/DotNetCore.CAP/Models/CapPublishedMessage.cs
#	src/DotNetCore.CAP/Processor/IProcessor.NeedRetry.cs
parents 325cba9b 1e250edd
...@@ -39,3 +39,5 @@ Properties ...@@ -39,3 +39,5 @@ Properties
/src/DotNetCore.CAP/packages.config /src/DotNetCore.CAP/packages.config
/src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj /src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj
/NuGet.config /NuGet.config
.vscode/*
samples/Sample.RabbitMQ.MongoDB/appsettings.Development.json
...@@ -58,7 +58,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql", ...@@ -58,7 +58,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql",
EndProject 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}" 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 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
...@@ -109,10 +115,22 @@ Global ...@@ -109,10 +115,22 @@ Global
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{7CA3625D-1817-4695-881D-7E79A1E1DED2}.Release|Any CPU.Build.0 = 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 {C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Debug|Any CPU.Build.0 = Debug|Any CPU {C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CB51105-A85B-42A4-AFDE-A4FC34D9EA91}.Release|Any CPU.ActiveCfg = Release|Any CPU {C143FCDF-E7F3-46F8-987E-A1BA38C1639D}.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}.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
...@@ -129,7 +147,10 @@ Global ...@@ -129,7 +147,10 @@ Global
{9F3F9BFE-7B6A-4A7A-A6E6-8B517D611873} = {3A6B6931-A123-477A-9469-8B468B5385AF} {9F3F9BFE-7B6A-4A7A-A6E6-8B517D611873} = {3A6B6931-A123-477A-9469-8B468B5385AF}
{82C403AB-ED68-4084-9A1D-11334F9F08F9} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {82C403AB-ED68-4084-9A1D-11334F9F08F9} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4}
{7CA3625D-1817-4695-881D-7E79A1E1DED2} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB}
......
# 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 ...@@ -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: 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) ![cap.png](http://oowr92l0m.bkt.clouddn.com/cap.png)
> The solid line in the figure represents the user code, and the dotted line represents the internal implementation of the CAP.
## Getting Started ## Getting Started
...@@ -30,27 +28,22 @@ You can run the following command to install the CAP in your project. ...@@ -30,27 +28,22 @@ You can run the following command to install the CAP in your project.
PM> Install-Package DotNetCore.CAP 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 PM> Install-Package DotNetCore.CAP.Kafka
```
If you want use RabbitMQ to send integrating event, installing by:
```
PM> Install-Package DotNetCore.CAP.RabbitMQ 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. // select a database provider you are using, event log table will integrate into.
PM> Install-Package DotNetCore.CAP.SqlServer PM> Install-Package DotNetCore.CAP.SqlServer
PM> Install-Package DotNetCore.CAP.MySql PM> Install-Package DotNetCore.CAP.MySql
PM> Install-Package DotNetCore.CAP.PostgreSql PM> Install-Package DotNetCore.CAP.PostgreSql
PM> Install-Package DotNetCore.CAP.MongoDB //need MongoDB 4.0+ cluster
``` ```
### Configuration ### Configuration
...@@ -62,19 +55,22 @@ public void ConfigureServices(IServiceCollection services) ...@@ -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 => services.AddCap(x =>
{ {
// If you are using EF, you need to add the following configuration: // If you are using EF, you need to add the configuration:
// Notice: You don't need to config x.UseSqlServer(""") again! CAP can autodiscovery. x.UseEntityFramework<AppDbContext>(); //Options, 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: // If you are using Ado.Net, you need to add the configuration:
x.UseSqlServer("Your ConnectionStrings"); x.UseSqlServer("Your ConnectionStrings");
x.UseMySql("Your ConnectionStrings"); x.UseMySql("Your ConnectionStrings");
x.UsePostgreSql("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: // If you are using RabbitMQ, you need to add the configuration:
x.UseRabbitMQ("localhost"); x.UseRabbitMQ("localhost");
...@@ -83,13 +79,6 @@ public void ConfigureServices(IServiceCollection services) ...@@ -83,13 +79,6 @@ public void ConfigureServices(IServiceCollection services)
}); });
} }
public void Configure(IApplicationBuilder app)
{
//.....
app.UseCap();
}
``` ```
### Publish ### Publish
...@@ -99,38 +88,39 @@ Inject `ICapPublisher` in your Controller, then use the `ICapPublisher` to send ...@@ -99,38 +88,39 @@ Inject `ICapPublisher` in your Controller, then use the `ICapPublisher` to send
```c# ```c#
public class PublishController : Controller public class PublishController : Controller
{ {
[Route("~/publishWithTransactionUsingEF")] private readonly ICapPublisher _capBus;
public async Task<IActionResult> PublishMessageWithTransactionUsingEF([FromServices]AppDbContext dbContext, [FromServices]ICapPublisher publisher)
{ 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. [Route("~/adonet/transaction")]
//Achieving atomicity between original database operation and the publish event log thanks to a local transaction. public IActionResult AdonetWithTransaction()
await publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }); {
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(); return Ok();
} }
[Route("~/publishWithTransactionUsingAdonet")] [Route("~/ef/transaction")]
public async Task<IActionResult> PublishMessageWithTransactionUsingAdonet([FromServices]ICapPublisher publisher) public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{
var connectionString = "";
using (var sqlConnection = new SqlConnection(connectionString))
{ {
sqlConnection.Open(); using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: true))
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(); return Ok();
} }
} }
...@@ -146,12 +136,10 @@ Add the Attribute `[CapSubscribe()]` on Action to subscribe message: ...@@ -146,12 +136,10 @@ Add the Attribute `[CapSubscribe()]` on Action to subscribe message:
```c# ```c#
public class PublishController : Controller public class PublishController : Controller
{ {
[CapSubscribe("xxx.services.account.check")] [CapSubscribe("xxx.services.show.time")]
public async Task CheckReceivedMessage(Person person) public void CheckReceivedMessage(DateTime datetime)
{ {
Console.WriteLine(person.Name); Console.WriteLine(datetime);
Console.WriteLine(person.Age);
return Task.CompletedTask;
} }
} }
...@@ -170,11 +158,10 @@ namespace xxx.Service ...@@ -170,11 +158,10 @@ namespace xxx.Service
public void CheckReceivedMessage(Person person); public void CheckReceivedMessage(Person person);
} }
public class SubscriberService: ISubscriberService, ICapSubscribe public class SubscriberService: ISubscriberService, ICapSubscribe
{ {
[CapSubscribe("xxx.services.account.check")] [CapSubscribe("xxx.services.show.time")]
public void CheckReceivedMessage(Person person) public void CheckReceivedMessage(DateTime datetime)
{ {
} }
} }
...@@ -196,7 +183,7 @@ public void ConfigureServices(IServiceCollection services) ...@@ -196,7 +183,7 @@ public void ConfigureServices(IServiceCollection services)
### Dashboard ### 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. 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.
......
...@@ -4,12 +4,13 @@ environment: ...@@ -4,12 +4,13 @@ environment:
BUILDING_ON_PLATFORM: win BUILDING_ON_PLATFORM: win
BuildEnvironment: appveyor BuildEnvironment: appveyor
Cap_SqlServer_ConnectionStringTemplate: Server=(local)\SQL2014;Database={0};User ID=sa;Password=Password12! 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! Cap_PostgreSql_ConnectionStringTemplate: Server=localhost;Database={0};UserId=postgres;Password=Password12!
services: services:
- mssql2014 - mssql2014
- mysql - mysql
- postgresql - postgresql
- mongodb
build_script: build_script:
- ps: ./build.ps1 - ps: ./build.ps1
test: off test: off
......
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<VersionMajor>2</VersionMajor> <VersionMajor>2</VersionMajor>
<VersionMinor>2</VersionMinor> <VersionMinor>3</VersionMinor>
<VersionPatch>5</VersionPatch> <VersionPatch>0</VersionPatch>
<VersionQuality></VersionQuality> <VersionQuality></VersionQuality>
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
</PropertyGroup> </PropertyGroup>
......
using System; using System;
using System.Data;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP; using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
...@@ -16,24 +18,37 @@ namespace Sample.Kafka.MySql.Controllers ...@@ -16,24 +18,37 @@ namespace Sample.Kafka.MySql.Controllers
_capBus = producer; _capBus = producer;
} }
[Route("~/publish")] [Route("~/without/transaction")]
public async Task<IActionResult> PublishMessage() public async Task<IActionResult> WithoutTransaction()
{ {
using (var connection = new MySqlConnection("Server=192.168.10.110;Database=testcap;UserId=root;Password=123123;")) await _capBus.PublishAsync("sample.rabbitmq.mysql", DateTime.Now);
{
connection.Open(); return Ok();
var transaction = connection.BeginTransaction(); }
//your business code here [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);
await _capBus.PublishAsync("xxx.xxx.test2", 123456, transaction); for (int i = 0; i < 5; i++)
{
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}
transaction.Commit(); transaction.Commit();
} }
}
return Ok("publish successful!"); return Ok();
} }
[CapSubscribe("#.test2")] [CapSubscribe("#.test2")]
public void Test2(int value) public void Test2(int value)
{ {
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="MySqlConnector" Version="0.40.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.Kafka\DotNetCore.CAP.Kafka.csproj" /> <ProjectReference Include="..\..\src\DotNetCore.CAP.Kafka\DotNetCore.CAP.Kafka.csproj" />
......
...@@ -21,8 +21,6 @@ namespace Sample.Kafka.MySql ...@@ -21,8 +21,6 @@ namespace Sample.Kafka.MySql
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseMvc(); app.UseMvc();
app.UseCap();
} }
} }
} }
\ No newline at end of file
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}");
}
}
}
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>();
}
}
<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>
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();
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MongoDB": "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0"
},
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "",
"Password": ""
}
}
\ No newline at end of file
...@@ -2,11 +2,22 @@ ...@@ -2,11 +2,22 @@
namespace Sample.RabbitMQ.MySql namespace Sample.RabbitMQ.MySql
{ {
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class AppDbContext : DbContext 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) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
optionsBuilder.UseMySql("Server=192.168.10.110;Database=testcap;UserId=root;Password=123123;"); optionsBuilder.UseMySql(ConnectionString);
} }
} }
} }
using System; using System;
using System.Data;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper;
using DotNetCore.CAP; using DotNetCore.CAP;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;
namespace Sample.RabbitMQ.MySql.Controllers namespace Sample.RabbitMQ.MySql.Controllers
{ {
[Route("api/[controller]")] [Route("api/[controller]")]
public class ValuesController : Controller public class ValuesController : Controller
{ {
private readonly AppDbContext _dbContext;
private readonly ICapPublisher _capBus; private readonly ICapPublisher _capBus;
public ValuesController(AppDbContext dbContext, ICapPublisher capPublisher) public ValuesController(ICapPublisher capPublisher)
{ {
_dbContext = dbContext;
_capBus = capPublisher; _capBus = capPublisher;
} }
[Route("~/publish")] [Route("~/without/transaction")]
public IActionResult PublishMessage() public async Task<IActionResult> WithoutTransaction()
{ {
_capBus.Publish("sample.rabbitmq.mysql", DateTime.Now); await _capBus.PublishAsync("sample.rabbitmq.mysql", DateTime.Now);
return Ok(); return Ok();
} }
[Route("~/publish2")] [Route("~/adonet/transaction")]
public IActionResult PublishMessage2() public IActionResult AdonetWithTransaction()
{
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.kafka.sqlserver4", DateTime.Now); _capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}
transaction.Commit();
}
}
return Ok(); return Ok();
} }
[Route("~/publishWithTrans")] [Route("~/ef/transaction")]
public async Task<IActionResult> PublishMessageWithTransaction() public IActionResult EntityFrameworkWithTransaction([FromServices]AppDbContext dbContext)
{ {
using (var trans = await _dbContext.Database.BeginTransactionAsync()) using (var trans = dbContext.Database.BeginTransaction(_capBus, autoCommit: false))
{
dbContext.Persons.Add(new Person() { Name = "ef.transaction" });
for (int i = 0; i < 5; i++)
{ {
await _capBus.PublishAsync("sample.kafka.sqlserver", ""); _capBus.Publish("sample.rabbitmq.mysql", DateTime.Now);
}
dbContext.SaveChanges();
trans.Commit(); trans.Commit();
} }
...@@ -47,9 +69,9 @@ namespace Sample.RabbitMQ.MySql.Controllers ...@@ -47,9 +69,9 @@ namespace Sample.RabbitMQ.MySql.Controllers
[NonAction] [NonAction]
[CapSubscribe("#.rabbitmq.mysql")] [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}");
} }
} }
} }
// <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
}
}
}
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");
}
}
}
// <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 @@ ...@@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" /> <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>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\DotNetCore.CAP.MySql\DotNetCore.CAP.MySql.csproj" /> <ProjectReference Include="..\..\src\DotNetCore.CAP.MySql\DotNetCore.CAP.MySql.csproj" />
......
...@@ -30,8 +30,6 @@ namespace Sample.RabbitMQ.MySql ...@@ -30,8 +30,6 @@ namespace Sample.RabbitMQ.MySql
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{ {
app.UseMvc(); app.UseMvc();
app.UseCap();
} }
} }
} }
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Confluent.Kafka" Version="0.11.4" /> <PackageReference Include="Confluent.Kafka" Version="0.11.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
......
// 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);
}
}
}
\ No newline at end of file
// 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";
}
}
\ No newline at end of file
// 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;
}
}
}
\ No newline at end of file
<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>
// 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);
}
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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();
}
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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;
}
}
}
\ No newline at end of file
// 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.");
}
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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")]
\ No newline at end of file
...@@ -24,9 +24,12 @@ namespace DotNetCore.CAP ...@@ -24,9 +24,12 @@ namespace DotNetCore.CAP
services.AddSingleton<CapDatabaseStorageMarkerService>(); services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, MySqlStorage>(); services.AddSingleton<IStorage, MySqlStorage>();
services.AddSingleton<IStorageConnection, MySqlStorageConnection>(); 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<ICollectProcessor, MySqlCollectProcessor>();
services.AddTransient<CapTransactionBase, MySqlCapTransaction>();
AddSingletionMySqlOptions(services); AddSingletionMySqlOptions(services);
} }
...@@ -44,7 +47,7 @@ namespace DotNetCore.CAP ...@@ -44,7 +47,7 @@ namespace DotNetCore.CAP
using (var scope = x.CreateScope()) using (var scope = x.CreateScope())
{ {
var provider = scope.ServiceProvider; var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(mysqlOptions.DbContextType); var dbContext = (DbContext) provider.GetService(mysqlOptions.DbContextType);
mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return mysqlOptions; return mysqlOptions;
} }
......
// 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
}
}
\ No newline at end of file
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<PackageReference Include="Dapper" Version="1.50.5" /> <PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" 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>
<ItemGroup> <ItemGroup>
......
// 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
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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; }
}
}
\ No newline at end of file
...@@ -126,7 +126,7 @@ select count(Id) from `{0}.received` where StatusName = N'Failed';", _prefix); ...@@ -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 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; return count;
} }
...@@ -169,7 +169,7 @@ select aggr.* from ( ...@@ -169,7 +169,7 @@ select aggr.* from (
var valuesMap = connection.Query<TimelineCounter>( var valuesMap = connection.Query<TimelineCounter>(
sqlQuery, sqlQuery,
new { keys = keyMaps.Keys, statusName }) new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => x.Key, x => x.Count); .ToDictionary(x => x.Key, x => x.Count);
foreach (var key in keyMaps.Keys) foreach (var key in keyMaps.Keys)
......
...@@ -58,10 +58,8 @@ namespace DotNetCore.CAP.MySql ...@@ -58,10 +58,8 @@ namespace DotNetCore.CAP.MySql
{ {
var batchSql = var batchSql =
$@" $@"
DROP TABLE IF EXISTS `{prefix}.queue`;
CREATE TABLE IF NOT EXISTS `{prefix}.received` ( CREATE TABLE IF NOT EXISTS `{prefix}.received` (
`Id` int(127) NOT NULL AUTO_INCREMENT, `Id` bigint NOT NULL,
`Name` varchar(400) NOT NULL, `Name` varchar(400) NOT NULL,
`Group` varchar(200) DEFAULT NULL, `Group` varchar(200) DEFAULT NULL,
`Content` longtext, `Content` longtext,
...@@ -73,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `{prefix}.received` ( ...@@ -73,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `{prefix}.received` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `{prefix}.published` ( CREATE TABLE IF NOT EXISTS `{prefix}.published` (
`Id` int(127) NOT NULL AUTO_INCREMENT, `Id` bigint NOT NULL,
`Name` varchar(200) NOT NULL, `Name` varchar(200) NOT NULL,
`Content` longtext, `Content` longtext,
`Retries` int(11) DEFAULT NULL, `Retries` int(11) DEFAULT NULL,
...@@ -81,7 +79,8 @@ CREATE TABLE IF NOT EXISTS `{prefix}.published` ( ...@@ -81,7 +79,8 @@ CREATE TABLE IF NOT EXISTS `{prefix}.published` (
`ExpiresAt` datetime DEFAULT NULL, `ExpiresAt` datetime DEFAULT NULL,
`StatusName` varchar(40) NOT NULL, `StatusName` varchar(40) NOT NULL,
PRIMARY KEY (`Id`) PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;"; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
";
return batchSql; return batchSql;
} }
......
...@@ -30,7 +30,7 @@ namespace DotNetCore.CAP.MySql ...@@ -30,7 +30,7 @@ namespace DotNetCore.CAP.MySql
return new MySqlStorageTransaction(this); 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};"; var sql = $@"SELECT * FROM `{_prefix}.published` WHERE `Id`={id};";
...@@ -52,7 +52,7 @@ namespace DotNetCore.CAP.MySql ...@@ -52,7 +52,7 @@ namespace DotNetCore.CAP.MySql
} }
} }
public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message) public void StoreReceivedMessage(CapReceivedMessage message)
{ {
if (message == null) if (message == null)
{ {
...@@ -60,16 +60,16 @@ namespace DotNetCore.CAP.MySql ...@@ -60,16 +60,16 @@ namespace DotNetCore.CAP.MySql
} }
var sql = $@" var sql = $@"
INSERT INTO `{_prefix}.received`(`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`) INSERT INTO `{_prefix}.received`(`Id`,`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST_INSERT_ID();"; VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
using (var connection = new MySqlConnection(Options.ConnectionString)) 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};"; var sql = $@"SELECT * FROM `{_prefix}.received` WHERE Id={id};";
using (var connection = new MySqlConnection(Options.ConnectionString)) using (var connection = new MySqlConnection(Options.ConnectionString))
...@@ -89,7 +89,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST ...@@ -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 = var sql =
$"UPDATE `{_prefix}.published` SET `Retries`=`Retries`+1,`ExpiresAt`=NULL,`StatusName` = '{state}' WHERE `Id`={messageId}"; $"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 ...@@ -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 = var sql =
$"UPDATE `{_prefix}.received` SET `Retries`=`Retries`+1,`ExpiresAt`=NULL,`StatusName` = '{state}' WHERE `Id`={messageId}"; $"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 ...@@ -110,9 +110,5 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT LAST
return connection.Execute(sql) > 0; return connection.Execute(sql) > 0;
} }
} }
public void Dispose()
{
}
} }
} }
\ No newline at end of file
...@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.MySql ...@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.MySql
{ {
private readonly IDbConnection _dbConnection; private readonly IDbConnection _dbConnection;
//private readonly IDbTransaction _dbTransaction;
private readonly string _prefix; private readonly string _prefix;
public MySqlStorageTransaction(MySqlStorageConnection connection) public MySqlStorageTransaction(MySqlStorageConnection connection)
...@@ -23,8 +22,6 @@ namespace DotNetCore.CAP.MySql ...@@ -23,8 +22,6 @@ namespace DotNetCore.CAP.MySql
_prefix = options.TableNamePrefix; _prefix = options.TableNamePrefix;
_dbConnection = new MySqlConnection(options.ConnectionString); _dbConnection = new MySqlConnection(options.ConnectionString);
// _dbConnection.Open(); for performance
// _dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted);
} }
public void UpdateMessage(CapPublishedMessage message) public void UpdateMessage(CapPublishedMessage message)
...@@ -55,13 +52,11 @@ namespace DotNetCore.CAP.MySql ...@@ -55,13 +52,11 @@ namespace DotNetCore.CAP.MySql
{ {
_dbConnection.Close(); _dbConnection.Close();
_dbConnection.Dispose(); _dbConnection.Dispose();
//_dbTransaction.Commit();
return Task.CompletedTask; return Task.CompletedTask;
} }
public void Dispose() public void Dispose()
{ {
//_dbTransaction.Dispose();
_dbConnection.Dispose(); _dbConnection.Dispose();
} }
} }
......
...@@ -24,9 +24,12 @@ namespace DotNetCore.CAP ...@@ -24,9 +24,12 @@ namespace DotNetCore.CAP
services.AddSingleton<CapDatabaseStorageMarkerService>(); services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<IStorage, PostgreSqlStorage>(); services.AddSingleton<IStorage, PostgreSqlStorage>();
services.AddSingleton<IStorageConnection, PostgreSqlStorageConnection>(); 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<ICollectProcessor, PostgreSqlCollectProcessor>();
services.AddTransient<CapTransactionBase, PostgreSqlCapTransaction>();
AddSingletonPostgreSqlOptions(services); AddSingletonPostgreSqlOptions(services);
} }
......
// 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
}
}
\ No newline at end of file
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<PackageReference Include="Dapper" Version="1.50.5" /> <PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" 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>
<ItemGroup> <ItemGroup>
......
// 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
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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; }
}
}
\ No newline at end of file
...@@ -128,7 +128,7 @@ select count(""Id"") from ""{0}"".""received"" where ""StatusName"" = N'Failed' ...@@ -128,7 +128,7 @@ select count(""Id"") from ""{0}"".""received"" where ""StatusName"" = N'Failed'
var sqlQuery = var sqlQuery =
$"select count(\"Id\") from \"{_options.Schema}\".\"{tableName}\" where Lower(\"StatusName\") = Lower(@state)"; $"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; return count;
} }
...@@ -170,7 +170,7 @@ with aggr as ( ...@@ -170,7 +170,7 @@ with aggr as (
) )
select ""Key"",""Count"" from aggr where ""Key""= Any(@keys);"; 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() .ToList()
.ToDictionary(x => x.Key, x => x.Count); .ToDictionary(x => x.Key, x => x.Count);
......
...@@ -100,10 +100,8 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -100,10 +100,8 @@ namespace DotNetCore.CAP.PostgreSql
var batchSql = $@" var batchSql = $@"
CREATE SCHEMA IF NOT EXISTS ""{schema}""; CREATE SCHEMA IF NOT EXISTS ""{schema}"";
DROP TABLE IF EXISTS ""{schema}"".""queue"";
CREATE TABLE IF NOT EXISTS ""{schema}"".""received""( CREATE TABLE IF NOT EXISTS ""{schema}"".""received""(
""Id"" SERIAL PRIMARY KEY NOT NULL, ""Id"" BIGINT PRIMARY KEY NOT NULL,
""Name"" VARCHAR(200) NOT NULL, ""Name"" VARCHAR(200) NOT NULL,
""Group"" VARCHAR(200) NULL, ""Group"" VARCHAR(200) NULL,
""Content"" TEXT NULL, ""Content"" TEXT NULL,
...@@ -114,7 +112,7 @@ CREATE TABLE IF NOT EXISTS ""{schema}"".""received""( ...@@ -114,7 +112,7 @@ CREATE TABLE IF NOT EXISTS ""{schema}"".""received""(
); );
CREATE TABLE IF NOT EXISTS ""{schema}"".""published""( CREATE TABLE IF NOT EXISTS ""{schema}"".""published""(
""Id"" SERIAL PRIMARY KEY NOT NULL, ""Id"" BIGINT PRIMARY KEY NOT NULL,
""Name"" VARCHAR(200) NOT NULL, ""Name"" VARCHAR(200) NOT NULL,
""Content"" TEXT NULL, ""Content"" TEXT NULL,
""Retries"" INT NOT NULL, ""Retries"" INT NOT NULL,
......
...@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.PostgreSql
return new PostgreSqlStorageTransaction(this); 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"; var sql = $"SELECT * FROM \"{Options.Schema}\".\"published\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";
...@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.PostgreSql
} }
} }
public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message) public void StoreReceivedMessage(CapReceivedMessage message)
{ {
if (message == null) if (message == null)
{ {
...@@ -58,15 +58,15 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -58,15 +58,15 @@ namespace DotNetCore.CAP.PostgreSql
} }
var sql = 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)) 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"; var sql = $"SELECT * FROM \"{Options.Schema}\".\"received\" WHERE \"Id\"={id} FOR UPDATE SKIP LOCKED";
using (var connection = new NpgsqlConnection(Options.ConnectionString)) using (var connection = new NpgsqlConnection(Options.ConnectionString))
...@@ -86,11 +86,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -86,11 +86,7 @@ namespace DotNetCore.CAP.PostgreSql
} }
} }
public void Dispose() public bool ChangePublishedState(long messageId, string state)
{
}
public bool ChangePublishedState(int messageId, string state)
{ {
var sql = var sql =
$"UPDATE \"{Options.Schema}\".\"published\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}"; $"UPDATE \"{Options.Schema}\".\"published\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
...@@ -101,7 +97,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -101,7 +97,7 @@ namespace DotNetCore.CAP.PostgreSql
} }
} }
public bool ChangeReceivedState(int messageId, string state) public bool ChangeReceivedState(long messageId, string state)
{ {
var sql = var sql =
$"UPDATE \"{Options.Schema}\".\"received\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}"; $"UPDATE \"{Options.Schema}\".\"received\" SET \"Retries\"=\"Retries\"+1,\"ExpiresAt\"=NULL,\"StatusName\" = '{state}' WHERE \"Id\"={messageId}";
...@@ -111,5 +107,9 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -111,5 +107,9 @@ namespace DotNetCore.CAP.PostgreSql
return connection.Execute(sql) > 0; return connection.Execute(sql) > 0;
} }
} }
public void Dispose()
{
}
} }
} }
\ No newline at end of file
...@@ -35,9 +35,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -35,9 +35,7 @@ namespace DotNetCore.CAP.PostgreSql
} }
var sql = var sql =
$@"UPDATE ""{ $@"UPDATE ""{_schema}"".""published"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_schema
}"".""published"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction); _dbConnection.Execute(sql, message, _dbTransaction);
} }
...@@ -49,9 +47,7 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -49,9 +47,7 @@ namespace DotNetCore.CAP.PostgreSql
} }
var sql = var sql =
$@"UPDATE ""{ $@"UPDATE ""{_schema}"".""received"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_schema
}"".""received"" SET ""Retries""=@Retries,""Content""= @Content,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;";
_dbConnection.Execute(sql, message, _dbTransaction); _dbConnection.Execute(sql, message, _dbTransaction);
} }
...@@ -66,29 +62,5 @@ namespace DotNetCore.CAP.PostgreSql ...@@ -66,29 +62,5 @@ namespace DotNetCore.CAP.PostgreSql
_dbTransaction.Dispose(); _dbTransaction.Dispose();
_dbConnection.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);
}
} }
} }
\ No newline at end of file
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="5.0.1" /> <PackageReference Include="RabbitMQ.Client" Version="5.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
......
...@@ -20,5 +20,13 @@ namespace DotNetCore.CAP ...@@ -20,5 +20,13 @@ namespace DotNetCore.CAP
/// EF dbcontext type. /// EF dbcontext type.
/// </summary> /// </summary>
internal Type DbContextType { get; set; } internal Type DbContextType { get; set; }
internal bool IsSqlServer2008 { get; set; }
public EFOptions UseSqlServer2008()
{
IsSqlServer2008 = true;
return this;
}
} }
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
using System; using System;
using DotNetCore.CAP.Processor; using DotNetCore.CAP.Processor;
using DotNetCore.CAP.SqlServer; using DotNetCore.CAP.SqlServer;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
...@@ -22,11 +23,15 @@ namespace DotNetCore.CAP ...@@ -22,11 +23,15 @@ namespace DotNetCore.CAP
public void AddServices(IServiceCollection services) public void AddServices(IServiceCollection services)
{ {
services.AddSingleton<CapDatabaseStorageMarkerService>(); services.AddSingleton<CapDatabaseStorageMarkerService>();
services.AddSingleton<DiagnosticProcessorObserver>();
services.AddSingleton<IStorage, SqlServerStorage>(); services.AddSingleton<IStorage, SqlServerStorage>();
services.AddSingleton<IStorageConnection, SqlServerStorageConnection>(); 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<ICollectProcessor, SqlServerCollectProcessor>();
services.AddTransient<CapTransactionBase, SqlServerCapTransaction>();
AddSqlServerOptions(services); AddSqlServerOptions(services);
} }
...@@ -44,7 +49,7 @@ namespace DotNetCore.CAP ...@@ -44,7 +49,7 @@ namespace DotNetCore.CAP
using (var scope = x.CreateScope()) using (var scope = x.CreateScope())
{ {
var provider = scope.ServiceProvider; var provider = scope.ServiceProvider;
var dbContext = (DbContext)provider.GetService(sqlServerOptions.DbContextType); var dbContext = (DbContext) provider.GetService(sqlServerOptions.DbContextType);
sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString;
return sqlServerOptions; return sqlServerOptions;
} }
......
// 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
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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));
}
}
}
}
\ No newline at end of file
// 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
}
}
\ No newline at end of file
// 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);
}
}
}
\ No newline at end of file
// 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; }
}
}
\ No newline at end of file
...@@ -89,10 +89,17 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed'; ...@@ -89,10 +89,17 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
where += " and content like '%@Content%'"; 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 = var sqlQuery =
$"select * from [{_options.Schema}].{tableName} where 1=1 {where} order by Added desc offset @Offset rows fetch next @Limit rows only"; $"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.StatusName,
queryDto.Group, queryDto.Group,
...@@ -128,7 +135,7 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed'; ...@@ -128,7 +135,7 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
var sqlQuery = var sqlQuery =
$"select count(Id) from [{_options.Schema}].{tableName} with (nolock) where StatusName = @state"; $"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; return count;
} }
...@@ -159,9 +166,18 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed'; ...@@ -159,9 +166,18 @@ select count(Id) from [{0}].Received with (nolock) where StatusName = N'Failed';
string statusName, string statusName,
IDictionary<string, DateTime> keyMaps) IDictionary<string, DateTime> keyMaps)
{ {
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+ //SQL Server 2012+
var sqlQuery = var sqlQuery = $@"
$@"
with aggr as ( with aggr as (
select FORMAT(Added,'yyyy-MM-dd-HH') as [Key], select FORMAT(Added,'yyyy-MM-dd-HH') as [Key],
count(id) [Count] count(id) [Count]
...@@ -172,8 +188,8 @@ with aggr as ( ...@@ -172,8 +188,8 @@ with aggr as (
select [Key], [Count] from aggr with (nolock) where [Key] in @keys;"; select [Key], [Count] from aggr with (nolock) where [Key] in @keys;";
var valuesMap = connection.Query<TimelineCounter>( var valuesMap = connection.Query<TimelineCounter>(
sqlQuery, _options.IsSqlServer2008 ? sqlQuery2008 : sqlQuery,
new { keys = keyMaps.Keys, statusName }) new {keys = keyMaps.Keys, statusName})
.ToDictionary(x => x.Key, x => x.Count); .ToDictionary(x => x.Key, x => x.Count);
foreach (var key in keyMaps.Keys) foreach (var key in keyMaps.Keys)
......
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
using System; using System;
using System.Data; using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using DotNetCore.CAP.Dashboard; using DotNetCore.CAP.Dashboard;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace DotNetCore.CAP.SqlServer namespace DotNetCore.CAP.SqlServer
...@@ -15,15 +17,18 @@ namespace DotNetCore.CAP.SqlServer ...@@ -15,15 +17,18 @@ namespace DotNetCore.CAP.SqlServer
public class SqlServerStorage : IStorage public class SqlServerStorage : IStorage
{ {
private readonly CapOptions _capOptions; private readonly CapOptions _capOptions;
private readonly DiagnosticProcessorObserver _diagnosticProcessorObserver;
private readonly IDbConnection _existingConnection = null; private readonly IDbConnection _existingConnection = null;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly SqlServerOptions _options; private readonly SqlServerOptions _options;
public SqlServerStorage(ILogger<SqlServerStorage> logger, public SqlServerStorage(ILogger<SqlServerStorage> logger,
CapOptions capOptions, CapOptions capOptions,
SqlServerOptions options) SqlServerOptions options,
DiagnosticProcessorObserver diagnosticProcessorObserver)
{ {
_options = options; _options = options;
_diagnosticProcessorObserver = diagnosticProcessorObserver;
_logger = logger; _logger = logger;
_capOptions = capOptions; _capOptions = capOptions;
} }
...@@ -38,7 +43,7 @@ namespace DotNetCore.CAP.SqlServer ...@@ -38,7 +43,7 @@ namespace DotNetCore.CAP.SqlServer
return new SqlServerMonitoringApi(this, _options); return new SqlServerMonitoringApi(this, _options);
} }
public async Task InitializeAsync(CancellationToken cancellationToken) public async Task InitializeAsync(CancellationToken cancellationToken = default(CancellationToken))
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{ {
...@@ -53,6 +58,8 @@ namespace DotNetCore.CAP.SqlServer ...@@ -53,6 +58,8 @@ namespace DotNetCore.CAP.SqlServer
} }
_logger.LogDebug("Ensuring all create database tables script are applied."); _logger.LogDebug("Ensuring all create database tables script are applied.");
DiagnosticListener.AllListeners.Subscribe(_diagnosticProcessorObserver);
} }
protected virtual string CreateDbTablesScript(string schema) protected virtual string CreateDbTablesScript(string schema)
...@@ -64,15 +71,10 @@ BEGIN ...@@ -64,15 +71,10 @@ BEGIN
EXEC('CREATE SCHEMA [{schema}]') EXEC('CREATE SCHEMA [{schema}]')
END; 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 IF OBJECT_ID(N'[{schema}].[Received]',N'U') IS NULL
BEGIN BEGIN
CREATE TABLE [{schema}].[Received]( CREATE TABLE [{schema}].[Received](
[Id] [int] IDENTITY(1,1) NOT NULL, [Id] [bigint] NOT NULL,
[Name] [nvarchar](200) NOT NULL, [Name] [nvarchar](200) NOT NULL,
[Group] [nvarchar](200) NULL, [Group] [nvarchar](200) NULL,
[Content] [nvarchar](max) NULL, [Content] [nvarchar](max) NULL,
...@@ -90,7 +92,7 @@ END; ...@@ -90,7 +92,7 @@ END;
IF OBJECT_ID(N'[{schema}].[Published]',N'U') IS NULL IF OBJECT_ID(N'[{schema}].[Published]',N'U') IS NULL
BEGIN BEGIN
CREATE TABLE [{schema}].[Published]( CREATE TABLE [{schema}].[Published](
[Id] [int] IDENTITY(1,1) NOT NULL, [Id] [bigint] NOT NULL,
[Name] [nvarchar](200) NOT NULL, [Name] [nvarchar](200) NOT NULL,
[Content] [nvarchar](max) NULL, [Content] [nvarchar](max) NULL,
[Retries] [int] NOT NULL, [Retries] [int] NOT NULL,
......
...@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.SqlServer ...@@ -28,7 +28,7 @@ namespace DotNetCore.CAP.SqlServer
return new SqlServerStorageTransaction(this); 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}"; var sql = $@"SELECT * FROM [{Options.Schema}].[Published] WITH (readpast) WHERE Id={id}";
...@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.SqlServer ...@@ -50,7 +50,7 @@ namespace DotNetCore.CAP.SqlServer
} }
} }
public async Task<int> StoreReceivedMessageAsync(CapReceivedMessage message) public void StoreReceivedMessage(CapReceivedMessage message)
{ {
if (message == null) if (message == null)
{ {
...@@ -58,16 +58,16 @@ namespace DotNetCore.CAP.SqlServer ...@@ -58,16 +58,16 @@ namespace DotNetCore.CAP.SqlServer
} }
var sql = $@" var sql = $@"
INSERT INTO [{Options.Schema}].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) INSERT INTO [{Options.Schema}].[Received]([Id],[Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName])
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOPE_IDENTITY();"; VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
using (var connection = new SqlConnection(Options.ConnectionString)) 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}"; var sql = $@"SELECT * FROM [{Options.Schema}].[Received] WITH (readpast) WHERE Id={id}";
using (var connection = new SqlConnection(Options.ConnectionString)) using (var connection = new SqlConnection(Options.ConnectionString))
...@@ -87,7 +87,7 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOP ...@@ -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 = var sql =
$"UPDATE [{Options.Schema}].[Published] SET Retries=Retries+1,ExpiresAt=NULL,StatusName = '{state}' WHERE Id={messageId}"; $"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 ...@@ -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 = var sql =
$"UPDATE [{Options.Schema}].[Received] SET Retries=Retries+1,ExpiresAt=NULL,StatusName = '{state}' WHERE Id={messageId}"; $"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 ...@@ -108,9 +108,5 @@ VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT SCOP
return connection.Execute(sql) > 0; return connection.Execute(sql) > 0;
} }
} }
public void Dispose()
{
}
} }
} }
\ No newline at end of file
...@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.SqlServer ...@@ -14,7 +14,6 @@ namespace DotNetCore.CAP.SqlServer
{ {
private readonly IDbConnection _dbConnection; private readonly IDbConnection _dbConnection;
private readonly IDbTransaction _dbTransaction;
private readonly string _schema; private readonly string _schema;
public SqlServerStorageTransaction(SqlServerStorageConnection connection) public SqlServerStorageTransaction(SqlServerStorageConnection connection)
...@@ -24,7 +23,6 @@ namespace DotNetCore.CAP.SqlServer ...@@ -24,7 +23,6 @@ namespace DotNetCore.CAP.SqlServer
_dbConnection = new SqlConnection(options.ConnectionString); _dbConnection = new SqlConnection(options.ConnectionString);
_dbConnection.Open(); _dbConnection.Open();
_dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted);
} }
public void UpdateMessage(CapPublishedMessage message) public void UpdateMessage(CapPublishedMessage message)
...@@ -36,7 +34,7 @@ namespace DotNetCore.CAP.SqlServer ...@@ -36,7 +34,7 @@ namespace DotNetCore.CAP.SqlServer
var sql = 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); _dbConnection.Execute(sql, message);
} }
public void UpdateMessage(CapReceivedMessage message) public void UpdateMessage(CapReceivedMessage message)
...@@ -48,43 +46,17 @@ namespace DotNetCore.CAP.SqlServer ...@@ -48,43 +46,17 @@ namespace DotNetCore.CAP.SqlServer
var sql = 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); _dbConnection.Execute(sql, message);
} }
public Task CommitAsync() public Task CommitAsync()
{ {
_dbTransaction.Commit();
return Task.CompletedTask; return Task.CompletedTask;
} }
public void Dispose() public void Dispose()
{ {
_dbTransaction.Dispose();
_dbConnection.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);
}
} }
} }
\ No newline at end of file
...@@ -2,252 +2,130 @@ ...@@ -2,252 +2,130 @@
// Licensed under the MIT License. See License.txt in the project root for license information. // Licensed under the MIT License. See License.txt in the project root for license information.
using System; using System;
using System.Data;
using System.Diagnostics; using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Diagnostics; using DotNetCore.CAP.Diagnostics;
using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Infrastructure;
using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Models; using DotNetCore.CAP.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP.Abstractions namespace DotNetCore.CAP.Abstractions
{ {
public abstract class CapPublisherBase : ICapPublisher, IDisposable public abstract class CapPublisherBase : ICapPublisher
{ {
private readonly IDispatcher _dispatcher; private readonly CapTransactionBase _transaction;
private readonly ILogger _logger; private readonly IMessagePacker _msgPacker;
private readonly IContentSerializer _serializer;
protected bool NotUseTransaction;
// diagnostics listener
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private static readonly DiagnosticListener s_diagnosticListener = protected static readonly DiagnosticListener s_diagnosticListener =
new DiagnosticListener(CapDiagnosticListenerExtensions.DiagnosticListenerName); new DiagnosticListener(CapDiagnosticListenerExtensions.DiagnosticListenerName);
protected CapPublisherBase(ILogger<CapPublisherBase> logger, IDispatcher dispatcher) protected CapPublisherBase(IServiceProvider service)
{
_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)
{ {
CheckIsUsingEF(name); ServiceProvider = service;
PrepareConnectionForEF(); _transaction = service.GetRequiredService<CapTransactionBase>();
_msgPacker = service.GetRequiredService<IMessagePacker>();
return PublishWithTransAsync(name, contentObj, callbackName); _serializer = service.GetRequiredService<IContentSerializer>();
} }
public void Publish<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null) protected IServiceProvider ServiceProvider { get; }
{
CheckIsAdoNet(name);
PrepareConnectionForAdo(dbTransaction);
PublishWithTrans(name, contentObj, callbackName); public ICapTransaction Transaction => _transaction;
}
public Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null) public void Publish<T>(string name, T contentObj, 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)
{
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 CapPublishedMessage
}
var message = new CapMessageDto(content)
{ {
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) PublishAsyncInternal(message).GetAwaiter().GetResult();
{
DbTransaction = dbTransaction ?? throw new ArgumentNullException(nameof(dbTransaction));
DbConnection = DbTransaction.Connection;
if (DbConnection.State != ConnectionState.Open)
{
IsCapOpenedConn = true;
DbConnection.Open();
}
} }
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) await PublishAsyncInternal(message);
{
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.");
}
} }
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); Guid operationId = default(Guid);
var content = Serialize(contentObj, callbackName);
var message = new CapPublishedMessage
{
Name = name,
Content = content,
StatusName = StatusName.Scheduled
};
try try
{ {
operationId = s_diagnosticListener.WritePublishMessageStoreBefore(message); 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); s_diagnosticListener.WritePublishMessageStoreAfter(operationId, message);
message.Id = id; if (NotUseTransaction || Transaction.AutoCommit)
{
Enqueue(message); _transaction.Commit();
} }
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "An exception was occurred when publish message async. exception message:" + name);
s_diagnosticListener.WritePublishMessageStoreError(operationId, message, e); s_diagnosticListener.WritePublishMessageStoreError(operationId, message, e);
Console.WriteLine(e);
throw; throw;
} }
} finally
private void PublishWithTrans<T>(string name, T contentObj, string callbackName = null)
{ {
Guid operationId = default(Guid); if (NotUseTransaction || Transaction.AutoCommit)
var content = Serialize(contentObj, callbackName);
var message = new CapPublishedMessage
{ {
Name = name, _transaction.Dispose();
Content = content,
StatusName = StatusName.Scheduled
};
try
{
Console.WriteLine("================22222222222222=====================");
operationId = s_diagnosticListener.WritePublishMessageStoreBefore(message);
var id = Execute(DbConnection, DbTransaction, message);
Console.WriteLine("================777777777777777777777=====================");
ClosedCap();
if (id > 0)
{
_logger.LogInformation($"message [{message}] has been persisted in the database.");
s_diagnosticListener.WritePublishMessageStoreAfter(operationId, message);
message.Id = id;
Enqueue(message);
}
} }
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(); content = Helper.IsComplexType(obj.GetType())
DbTransaction.Dispose(); ? _serializer.Serialize(obj)
: obj.ToString();
} }
else
if (IsCapOpenedConn)
{ {
DbConnection.Dispose(); content = string.Empty;
}
} }
var message = new CapMessageDto(content)
public void Dispose()
{ {
DbTransaction?.Dispose(); CallbackName = callbackName
DbConnection?.Dispose(); };
return _msgPacker.Pack(message);
} }
#endregion private methods
} }
} }
\ No newline at end of file
// 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);
}
}
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
using System; using System;
using DotNetCore.CAP; using DotNetCore.CAP;
using DotNetCore.CAP.Dashboard.GatewayProxy; using DotNetCore.CAP.Dashboard.GatewayProxy;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
// ReSharper disable once CheckNamespace // ReSharper disable once CheckNamespace
...@@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Builder ...@@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Builder
/// <summary> /// <summary>
/// app extensions for <see cref="IApplicationBuilder" /> /// app extensions for <see cref="IApplicationBuilder" />
/// </summary> /// </summary>
public static class AppBuilderExtensions internal static class AppBuilderExtensions
{ {
/// <summary> /// <summary>
/// Enables cap for the current application /// Enables cap for the current application
...@@ -70,4 +71,17 @@ namespace Microsoft.AspNetCore.Builder ...@@ -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);
};
}
}
} }
\ No newline at end of file
...@@ -8,6 +8,8 @@ using DotNetCore.CAP.Abstractions; ...@@ -8,6 +8,8 @@ using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Internal; using DotNetCore.CAP.Internal;
using DotNetCore.CAP.Processor; using DotNetCore.CAP.Processor;
using DotNetCore.CAP.Processor.States; using DotNetCore.CAP.Processor.States;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
// ReSharper disable once CheckNamespace // ReSharper disable once CheckNamespace
...@@ -24,9 +26,7 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -24,9 +26,7 @@ namespace Microsoft.Extensions.DependencyInjection
/// <param name="services">The services available in the application.</param> /// <param name="services">The services available in the application.</param>
/// <param name="setupAction">An action to configure the <see cref="CapOptions" />.</param> /// <param name="setupAction">An action to configure the <see cref="CapOptions" />.</param>
/// <returns>An <see cref="CapBuilder" /> for application services.</returns> /// <returns>An <see cref="CapBuilder" /> for application services.</returns>
public static CapBuilder AddCap( public static CapBuilder AddCap(this IServiceCollection services, Action<CapOptions> setupAction)
this IServiceCollection services,
Action<CapOptions> setupAction)
{ {
if (setupAction == null) if (setupAction == null)
{ {
...@@ -34,8 +34,8 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -34,8 +34,8 @@ namespace Microsoft.Extensions.DependencyInjection
} }
services.TryAddSingleton<CapMarkerService>(); services.TryAddSingleton<CapMarkerService>();
services.Configure(setupAction);
//Consumer service
AddSubscribeServices(services); AddSubscribeServices(services);
//Serializer and model binder //Serializer and model binder
...@@ -49,18 +49,18 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -49,18 +49,18 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<MethodMatcherCache>(); services.TryAddSingleton<MethodMatcherCache>();
//Bootstrapper and Processors //Bootstrapper and Processors
services.AddSingleton<IProcessingServer, ConsumerHandler>(); services.TryAddEnumerable(ServiceDescriptor.Singleton<IProcessingServer, ConsumerHandler>());
services.AddSingleton<IProcessingServer, CapProcessingServer>(); services.TryAddEnumerable(ServiceDescriptor.Singleton<IProcessingServer, CapProcessingServer>());
services.AddSingleton<IBootstrapper, DefaultBootstrapper>(); services.TryAddSingleton<IBootstrapper, DefaultBootstrapper>();
services.AddSingleton<IStateChanger, StateChanger>(); services.TryAddSingleton<IStateChanger, StateChanger>();
//Queue's message processor //Queue's message processor
services.AddTransient<NeedRetryMessageProcessor>(); services.TryAddSingleton<NeedRetryMessageProcessor>();
//Sender and Executors //Sender and Executors
services.AddSingleton<IDispatcher, Dispatcher>(); services.TryAddSingleton<IDispatcher, Dispatcher>();
// Warning: IPublishMessageSender need to inject at extension project. // Warning: IPublishMessageSender need to inject at extension project.
services.AddSingleton<ISubscriberExecutor, DefaultSubscriberExecutor>(); services.TryAddSingleton<ISubscriberExecutor, DefaultSubscriberExecutor>();
//Options and extension service //Options and extension service
var options = new CapOptions(); var options = new CapOptions();
...@@ -69,9 +69,11 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -69,9 +69,11 @@ namespace Microsoft.Extensions.DependencyInjection
{ {
serviceExtension.AddServices(services); serviceExtension.AddServices(services);
} }
services.AddSingleton(options); services.AddSingleton(options);
//Startup and Middleware
services.AddTransient<IStartupFilter, CapStartupFilter>();
return new CapBuilder(services); return new CapBuilder(services);
} }
...@@ -90,7 +92,7 @@ namespace Microsoft.Extensions.DependencyInjection ...@@ -90,7 +92,7 @@ namespace Microsoft.Extensions.DependencyInjection
foreach (var service in consumerListenerServices) 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 ...@@ -82,14 +82,14 @@ namespace DotNetCore.CAP.Dashboard
Routes.AddJsonResult("/published/message/(?<Id>.+)", x => 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) var message = x.Storage.GetConnection().GetPublishedMessageAsync(id)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
return message.Content; return message.Content;
}); });
Routes.AddJsonResult("/received/message/(?<Id>.+)", x => 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) var message = x.Storage.GetConnection().GetReceivedMessageAsync(id)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
return message.Content; return message.Content;
......
...@@ -7,7 +7,7 @@ namespace DotNetCore.CAP.Dashboard.Monitoring ...@@ -7,7 +7,7 @@ namespace DotNetCore.CAP.Dashboard.Monitoring
{ {
public class MessageDto public class MessageDto
{ {
public int Id { get; set; } public long Id { get; set; }
public string Group { get; set; } public string Group { get; set; }
......
...@@ -31,16 +31,12 @@ ...@@ -31,16 +31,12 @@
</ItemGroup> </ItemGroup>
<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.Hosting.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.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="Microsoft.Extensions.Logging.Abstractions" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <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.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" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
</ItemGroup> </ItemGroup>
......
// Copyright (c) .NET Core Community. All rights reserved. // Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information. // Licensed under the MIT License. See License.txt in the project root for license information.
using System.Data; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DotNetCore.CAP namespace DotNetCore.CAP
...@@ -12,47 +12,25 @@ namespace DotNetCore.CAP ...@@ -12,47 +12,25 @@ namespace DotNetCore.CAP
public interface ICapPublisher public interface ICapPublisher
{ {
/// <summary> /// <summary>
/// (EntityFramework) Asynchronous publish a object message. /// CAP transaction context object
/// <para>
/// If you are using the EntityFramework, you need to configure the DbContextType first.
/// otherwise you need to use overloaded method with IDbTransaction.
/// </para>
/// </summary> /// </summary>
/// <typeparam name="T">The type of content object.</typeparam> ICapTransaction Transaction { get; }
/// <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);
/// <summary> /// <summary>
/// (EntityFramework) Publish a object message. /// Asynchronous publish an 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>
/// </summary> /// </summary>
/// <typeparam name="T">The type of content object.</typeparam>
/// <param name="name">the topic name or exchange router key.</param> /// <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="contentObj">message body content, that will be serialized of json.</param>
/// <param name="callbackName">callback subscriber name</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> /// <summary>
/// (ado.net) Asynchronous publish a object message. /// Publish an object message.
/// </summary> /// </summary>
/// <param name="name">the topic name or exchange router key.</param> /// <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="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> /// <param name="callbackName">callback subscriber name</param>
Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null); void Publish<T>(string name, T contentObj, 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);
} }
} }
\ No newline at end of file
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();
}
}
// 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();
}
}
\ No newline at end of file
...@@ -112,6 +112,7 @@ namespace DotNetCore.CAP ...@@ -112,6 +112,7 @@ namespace DotNetCore.CAP
var receivedMessage = new CapReceivedMessage(messageContext) var receivedMessage = new CapReceivedMessage(messageContext)
{ {
Id = SnowflakeId.Default().NextId(),
StatusName = StatusName.Scheduled, StatusName = StatusName.Scheduled,
Content = messageBody Content = messageBody
}; };
...@@ -170,10 +171,7 @@ namespace DotNetCore.CAP ...@@ -170,10 +171,7 @@ namespace DotNetCore.CAP
private void StoreMessage(CapReceivedMessage receivedMessage) private void StoreMessage(CapReceivedMessage receivedMessage)
{ {
var id = _connection.StoreReceivedMessageAsync(receivedMessage) _connection.StoreReceivedMessage(receivedMessage);
.GetAwaiter().GetResult();
receivedMessage.Id = id;
} }
private (Guid, string) TracingBefore(string topic, string values) private (Guid, string) TracingBefore(string topic, string values)
......
...@@ -173,8 +173,6 @@ namespace DotNetCore.CAP ...@@ -173,8 +173,6 @@ namespace DotNetCore.CAP
du); du);
s_diagnosticListener.WritePublishAfter(eventData); s_diagnosticListener.WritePublishAfter(eventData);
_logger.MessageHasBeenSent(du.TotalSeconds);
} }
private void TracingError(Guid operationId, CapPublishedMessage message, OperateResult result, DateTimeOffset startTime, TimeSpan du) private void TracingError(Guid operationId, CapPublishedMessage message, OperateResult result, DateTimeOffset startTime, TimeSpan du)
......
// Copyright (c) .NET Core Community. All rights reserved. // Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information. // Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Models; using DotNetCore.CAP.Models;
...@@ -11,7 +10,7 @@ namespace DotNetCore.CAP ...@@ -11,7 +10,7 @@ namespace DotNetCore.CAP
/// <summary> /// <summary>
/// Represents a connection to the storage. /// Represents a connection to the storage.
/// </summary> /// </summary>
public interface IStorageConnection : IDisposable public interface IStorageConnection
{ {
//Sent messages //Sent messages
...@@ -19,7 +18,7 @@ namespace DotNetCore.CAP ...@@ -19,7 +18,7 @@ namespace DotNetCore.CAP
/// Returns the message with the given id. /// Returns the message with the given id.
/// </summary> /// </summary>
/// <param name="id">The message's id.</param> /// <param name="id">The message's id.</param>
Task<CapPublishedMessage> GetPublishedMessageAsync(int id); Task<CapPublishedMessage> GetPublishedMessageAsync(long id);
/// <summary> /// <summary>
/// Returns executed failed messages. /// Returns executed failed messages.
...@@ -32,13 +31,13 @@ namespace DotNetCore.CAP ...@@ -32,13 +31,13 @@ namespace DotNetCore.CAP
/// Stores the message. /// Stores the message.
/// </summary> /// </summary>
/// <param name="message">The message to store.</param> /// <param name="message">The message to store.</param>
Task<int> StoreReceivedMessageAsync(CapReceivedMessage message); void StoreReceivedMessage(CapReceivedMessage message);
/// <summary> /// <summary>
/// Returns the message with the given id. /// Returns the message with the given id.
/// </summary> /// </summary>
/// <param name="id">The message's id.</param> /// <param name="id">The message's id.</param>
Task<CapReceivedMessage> GetReceivedMessageAsync(int id); Task<CapReceivedMessage> GetReceivedMessageAsync(long id);
/// <summary> /// <summary>
/// Returns executed failed message. /// Returns executed failed message.
...@@ -55,13 +54,13 @@ namespace DotNetCore.CAP ...@@ -55,13 +54,13 @@ namespace DotNetCore.CAP
/// </summary> /// </summary>
/// <param name="messageId">Message id</param> /// <param name="messageId">Message id</param>
/// <param name="state">State name</param> /// <param name="state">State name</param>
bool ChangePublishedState(int messageId, string state); bool ChangePublishedState(long messageId, string state);
/// <summary> /// <summary>
/// Change specified message's state of received message /// Change specified message's state of received message
/// </summary> /// </summary>
/// <param name="messageId">Message id</param> /// <param name="messageId">Message id</param>
/// <param name="state">State name</param> /// <param name="state">State name</param>
bool ChangeReceivedState(int messageId, string state); bool ChangeReceivedState(long messageId, string state);
} }
} }
\ No newline at end of file
...@@ -18,15 +18,13 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -18,15 +18,13 @@ namespace DotNetCore.CAP.Infrastructure
public struct ObjectId : IComparable<ObjectId>, IEquatable<ObjectId> public struct ObjectId : IComparable<ObjectId>, IEquatable<ObjectId>
{ {
// private static fields // private static fields
private static readonly DateTime __unixEpoch; private static readonly DateTime UnixEpoch;
private static readonly long __dateTimeMaxValueMillisecondsSinceEpoch; private static readonly int StaticMachine;
private static readonly long __dateTimeMinValueMillisecondsSinceEpoch; private static readonly short StaticPid;
private static readonly int __staticMachine; private static int _staticIncrement; // high byte will be masked out when generating new ObjectId
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"); var s = i.ToString("x2");
return (uint) s[0] + ((uint) s[1] << 16); return (uint) s[0] + ((uint) s[1] << 16);
...@@ -44,40 +42,13 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -44,40 +42,13 @@ namespace DotNetCore.CAP.Infrastructure
// static constructor // static constructor
static ObjectId() static ObjectId()
{ {
__unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
__dateTimeMaxValueMillisecondsSinceEpoch = (DateTime.MaxValue - __unixEpoch).Ticks / 10000; StaticMachine = GetMachineHash();
__dateTimeMinValueMillisecondsSinceEpoch = (DateTime.MinValue - __unixEpoch).Ticks / 10000; _staticIncrement = new Random().Next();
__staticMachine = GetMachineHash(); StaticPid = (short) GetCurrentProcessId();
__staticIncrement = new Random().Next();
__staticPid = (short) GetCurrentProcessId();
} }
// constructors // 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> /// <summary>
/// Initializes a new instance of the ObjectId class. /// Initializes a new instance of the ObjectId class.
...@@ -90,14 +61,14 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -90,14 +61,14 @@ namespace DotNetCore.CAP.Infrastructure
{ {
if ((machine & 0xff000000) != 0) if ((machine & 0xff000000) != 0)
{ {
throw new ArgumentOutOfRangeException("machine", throw new ArgumentOutOfRangeException(nameof(machine),
"The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); @"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
} }
if ((increment & 0xff000000) != 0) if ((increment & 0xff000000) != 0)
{ {
throw new ArgumentOutOfRangeException("increment", throw new ArgumentOutOfRangeException(nameof(increment),
"The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); @"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
} }
_timestamp = timestamp; _timestamp = timestamp;
...@@ -106,75 +77,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -106,75 +77,6 @@ namespace DotNetCore.CAP.Infrastructure
_increment = increment; _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> /// <summary>
/// Compares two ObjectIds. /// Compares two ObjectIds.
/// </summary> /// </summary>
...@@ -197,28 +99,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -197,28 +99,6 @@ namespace DotNetCore.CAP.Infrastructure
return !(lhs == rhs); 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 // public static methods
/// <summary> /// <summary>
/// Generates a new ObjectId with a unique value. /// Generates a new ObjectId with a unique value.
...@@ -229,16 +109,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -229,16 +109,6 @@ namespace DotNetCore.CAP.Infrastructure
return GenerateNewId(GetTimestampFromDateTime(DateTime.UtcNow)); 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> /// <summary>
/// Generates a new ObjectId with a unique value (with the given timestamp). /// Generates a new ObjectId with a unique value (with the given timestamp).
/// </summary> /// </summary>
...@@ -246,8 +116,8 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -246,8 +116,8 @@ namespace DotNetCore.CAP.Infrastructure
/// <returns>An ObjectId.</returns> /// <returns>An ObjectId.</returns>
public static ObjectId GenerateNewId(int timestamp) public static ObjectId GenerateNewId(int timestamp)
{ {
var increment = Interlocked.Increment(ref __staticIncrement) & 0x00ffffff; // only use low order 3 bytes var increment = Interlocked.Increment(ref _staticIncrement) & 0x00ffffff; // only use low order 3 bytes
return new ObjectId(timestamp, __staticMachine, __staticPid, increment); return new ObjectId(timestamp, StaticMachine, StaticPid, increment);
} }
/// <summary> /// <summary>
...@@ -271,14 +141,14 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -271,14 +141,14 @@ namespace DotNetCore.CAP.Infrastructure
{ {
if ((machine & 0xff000000) != 0) if ((machine & 0xff000000) != 0)
{ {
throw new ArgumentOutOfRangeException("machine", throw new ArgumentOutOfRangeException(nameof(machine),
"The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); @"The machine value must be between 0 and 16777215 (it must fit in 3 bytes).");
} }
if ((increment & 0xff000000) != 0) if ((increment & 0xff000000) != 0)
{ {
throw new ArgumentOutOfRangeException("increment", throw new ArgumentOutOfRangeException(nameof(increment),
"The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); @"The increment value must be between 0 and 16777215 (it must fit in 3 bytes).");
} }
var bytes = new byte[12]; var bytes = new byte[12];
...@@ -297,53 +167,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -297,53 +167,6 @@ namespace DotNetCore.CAP.Infrastructure
return bytes; 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> /// <summary>
/// Gets the current process id. This method exists because of how CAS operates on the call stack, checking /// 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 /// 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 ...@@ -365,7 +188,7 @@ namespace DotNetCore.CAP.Infrastructure
private static int GetTimestampFromDateTime(DateTime timestamp) private static int GetTimestampFromDateTime(DateTime timestamp)
{ {
return (int) Math.Floor((ToUniversalTime(timestamp) - __unixEpoch).TotalSeconds); return (int) Math.Floor((ToUniversalTime(timestamp) - UnixEpoch).TotalSeconds);
} }
// public methods // public methods
...@@ -421,9 +244,9 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -421,9 +244,9 @@ namespace DotNetCore.CAP.Infrastructure
/// <returns>True if the other object is an ObjectId and equal to this one.</returns> /// <returns>True if the other object is an ObjectId and equal to this one.</returns>
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (obj is ObjectId) if (obj is ObjectId id)
{ {
return Equals((ObjectId) obj); return Equals(id);
} }
return false; return false;
...@@ -461,33 +284,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -461,33 +284,6 @@ namespace DotNetCore.CAP.Infrastructure
return ToHexString(ToByteArray()); 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> /// <summary>
/// Converts a byte array to a hex string. /// Converts a byte array to a hex string.
/// </summary> /// </summary>
...@@ -497,13 +293,13 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -497,13 +293,13 @@ namespace DotNetCore.CAP.Infrastructure
{ {
if (bytes == null) if (bytes == null)
{ {
throw new ArgumentNullException("bytes"); throw new ArgumentNullException(nameof(bytes));
} }
var result = new char[bytes.Length * 2]; var result = new char[bytes.Length * 2];
for (var i = 0; i < bytes.Length; i++) 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] = (char) val;
result[2 * i + 1] = (char) (val >> 16); result[2 * i + 1] = (char) (val >> 16);
} }
...@@ -511,17 +307,6 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -511,17 +307,6 @@ namespace DotNetCore.CAP.Infrastructure
return new string(result); 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> /// <summary>
/// Converts a DateTime to UTC (with special handling for MinValue and MaxValue). /// Converts a DateTime to UTC (with special handling for MinValue and MaxValue).
/// </summary> /// </summary>
...@@ -541,16 +326,5 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -541,16 +326,5 @@ namespace DotNetCore.CAP.Infrastructure
return dateTime.ToUniversalTime(); 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));
}
} }
} }
\ No newline at end of file
// 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();
}
}
}
\ No newline at end of file
...@@ -11,5 +11,17 @@ namespace DotNetCore.CAP.Infrastructure ...@@ -11,5 +11,17 @@ namespace DotNetCore.CAP.Infrastructure
public const string Scheduled = nameof(Scheduled); public const string Scheduled = nameof(Scheduled);
public const string Succeeded = nameof(Succeeded); public const string Succeeded = nameof(Succeeded);
public const string Failed = nameof(Failed); 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;
}
} }
} }
\ No newline at end of file
...@@ -54,6 +54,7 @@ namespace DotNetCore.CAP.Internal ...@@ -54,6 +54,7 @@ namespace DotNetCore.CAP.Internal
var publishedMessage = new CapPublishedMessage var publishedMessage = new CapPublishedMessage
{ {
Id = SnowflakeId.Default().NextId(),
Name = topicName, Name = topicName,
Content = content, Content = content,
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
......
...@@ -10,12 +10,12 @@ namespace DotNetCore.CAP ...@@ -10,12 +10,12 @@ namespace DotNetCore.CAP
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
internal static class LoggerExtensions 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."); 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."); 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 ...@@ -25,22 +25,22 @@ namespace DotNetCore.CAP
logger.LogWarning(ex, "FailedThresholdCallback action raised an exception:" + ex.Message); 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}"); 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} "); 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}"); logger.LogError(ex, $"An exception occured while publishing a message, reason:{reason}. message id:{messageId}");
} }
......
// Copyright (c) .NET Core Community. All rights reserved. // Copyright (c) .NET Core Community. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information. // Licensed under the MIT License. See License.txt in the project root for license information.
namespace DotNetCore.CAP.Models namespace DotNetCore.CAP.Internal
{ {
public class CapQueue public class NoopTransaction
{ {
public int MessageId { get; set; }
/// <summary>
/// 0 is CapSentMessage, 1 is CapReceivedMessage
/// </summary>
public MessageType MessageType { get; set; }
} }
} }
...@@ -15,7 +15,7 @@ namespace DotNetCore.CAP.Models ...@@ -15,7 +15,7 @@ namespace DotNetCore.CAP.Models
Added = DateTime.Now; Added = DateTime.Now;
} }
public int Id { get; set; } public long Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
......
...@@ -22,7 +22,7 @@ namespace DotNetCore.CAP.Models ...@@ -22,7 +22,7 @@ namespace DotNetCore.CAP.Models
Content = message.Content; Content = message.Content;
} }
public int Id { get; set; } public long Id { get; set; }
public string Group { get; set; } public string Group { get; set; }
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace DotNetCore.CAP.Processor namespace DotNetCore.CAP.Processor
{ {
...@@ -16,13 +15,13 @@ namespace DotNetCore.CAP.Processor ...@@ -16,13 +15,13 @@ namespace DotNetCore.CAP.Processor
private readonly TimeSpan _waitingInterval; private readonly TimeSpan _waitingInterval;
public NeedRetryMessageProcessor( public NeedRetryMessageProcessor(
IOptions<CapOptions> options, CapOptions options,
ISubscriberExecutor subscriberExecutor, ISubscriberExecutor subscriberExecutor,
IPublishMessageSender publishMessageSender) IPublishMessageSender publishMessageSender)
{ {
_subscriberExecutor = subscriberExecutor; _subscriberExecutor = subscriberExecutor;
_publishMessageSender = publishMessageSender; _publishMessageSender = publishMessageSender;
_waitingInterval = TimeSpan.FromSeconds(options.Value.FailedRetryInterval); _waitingInterval = TimeSpan.FromSeconds(options.FailedRetryInterval);
} }
public async Task ProcessAsync(ProcessingContext context) public async Task ProcessAsync(ProcessingContext context)
......
namespace DotNetCore.CAP.MongoDB.Test
{
public class ConnectionUtil
{
public static string ConnectionString = "mongodb://localhost:27017";
}
}
\ No newline at end of file
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);
}
}
}
\ No newline at end of file
<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>
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);
}
}
}
\ No newline at end of file
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();
}
}
}
\ No newline at end of file
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);
}
}
}
\ No newline at end of file
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);
}
}
}
...@@ -12,7 +12,7 @@ namespace DotNetCore.CAP.MySql.Test ...@@ -12,7 +12,7 @@ namespace DotNetCore.CAP.MySql.Test
private const string DefaultDatabaseName = @"DotNetCore.CAP.MySql.Test"; private const string DefaultDatabaseName = @"DotNetCore.CAP.MySql.Test";
private const string DefaultConnectionStringTemplate = private const string DefaultConnectionStringTemplate =
@"Server=localhost;Database={0};Uid=root;Pwd=123123;Allow User Variables=True;"; @"Server=localhost;Database={0};Uid=root;Pwd=123123;Allow User Variables=True;SslMode=none;";
public static string GetDatabaseName() public static string GetDatabaseName()
{ {
......
...@@ -13,12 +13,11 @@ ...@@ -13,12 +13,11 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="1.50.5" /> <PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="MySqlConnector" Version="0.40.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="xunit" Version="2.3.1" /> <PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Moq" Version="4.8.2" /> <PackageReference Include="Moq" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" />
......
...@@ -22,17 +22,18 @@ namespace DotNetCore.CAP.MySql.Test ...@@ -22,17 +22,18 @@ namespace DotNetCore.CAP.MySql.Test
[Fact] [Fact]
public async Task GetPublishedMessageAsync_Test() public async Task GetPublishedMessageAsync_Test()
{ {
var sql = "INSERT INTO `cap.published`(`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`) VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT @@IDENTITY;"; var sql = "INSERT INTO `cap.published`(`Id`,`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`) VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var insertedId = SnowflakeId.Default().NextId();
var publishMessage = new CapPublishedMessage var publishMessage = new CapPublishedMessage
{ {
Id = insertedId,
Name = "MySqlStorageConnectionTest", Name = "MySqlStorageConnectionTest",
Content = "", Content = "",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, publishMessage); await connection.ExecuteAsync(sql, publishMessage);
} }
var message = await _storage.GetPublishedMessageAsync(insertedId); var message = await _storage.GetPublishedMessageAsync(insertedId);
Assert.NotNull(message); Assert.NotNull(message);
...@@ -41,10 +42,11 @@ namespace DotNetCore.CAP.MySql.Test ...@@ -41,10 +42,11 @@ namespace DotNetCore.CAP.MySql.Test
} }
[Fact] [Fact]
public async Task StoreReceivedMessageAsync_Test() public void StoreReceivedMessageAsync_Test()
{ {
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
Id = SnowflakeId.Default().NextId(),
Name = "MySqlStorageConnectionTest", Name = "MySqlStorageConnectionTest",
Content = "", Content = "",
Group = "mygroup", Group = "mygroup",
...@@ -54,7 +56,7 @@ namespace DotNetCore.CAP.MySql.Test ...@@ -54,7 +56,7 @@ namespace DotNetCore.CAP.MySql.Test
Exception exception = null; Exception exception = null;
try try
{ {
await _storage.StoreReceivedMessageAsync(receivedMessage); _storage.StoreReceivedMessage(receivedMessage);
} }
catch (Exception ex) catch (Exception ex)
{ {
...@@ -67,23 +69,23 @@ namespace DotNetCore.CAP.MySql.Test ...@@ -67,23 +69,23 @@ namespace DotNetCore.CAP.MySql.Test
public async Task GetReceivedMessageAsync_Test() public async Task GetReceivedMessageAsync_Test()
{ {
var sql = $@" var sql = $@"
INSERT INTO `cap.received`(`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`) INSERT INTO `cap.received`(`Id`,`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT @@IDENTITY;"; VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var insertedId = SnowflakeId.Default().NextId();
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
Id = insertedId,
Name = "MySqlStorageConnectionTest", Name = "MySqlStorageConnectionTest",
Content = "", Content = "",
Group = "mygroup", Group = "mygroup",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, receivedMessage); await connection.ExecuteAsync(sql, receivedMessage);
} }
var message = await _storage.GetReceivedMessageAsync(insertedId); var message = await _storage.GetReceivedMessageAsync(insertedId);
Assert.NotNull(message); Assert.NotNull(message);
Assert.Equal(StatusName.Scheduled, message.StatusName); Assert.Equal(StatusName.Scheduled, message.StatusName);
Assert.Equal("MySqlStorageConnectionTest", message.Name); Assert.Equal("MySqlStorageConnectionTest", message.Name);
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="1.50.5" /> <PackageReference Include="Dapper" Version="1.50.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="Npgsql" Version="4.0.0" /> <PackageReference Include="Npgsql" Version="4.0.2" />
<PackageReference Include="xunit" Version="2.3.1" /> <PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup> </ItemGroup>
......
...@@ -22,17 +22,18 @@ namespace DotNetCore.CAP.PostgreSql.Test ...@@ -22,17 +22,18 @@ namespace DotNetCore.CAP.PostgreSql.Test
[Fact] [Fact]
public async Task GetPublishedMessageAsync_Test() public async Task GetPublishedMessageAsync_Test()
{ {
var sql = @"INSERT INTO ""cap"".""published""(""Name"",""Content"",""Retries"",""Added"",""ExpiresAt"",""StatusName"") VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING ""Id"";"; var sql = @"INSERT INTO ""cap"".""published""(""Id"",""Name"",""Content"",""Retries"",""Added"",""ExpiresAt"",""StatusName"") VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var insertedId = SnowflakeId.Default().NextId();
var publishMessage = new CapPublishedMessage var publishMessage = new CapPublishedMessage
{ {
Id = insertedId,
Name = "PostgreSqlStorageConnectionTest", Name = "PostgreSqlStorageConnectionTest",
Content = "", Content = "",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, publishMessage); await connection.ExecuteAsync(sql, publishMessage);
} }
var message = await _storage.GetPublishedMessageAsync(insertedId); var message = await _storage.GetPublishedMessageAsync(insertedId);
Assert.NotNull(message); Assert.NotNull(message);
...@@ -41,10 +42,11 @@ namespace DotNetCore.CAP.PostgreSql.Test ...@@ -41,10 +42,11 @@ namespace DotNetCore.CAP.PostgreSql.Test
} }
[Fact] [Fact]
public async Task StoreReceivedMessageAsync_Test() public void StoreReceivedMessageAsync_Test()
{ {
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
Id = SnowflakeId.Default().NextId(),
Name = "PostgreSqlStorageConnectionTest", Name = "PostgreSqlStorageConnectionTest",
Content = "", Content = "",
Group = "mygroup", Group = "mygroup",
...@@ -54,7 +56,7 @@ namespace DotNetCore.CAP.PostgreSql.Test ...@@ -54,7 +56,7 @@ namespace DotNetCore.CAP.PostgreSql.Test
Exception exception = null; Exception exception = null;
try try
{ {
await _storage.StoreReceivedMessageAsync(receivedMessage); _storage.StoreReceivedMessage(receivedMessage);
} }
catch (Exception ex) catch (Exception ex)
{ {
...@@ -67,19 +69,21 @@ namespace DotNetCore.CAP.PostgreSql.Test ...@@ -67,19 +69,21 @@ namespace DotNetCore.CAP.PostgreSql.Test
public async Task GetReceivedMessageAsync_Test() public async Task GetReceivedMessageAsync_Test()
{ {
var sql = $@" var sql = $@"
INSERT INTO ""cap"".""received""(""Name"",""Group"",""Content"",""Retries"",""Added"",""ExpiresAt"",""StatusName"") INSERT INTO ""cap"".""received""(""Id"",""Name"",""Group"",""Content"",""Retries"",""Added"",""ExpiresAt"",""StatusName"")
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING ""Id"";"; VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var insertedId = SnowflakeId.Default().NextId();
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
Id= insertedId,
Name = "PostgreSqlStorageConnectionTest", Name = "PostgreSqlStorageConnectionTest",
Content = "", Content = "",
Group = "mygroup", Group = "mygroup",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, receivedMessage); await connection.ExecuteAsync(sql, receivedMessage);
} }
var message = await _storage.GetReceivedMessageAsync(insertedId); var message = await _storage.GetReceivedMessageAsync(insertedId);
......
...@@ -6,13 +6,11 @@ namespace DotNetCore.CAP.PostgreSql.Test ...@@ -6,13 +6,11 @@ namespace DotNetCore.CAP.PostgreSql.Test
[Collection("postgresql")] [Collection("postgresql")]
public class SqlServerStorageTest : DatabaseTestHost public class SqlServerStorageTest : DatabaseTestHost
{ {
private readonly string _dbName;
private readonly string _masterDbConnectionString; private readonly string _masterDbConnectionString;
private readonly string _dbConnectionString; private readonly string _dbConnectionString;
public SqlServerStorageTest() public SqlServerStorageTest()
{ {
_dbName = ConnectionUtil.GetDatabaseName();
_masterDbConnectionString = ConnectionUtil.GetMasterConnectionString(); _masterDbConnectionString = ConnectionUtil.GetMasterConnectionString();
_dbConnectionString = ConnectionUtil.GetConnectionString(); _dbConnectionString = ConnectionUtil.GetConnectionString();
} }
......
using System;
using System.Data; using System.Data;
using System.Data.SqlClient; using System.Data.SqlClient;
using System.Threading;
using Dapper; using Dapper;
using DotNetCore.CAP.SqlServer.Diagnostics;
using Microsoft.Extensions.Logging;
using Moq;
namespace DotNetCore.CAP.SqlServer.Test namespace DotNetCore.CAP.SqlServer.Test
{ {
public abstract class DatabaseTestHost : TestHost public abstract class DatabaseTestHost : IDisposable
{ {
private static bool _sqlObjectInstalled; protected ILogger<SqlServerStorage> Logger;
public static object _lock = new object(); protected CapOptions CapOptions;
protected SqlServerOptions SqlSeverOptions;
protected DiagnosticProcessorObserver DiagnosticProcessorObserver;
protected override void PostBuildServices() public bool SqlObjectInstalled;
{
base.PostBuildServices(); protected DatabaseTestHost()
lock (_lock)
{ {
if (!_sqlObjectInstalled) Logger = new Mock<ILogger<SqlServerStorage>>().Object;
CapOptions = new Mock<CapOptions>().Object;
SqlSeverOptions = new SqlServerOptions()
{ {
ConnectionString = ConnectionUtil.GetConnectionString()
};
DiagnosticProcessorObserver = new DiagnosticProcessorObserver(new Mock<IDispatcher>().Object);
InitializeDatabase(); InitializeDatabase();
} }
}
}
public override void Dispose() public void Dispose()
{ {
DeleteAllData(); DeleteAllData();
base.Dispose();
} }
private void InitializeDatabase() private void InitializeDatabase()
{
using (CreateScope())
{
var storage = GetService<SqlServerStorage>();
var token = new CancellationTokenSource().Token;
CreateDatabase();
storage.InitializeAsync(token).GetAwaiter().GetResult();
_sqlObjectInstalled = true;
}
}
private void CreateDatabase()
{ {
var masterConn = ConnectionUtil.GetMasterConnectionString(); var masterConn = ConnectionUtil.GetMasterConnectionString();
var databaseName = ConnectionUtil.GetDatabaseName(); var databaseName = ConnectionUtil.GetDatabaseName();
...@@ -50,8 +46,12 @@ namespace DotNetCore.CAP.SqlServer.Test ...@@ -50,8 +46,12 @@ namespace DotNetCore.CAP.SqlServer.Test
IF NOT EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}') IF NOT EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}')
CREATE DATABASE [{databaseName}];"); CREATE DATABASE [{databaseName}];");
} }
new SqlServerStorage(Logger, CapOptions, SqlSeverOptions, DiagnosticProcessorObserver).InitializeAsync().GetAwaiter().GetResult();
SqlObjectInstalled = true;
} }
private void DeleteAllData() private void DeleteAllData()
{ {
var conn = ConnectionUtil.GetConnectionString(); var conn = ConnectionUtil.GetConnectionString();
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<PackageReference Include="xunit" Version="2.3.1" /> <PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Moq" Version="4.8.2" /> <PackageReference Include="Moq" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" />
......
...@@ -10,30 +10,31 @@ namespace DotNetCore.CAP.SqlServer.Test ...@@ -10,30 +10,31 @@ namespace DotNetCore.CAP.SqlServer.Test
[Collection("sqlserver")] [Collection("sqlserver")]
public class SqlServerStorageConnectionTest : DatabaseTestHost public class SqlServerStorageConnectionTest : DatabaseTestHost
{ {
private SqlServerStorageConnection _storage; private readonly SqlServerStorageConnection _storage;
public SqlServerStorageConnectionTest() public SqlServerStorageConnectionTest()
{ {
var options = GetService<SqlServerOptions>(); _storage = new SqlServerStorageConnection(SqlSeverOptions, CapOptions);
var capOptions = GetService<CapOptions>();
_storage = new SqlServerStorageConnection(options, capOptions);
} }
[Fact] [Fact]
public async Task GetPublishedMessageAsync_Test() public async Task GetPublishedMessageAsync_Test()
{ {
var sql = "INSERT INTO [Cap].[Published]([Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) OUTPUT INSERTED.Id VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; var sql = "INSERT INTO [Cap].[Published]([Id],[Name],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) VALUES(@Id,@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var insertedId = SnowflakeId.Default().NextId();
var publishMessage = new CapPublishedMessage var publishMessage = new CapPublishedMessage
{ {
Id= insertedId,
Name = "SqlServerStorageConnectionTest", Name = "SqlServerStorageConnectionTest",
Content = "", Content = "",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, publishMessage); await connection.ExecuteAsync(sql, publishMessage);
} }
var message = await _storage.GetPublishedMessageAsync(insertedId); var message = await _storage.GetPublishedMessageAsync(insertedId);
Assert.NotNull(message); Assert.NotNull(message);
Assert.Equal("SqlServerStorageConnectionTest", message.Name); Assert.Equal("SqlServerStorageConnectionTest", message.Name);
...@@ -41,7 +42,7 @@ namespace DotNetCore.CAP.SqlServer.Test ...@@ -41,7 +42,7 @@ namespace DotNetCore.CAP.SqlServer.Test
} }
[Fact] [Fact]
public async Task StoreReceivedMessageAsync_Test() public void StoreReceivedMessageAsync_Test()
{ {
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
...@@ -54,7 +55,7 @@ namespace DotNetCore.CAP.SqlServer.Test ...@@ -54,7 +55,7 @@ namespace DotNetCore.CAP.SqlServer.Test
Exception exception = null; Exception exception = null;
try try
{ {
await _storage.StoreReceivedMessageAsync(receivedMessage); _storage.StoreReceivedMessage(receivedMessage);
} }
catch (Exception ex) catch (Exception ex)
{ {
...@@ -66,20 +67,20 @@ namespace DotNetCore.CAP.SqlServer.Test ...@@ -66,20 +67,20 @@ namespace DotNetCore.CAP.SqlServer.Test
[Fact] [Fact]
public async Task GetReceivedMessageAsync_Test() public async Task GetReceivedMessageAsync_Test()
{ {
var sql = $@" var sql = @"INSERT INTO [Cap].[Received]([Id],[Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) VALUES(@Id,@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
INSERT INTO [Cap].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) OUTPUT INSERTED.Id var insertedId = SnowflakeId.Default().NextId();
VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);";
var receivedMessage = new CapReceivedMessage var receivedMessage = new CapReceivedMessage
{ {
Id= insertedId,
Name = "SqlServerStorageConnectionTest", Name = "SqlServerStorageConnectionTest",
Content = "", Content = "",
Group = "mygroup", Group = "mygroup",
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var insertedId = default(int);
using (var connection = ConnectionUtil.CreateConnection()) using (var connection = ConnectionUtil.CreateConnection())
{ {
insertedId = connection.QueryFirst<int>(sql, receivedMessage); await connection.ExecuteAsync(sql, receivedMessage);
} }
var message = await _storage.GetReceivedMessageAsync(insertedId); var message = await _storage.GetReceivedMessageAsync(insertedId);
......
using System;
using Microsoft.Extensions.DependencyInjection;
namespace DotNetCore.CAP.SqlServer.Test
{
public abstract class TestHost : IDisposable
{
protected IServiceCollection _services;
protected string _connectionString;
private IServiceProvider _provider;
private IServiceProvider _scopedProvider;
public TestHost()
{
CreateServiceCollection();
PreBuildServices();
BuildServices();
PostBuildServices();
}
protected IServiceProvider Provider => _scopedProvider ?? _provider;
private void CreateServiceCollection()
{
var services = new ServiceCollection();
services.AddOptions();
services.AddLogging();
_connectionString = ConnectionUtil.GetConnectionString();
services.AddSingleton(new SqlServerOptions { ConnectionString = _connectionString });
services.AddSingleton(new CapOptions());
services.AddSingleton<SqlServerStorage>();
_services = services;
}
protected virtual void PreBuildServices()
{
}
private void BuildServices()
{
_provider = _services.BuildServiceProvider();
}
protected virtual void PostBuildServices()
{
}
public IDisposable CreateScope()
{
var scope = CreateScope(_provider);
var loc = scope.ServiceProvider;
_scopedProvider = loc;
return new DelegateDisposable(() =>
{
if (_scopedProvider == loc)
{
_scopedProvider = null;
}
scope.Dispose();
});
}
public IServiceScope CreateScope(IServiceProvider provider)
{
var scope = provider.GetService<IServiceScopeFactory>().CreateScope();
return scope;
}
public T GetService<T>() => Provider.GetService<T>();
public T Ensure<T>(ref T service)
where T : class
=> service ?? (service = GetService<T>());
public virtual void Dispose()
{
(_provider as IDisposable)?.Dispose();
}
private class DelegateDisposable : IDisposable
{
private Action _dispose;
public DelegateDisposable(Action dispose)
{
_dispose = dispose;
}
public void Dispose()
{
_dispose();
}
}
}
}
\ No newline at end of file
using System; using System;
using System.Data; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Abstractions;
using DotNetCore.CAP.Models; using DotNetCore.CAP.Models;
...@@ -119,47 +119,15 @@ namespace DotNetCore.CAP.Test ...@@ -119,47 +119,15 @@ namespace DotNetCore.CAP.Test
private class MyProducerService : ICapPublisher private class MyProducerService : ICapPublisher
{ {
public void Publish<T>(string name, T contentObj, string callbackName = null) public ICapTransaction Transaction { get; }
{
throw new NotImplementedException();
}
public void Publish<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{
throw new NotImplementedException();
}
public Task PublishAsync(string topic, string content) public Task PublishAsync<T>(string name, T contentObj, string callbackName = null,
CancellationToken cancellationToken = default(CancellationToken))
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task PublishAsync<T>(string topic, T contentObj) public void Publish<T>(string name, T contentObj, string callbackName = null)
{
throw new NotImplementedException();
}
public Task PublishAsync(string topic, string content, IDbConnection dbConnection)
{
throw new NotImplementedException();
}
public Task PublishAsync(string topic, string content, IDbConnection dbConnection, IDbTransaction dbTransaction)
{
throw new NotImplementedException();
}
public Task PublishAsync<T>(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null)
{
throw new NotImplementedException();
}
public Task PublishAsync<T>(string name, T contentObj, string callbackName = null)
{
throw new NotImplementedException();
}
public Task PublishAsync<T>(string name, T contentObj, IDbTransaction dbTransaction, string callbackName = null)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<PackageReference Include="xunit" Version="2.3.1" /> <PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Moq" Version="4.8.2" /> <PackageReference Include="Moq" Version="4.9.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.0" />
</ItemGroup> </ItemGroup>
......
...@@ -16,6 +16,7 @@ namespace DotNetCore.CAP.Test ...@@ -16,6 +16,7 @@ namespace DotNetCore.CAP.Test
var fixture = Create(); var fixture = Create();
var message = new CapPublishedMessage var message = new CapPublishedMessage
{ {
Id = SnowflakeId.Default().NextId(),
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var state = Mock.Of<IState>(s => s.Name == "s" && s.ExpiresAfter == null); var state = Mock.Of<IState>(s => s.Name == "s" && s.ExpiresAfter == null);
...@@ -39,6 +40,7 @@ namespace DotNetCore.CAP.Test ...@@ -39,6 +40,7 @@ namespace DotNetCore.CAP.Test
var fixture = Create(); var fixture = Create();
var message = new CapPublishedMessage var message = new CapPublishedMessage
{ {
Id = SnowflakeId.Default().NextId(),
StatusName = StatusName.Scheduled StatusName = StatusName.Scheduled
}; };
var state = Mock.Of<IState>(s => s.Name == "s" && s.ExpiresAfter == TimeSpan.FromHours(1)); var state = Mock.Of<IState>(s => s.Name == "s" && s.ExpiresAfter == TimeSpan.FromHours(1));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment