diff --git a/.gitignore b/.gitignore index 96cda33..d7fe0b1 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ bin/ /.idea/.idea.CAP /.idea/.idea.CAP /.idea -Properties \ No newline at end of file +Properties +/pack.bat diff --git a/.travis.yml b/.travis.yml index ec4e8c6..52f2ebc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,13 @@ matrix: include: - os: linux dist: trusty # Ubuntu 14.04 - dotnet: 1.0.1 + dotnet: 2.0.0 mono: none env: DOTNETCORE=1 sudo: required - os: osx - osx_image: xcode7.3 # macOS 10.11 - dotnet: 1.0.1 + osx_image: xcode8.3 # macOS 10.12 + dotnet: 2.0.0 mono: none env: DOTNETCORE=1 diff --git a/CAP.sln b/CAP.sln index d211d0c..c81828b 100644 --- a/CAP.sln +++ b/CAP.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B2AE124-6636-4DE9-83A3-70360DABD0C4}" EndProject @@ -22,15 +22,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.zh-cn.md = README.zh-cn.md EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{9E5A7F49-8E31-4A71-90CC-1DA9AEDA99EE}" - ProjectSection(SolutionItems) = preProject - test\Shared\MessageManagerTestBase.cs = test\Shared\MessageManagerTestBase.cs - test\Shared\TestLogger.cs = test\Shared\TestLogger.cs - EndProjectSection - ProjectSection(FolderStartupServices) = postProject - {82A7F48D-3B50-4B1E-B82E-3ADA8210C358} = {82A7F48D-3B50-4B1E-B82E-3ADA8210C358} - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP", "src\DotNetCore.CAP\DotNetCore.CAP.csproj", "{E8AF8611-0EA4-4B19-BC48-87C57A87DC66}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3A6B6931-A123-477A-9469-8B468B5385AF}" @@ -63,7 +54,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.MySql.Test", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.MySql", "samples\Sample.RabbitMQ.MySql\Sample.RabbitMQ.MySql.csproj", "{9F3F9BFE-7B6A-4A7A-A6E6-8B517D611873}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Kafka.SqlServer", "samples\Sample.Kafka.SqlServer\Sample.Kafka.SqlServer.csproj", "{AF17B956-B79E-48B7-9B5B-EB15A386B112}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.SqlServer", "samples\Sample.RabbitMQ.SqlServer\Sample.RabbitMQ.SqlServer.csproj", "{AF17B956-B79E-48B7-9B5B-EB15A386B112}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetCore.CAP.PostgreSql", "src\DotNetCore.CAP.PostgreSql\DotNetCore.CAP.PostgreSql.csproj", "{82C403AB-ED68-4084-9A1D-11334F9F08F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.RabbitMQ.PostgreSql", "samples\Sample.RabbitMQ.PostgreSql\Sample.RabbitMQ.PostgreSql.csproj", "{A17E8E72-DFFC-4822-BB38-73D59A8B264E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetCore.CAP.PostgreSql.Test", "test\DotNetCore.CAP.PostgreSql.Test\DotNetCore.CAP.PostgreSql.Test.csproj", "{7CA3625D-1817-4695-881D-7E79A1E1DED2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -110,12 +107,23 @@ Global {AF17B956-B79E-48B7-9B5B-EB15A386B112}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF17B956-B79E-48B7-9B5B-EB15A386B112}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF17B956-B79E-48B7-9B5B-EB15A386B112}.Release|Any CPU.Build.0 = Release|Any CPU + {82C403AB-ED68-4084-9A1D-11334F9F08F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82C403AB-ED68-4084-9A1D-11334F9F08F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82C403AB-ED68-4084-9A1D-11334F9F08F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82C403AB-ED68-4084-9A1D-11334F9F08F9}.Release|Any CPU.Build.0 = Release|Any CPU + {A17E8E72-DFFC-4822-BB38-73D59A8B264E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A17E8E72-DFFC-4822-BB38-73D59A8B264E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A17E8E72-DFFC-4822-BB38-73D59A8B264E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A17E8E72-DFFC-4822-BB38-73D59A8B264E}.Release|Any CPU.Build.0 = Release|Any CPU + {7CA3625D-1817-4695-881D-7E79A1E1DED2}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {9E5A7F49-8E31-4A71-90CC-1DA9AEDA99EE} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {E8AF8611-0EA4-4B19-BC48-87C57A87DC66} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {C42CDE33-0878-4BA0-96F2-4CB7C8FDEAAD} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} {9961B80E-0718-4280-B2A0-271B003DE26B} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} @@ -126,5 +134,11 @@ Global {80A84F62-1558-427B-BA74-B47AA8A665B5} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} {9F3F9BFE-7B6A-4A7A-A6E6-8B517D611873} = {3A6B6931-A123-477A-9469-8B468B5385AF} {AF17B956-B79E-48B7-9B5B-EB15A386B112} = {3A6B6931-A123-477A-9469-8B468B5385AF} + {82C403AB-ED68-4084-9A1D-11334F9F08F9} = {9B2AE124-6636-4DE9-83A3-70360DABD0C4} + {A17E8E72-DFFC-4822-BB38-73D59A8B264E} = {3A6B6931-A123-477A-9469-8B468B5385AF} + {7CA3625D-1817-4695-881D-7E79A1E1DED2} = {C09CDAB0-6DD4-46E9-B7F3-3EF2A4741EA0} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E70565D-94CF-40B4-BFE1-AC18D5F736AB} EndGlobalSection EndGlobal diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 7f29ea1..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at yangxiaodong1214@126.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 72fe766..5e6ce5f 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,13 @@ If your Message Queue is using RabbitMQ, you can: PM> Install-Package DotNetCore.CAP.RabbitMQ ``` -CAP provides EntityFramework as default database store extension (The MySQL version is under development): +CAP supported SqlServer, MySql, PostgreSql as message store extension: ``` +//Select a database provider you are using PM> Install-Package DotNetCore.CAP.SqlServer +PM> Install-Package DotNetCore.CAP.MySql +PM> Install-Package DotNetCore.CAP.PostgreSql ``` ### Configuration @@ -66,9 +69,11 @@ public void ConfigureServices(IServiceCollection services) // If your SqlServer is using EF for data operations, you need to add the following configuration: // Notice: You don't need to config x.UseSqlServer(""") again! x.UseEntityFramework(); - + // If you are using Dapper,you need to add the config: x.UseSqlServer("Your ConnectionStrings"); + //x.UseMySql("Your ConnectionStrings"); + //x.UsePostgreSql("Your ConnectionStrings"); // If your Message Queue is using RabbitMQ you need to add the config: x.UseRabbitMQ("localhost"); @@ -82,7 +87,7 @@ public void Configure(IApplicationBuilder app) { ..... - app.UseCap(); + app.UseCap(); } ``` @@ -114,12 +119,12 @@ public class PublishController : Controller [Route("~/checkAccountWithTrans")] public async Task PublishMessageWithTransaction([FromServices]AppDbContext dbContext) { - using (var trans = dbContext.Database.BeginTransaction()) - { + using (var trans = dbContext.Database.BeginTransaction()) + { await _publisher.PublishAsync("xxx.services.account.check", new Person { Name = "Foo", Age = 11 }); trans.Commit(); - } + } return Ok(); } } @@ -174,7 +179,7 @@ namespace xxx.Service [CapSubscribe("xxx.services.account.check")] public void CheckReceivedMessage(Person person) { - + } } } diff --git a/README.zh-cn.md b/README.zh-cn.md index 732ae68..6b65229 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -8,6 +8,8 @@ CAP 是一个在分布式系统(SOA、MicroService)中实现最终一致性的库,它具有轻量级、易使用、高性能等特点。 +你可以在这里[CAP Wiki](https://github.com/dotnetcore/CAP/wiki)看到更多详细资料。 + ## 预览(OverView) CAP 是在一个 ASP.NET Core 项目中使用的库,当然他可以用于 ASP.NET Core On .NET Framework 中。 @@ -44,10 +46,13 @@ PM> Install-Package DotNetCore.CAP.Kafka PM> Install-Package DotNetCore.CAP.RabbitMQ ``` -CAP 默认提供了 Sql Server 的扩展作为数据库存储(MySql的正在开发中): +CAP 提供了 Sql Server, MySql, PostgreSQL 的扩展作为数据库存储: ``` +// 按需选择安装你正在使用的数据库 PM> Install-Package DotNetCore.CAP.SqlServer +PM> Install-Package DotNetCore.CAP.MySql +PM> Install-Package DotNetCore.CAP.PostgreSql ``` ### Configuration diff --git a/appveyor.yml b/appveyor.yml index 2da2824..4185499 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,15 @@ version: '{build}' -os: Visual Studio 2015 +os: Visual Studio 2017 environment: BUILDING_ON_PLATFORM: win BuildEnvironment: appveyor Cap_SqlServer_ConnectionStringTemplate: Server=(local)\SQL2014;Database={0};User ID=sa;Password=Password12! Cap_MySql_ConnectionStringTemplate: Server=localhost;Database={0};Uid=root;Pwd=Password12! + Cap_PostgreSql_ConnectionStringTemplate: Server=localhost;Database={0};UserId=postgres;Password=Password12! services: - mssql2014 - mysql + - postgresql build_script: - ps: ./ConfigureMSDTC.ps1 - ps: ./build.ps1 diff --git a/build.sh b/build.sh index 91b52b0..e70f996 100644 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ dotnet --info dotnet restore -dotnet test test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj -f netcoreapp1.1 \ No newline at end of file +dotnet test test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj -f netcoreapp2.0 \ No newline at end of file diff --git a/build/common.props b/build/common.props index aa53f57..80c2d79 100644 --- a/build/common.props +++ b/build/common.props @@ -2,10 +2,6 @@ - - netstandard1.6 - - CAP savorboard;dotnetcore @@ -14,7 +10,7 @@ https://avatars2.githubusercontent.com/u/19404084 https://github.com/dotnetcore/CAP https://github.com/dotnetcore/CAP/blob/master/LICENSE.txt - eventbus;rabbitmq;kafka;cap;transaction; + CAP;EventBus;Distributed Transaction EventBus and eventually consistency in distributed architectures. diff --git a/build/util.cake b/build/util.cake index 4a18e87..db42eb7 100644 --- a/build/util.cake +++ b/build/util.cake @@ -20,6 +20,6 @@ Configuration: {Build.Configuration} public static string CreateStamp() { var seconds = (long)(DateTime.UtcNow - new DateTime(2017, 1, 1)).TotalSeconds; - return seconds.ToString().PadLeft(11, (char)'0'); + return seconds.ToString(); } } diff --git a/build/version.props b/build/version.props index 7a220b4..604d215 100644 --- a/build/version.props +++ b/build/version.props @@ -1,7 +1,7 @@ - 1 - 1 + 2 + 0 0 $(VersionMajor).$(VersionMinor).$(VersionPatch) diff --git a/samples/Sample.Kafka.SqlServer/AppDbContext.cs b/samples/Sample.Kafka.SqlServer/AppDbContext.cs deleted file mode 100644 index 53cecb7..0000000 --- a/samples/Sample.Kafka.SqlServer/AppDbContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Sample.Kafka -{ - public class AppDbContext : DbContext - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - //optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=Test;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True"); - optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Sample.Kafka.SqlServer;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True"); - } - } -} diff --git a/samples/Sample.Kafka.SqlServer/Controllers/ValuesController.cs b/samples/Sample.Kafka.SqlServer/Controllers/ValuesController.cs deleted file mode 100644 index 61b511a..0000000 --- a/samples/Sample.Kafka.SqlServer/Controllers/ValuesController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using DotNetCore.CAP; -using Microsoft.AspNetCore.Mvc; - -namespace Sample.Kafka.Controllers -{ - [Route("api/[controller]")] - public class ValuesController : Controller, ICapSubscribe - { - private readonly ICapPublisher _capBus; - private readonly AppDbContext _dbContext; - - public ValuesController(ICapPublisher producer, AppDbContext dbContext) - { - _capBus = producer; - _dbContext = dbContext; - } - - [Route("~/publish")] - public IActionResult PublishMessage() - { - _capBus.Publish("sample.rabbitmq.mysql", ""); - return Ok(); - } - - [Route("~/publishWithTrans")] - public async Task PublishMessageWithTransaction() - { - using (var trans = await _dbContext.Database.BeginTransactionAsync()) - { - await _capBus.PublishAsync("sample.rabbitmq.mysql", ""); - trans.Commit(); - } - return Ok(); - } - - [NonAction] - [CapSubscribe("sample.kafka.sqlserver", Group = "test")] - public void KafkaTest() - { - Console.WriteLine("[sample.kafka.sqlserver] message received"); - Debug.WriteLine("[sample.kafka.sqlserver] message received"); - } - } -} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.MySql/AppDbContext.cs b/samples/Sample.RabbitMQ.MySql/AppDbContext.cs index 5a60da7..cf3c96d 100644 --- a/samples/Sample.RabbitMQ.MySql/AppDbContext.cs +++ b/samples/Sample.RabbitMQ.MySql/AppDbContext.cs @@ -10,7 +10,8 @@ namespace Sample.RabbitMQ.MySql { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - optionsBuilder.UseMySql("Server=localhost;Database=Sample.RabbitMQ.MySql;Uid=root;Pwd=123123;"); + //optionsBuilder.UseMySql("Server=localhost;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;"); + optionsBuilder.UseMySql("Server=192.168.2.206;Database=Sample.RabbitMQ.MySql;UserId=root;Password=123123;"); } } } diff --git a/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs b/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs index eec782e..0138520 100644 --- a/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs +++ b/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs @@ -23,7 +23,16 @@ namespace Sample.RabbitMQ.MySql.Controllers [Route("~/publish")] public IActionResult PublishMessage() { - _capBus.Publish("sample.kafka.sqlserver", ""); + _capBus.Publish("sample.rabbitmq.mysql", DateTime.Now); + + return Ok(); + } + + + [Route("~/publish2")] + public IActionResult PublishMessage2() + { + _capBus.Publish("sample.kafka.sqlserver4", DateTime.Now); return Ok(); } @@ -34,6 +43,7 @@ namespace Sample.RabbitMQ.MySql.Controllers using (var trans = await _dbContext.Database.BeginTransactionAsync()) { await _capBus.PublishAsync("sample.kafka.sqlserver", ""); + trans.Commit(); } return Ok(); @@ -41,10 +51,9 @@ namespace Sample.RabbitMQ.MySql.Controllers [NonAction] [CapSubscribe("sample.rabbitmq.mysql")] - public void ReceiveMessage() + public void ReceiveMessage(DateTime time) { - Console.WriteLine("[sample.rabbitmq.mysql] message received"); - Debug.WriteLine("[sample.rabbitmq.mysql] message received"); + Console.WriteLine("[sample.rabbitmq.mysql] message received: "+ DateTime.Now.ToString() +" , sent time: " + time.ToString()); } } } diff --git a/samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj b/samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj index e90171f..1c67eb0 100644 --- a/samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj +++ b/samples/Sample.RabbitMQ.MySql/Sample.RabbitMQ.MySql.csproj @@ -1,27 +1,30 @@  - netcoreapp1.1 + netcoreapp2.0 + + + + 1701;1702;1705;3277; + NU1605;MSB3277 - - - - - - - - - + + + + + + + + - - + - + diff --git a/samples/Sample.RabbitMQ.MySql/Startup.cs b/samples/Sample.RabbitMQ.MySql/Startup.cs index 5a3d92f..19b1eac 100644 --- a/samples/Sample.RabbitMQ.MySql/Startup.cs +++ b/samples/Sample.RabbitMQ.MySql/Startup.cs @@ -18,7 +18,11 @@ namespace Sample.RabbitMQ.MySql services.AddCap(x => { x.UseEntityFramework(); - x.UseKafka("localhost:9092"); + x.UseRabbitMQ(y => { + y.HostName = "192.168.2.206"; + y.UserName = "admin"; + y.Password = "123123"; + }); }); services.AddMvc(); diff --git a/samples/Sample.RabbitMQ.PostgreSql/AppDbContext.cs b/samples/Sample.RabbitMQ.PostgreSql/AppDbContext.cs new file mode 100644 index 0000000..4fefe5c --- /dev/null +++ b/samples/Sample.RabbitMQ.PostgreSql/AppDbContext.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Sample.RabbitMQ.PostgreSql +{ + public class AppDbContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql("Server=localhost;Database=Sample.RabbitMQ.PostgreSql;UserId=postgre;Password=123123;"); + } + } +} diff --git a/samples/Sample.RabbitMQ.PostgreSql/Controllers/ValuesController.cs b/samples/Sample.RabbitMQ.PostgreSql/Controllers/ValuesController.cs new file mode 100644 index 0000000..10ea6d1 --- /dev/null +++ b/samples/Sample.RabbitMQ.PostgreSql/Controllers/ValuesController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using DotNetCore.CAP; +using Microsoft.AspNetCore.Mvc; + +namespace Sample.RabbitMQ.PostgreSql.Controllers +{ + [Route("api/[controller]")] + public class ValuesController : Controller + { + private readonly AppDbContext _dbContext; + private readonly ICapPublisher _capBus; + + public ValuesController(AppDbContext dbContext, ICapPublisher capPublisher) + { + _dbContext = dbContext; + _capBus = capPublisher; + } + + [Route("~/publish")] + public IActionResult PublishMessage() + { + _capBus.Publish("sample.rabbitmq.mysql", DateTime.Now); + + return Ok(); + } + + + [Route("~/publish2")] + public IActionResult PublishMessage2() + { + _capBus.Publish("sample.kafka.sqlserver4", DateTime.Now); + + return Ok(); + } + + [Route("~/publishWithTrans")] + public async Task PublishMessageWithTransaction() + { + using (var trans = await _dbContext.Database.BeginTransactionAsync()) + { + await _capBus.PublishAsync("sample.kafka.sqlserver", ""); + + trans.Commit(); + } + return Ok(); + } + + [NonAction] + [CapSubscribe("sample.rabbitmq.mysql")] + public void ReceiveMessage() + { + Console.WriteLine("[sample.rabbitmq.mysql] message received"); + Debug.WriteLine("[sample.rabbitmq.mysql] message received"); + } + } +} diff --git a/samples/Sample.RabbitMQ.PostgreSql/Program.cs b/samples/Sample.RabbitMQ.PostgreSql/Program.cs new file mode 100644 index 0000000..3cb0755 --- /dev/null +++ b/samples/Sample.RabbitMQ.PostgreSql/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Sample.RabbitMQ.PostgreSql +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/samples/Sample.RabbitMQ.PostgreSql/Sample.RabbitMQ.PostgreSql.csproj b/samples/Sample.RabbitMQ.PostgreSql/Sample.RabbitMQ.PostgreSql.csproj new file mode 100644 index 0000000..7dba0be --- /dev/null +++ b/samples/Sample.RabbitMQ.PostgreSql/Sample.RabbitMQ.PostgreSql.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + diff --git a/samples/Sample.RabbitMQ.PostgreSql/Startup.cs b/samples/Sample.RabbitMQ.PostgreSql/Startup.cs new file mode 100644 index 0000000..ac7ead0 --- /dev/null +++ b/samples/Sample.RabbitMQ.PostgreSql/Startup.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Sample.RabbitMQ.PostgreSql +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + app.UseMvc(); + } + } +} diff --git a/samples/Sample.RabbitMQ.SqlServer/AppDbContext.cs b/samples/Sample.RabbitMQ.SqlServer/AppDbContext.cs new file mode 100644 index 0000000..0607bb0 --- /dev/null +++ b/samples/Sample.RabbitMQ.SqlServer/AppDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sample.RabbitMQ.SqlServer +{ + public class AppDbContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlServer("Server=192.168.2.206;Initial Catalog=TestCap;User Id=cmswuliu;Password=h7xY81agBn*Veiu3;MultipleActiveResultSets=True"); + //optionsBuilder.UseSqlServer("Server=DESKTOP-M9R8T31;Initial Catalog=Sample.Kafka.SqlServer;User Id=sa;Password=P@ssw0rd;MultipleActiveResultSets=True"); + } + } +} diff --git a/samples/Sample.RabbitMQ.SqlServer/Controllers/ValuesController.cs b/samples/Sample.RabbitMQ.SqlServer/Controllers/ValuesController.cs new file mode 100644 index 0000000..09b1df3 --- /dev/null +++ b/samples/Sample.RabbitMQ.SqlServer/Controllers/ValuesController.cs @@ -0,0 +1,92 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using DotNetCore.CAP; +using Microsoft.AspNetCore.Mvc; + +namespace Sample.RabbitMQ.SqlServer.Controllers +{ + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + + public override string ToString() + { + return "Name:" + Name + ";Age:" + Age; + } + } + + + [Route("api/[controller]")] + public class ValuesController : Controller, ICapSubscribe + { + private readonly ICapPublisher _capBus; + private readonly AppDbContext _dbContext; + + public ValuesController(ICapPublisher producer, AppDbContext dbContext) + { + _capBus = producer; + _dbContext = dbContext; + } + + [Route("~/publish")] + public IActionResult PublishMessage() + { + using(var trans = _dbContext.Database.BeginTransaction()) + { + //_capBus.Publish("sample.rabbitmq.mysql22222", DateTime.Now); + _capBus.Publish("sample.rabbitmq.mysql33333", new Person { Name = "宜兴", Age = 11 }); + trans.Commit(); + } + return Ok(); + } + + [Route("~/publishWithTrans")] + public async Task PublishMessageWithTransaction() + { + using (var trans = await _dbContext.Database.BeginTransactionAsync()) + { + await _capBus.PublishAsync("sample.rabbitmq.mysql", ""); + + trans.Commit(); + } + return Ok(); + } + + [CapSubscribe("sample.rabbitmq.mysql33333")] + public void KafkaTest22(Person person) + { + var aa = _dbContext.Database; + + _dbContext.Dispose(); + + Console.WriteLine("[sample.kafka.sqlserver] message received " + person.ToString()); + Debug.WriteLine("[sample.kafka.sqlserver] message received " + person.ToString()); + } + + //[CapSubscribe("sample.rabbitmq.mysql22222")] + //public void KafkaTest22(DateTime time) + //{ + // Console.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString()); + // Debug.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString()); + //} + + [CapSubscribe("sample.rabbitmq.mysql22222")] + public async Task KafkaTest33(DateTime time) + { + Console.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString()); + Debug.WriteLine("[sample.kafka.sqlserver] message received " + time.ToString()); + return await Task.FromResult(time); + } + + [NonAction] + [CapSubscribe("sample.kafka.sqlserver3")] + [CapSubscribe("sample.kafka.sqlserver4")] + public void KafkaTest() + { + Console.WriteLine("[sample.kafka.sqlserver] message received"); + Debug.WriteLine("[sample.kafka.sqlserver] message received"); + } + } +} \ No newline at end of file diff --git a/samples/Sample.Kafka.SqlServer/Program.cs b/samples/Sample.RabbitMQ.SqlServer/Program.cs similarity index 95% rename from samples/Sample.Kafka.SqlServer/Program.cs rename to samples/Sample.RabbitMQ.SqlServer/Program.cs index 37d3089..2393f73 100644 --- a/samples/Sample.Kafka.SqlServer/Program.cs +++ b/samples/Sample.RabbitMQ.SqlServer/Program.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; -namespace Sample.Kafka +namespace Sample.RabbitMQ.SqlServer { public class Program { diff --git a/samples/Sample.Kafka.SqlServer/Sample.Kafka.SqlServer.csproj b/samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj similarity index 60% rename from samples/Sample.Kafka.SqlServer/Sample.Kafka.SqlServer.csproj rename to samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj index 675a95b..2f9fdaf 100644 --- a/samples/Sample.Kafka.SqlServer/Sample.Kafka.SqlServer.csproj +++ b/samples/Sample.RabbitMQ.SqlServer/Sample.RabbitMQ.SqlServer.csproj @@ -1,26 +1,25 @@  - netcoreapp1.1 - Sample.Kafka.SqlServer + netcoreapp2.0 + Sample.RabbitMQ.SqlServer - - - - - - - - + + + + + + + + - - + - + diff --git a/samples/Sample.Kafka.SqlServer/Startup.cs b/samples/Sample.RabbitMQ.SqlServer/Startup.cs similarity index 76% rename from samples/Sample.Kafka.SqlServer/Startup.cs rename to samples/Sample.RabbitMQ.SqlServer/Startup.cs index 08291c5..e6819ea 100644 --- a/samples/Sample.Kafka.SqlServer/Startup.cs +++ b/samples/Sample.RabbitMQ.SqlServer/Startup.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Sample.Kafka +namespace Sample.RabbitMQ.SqlServer { public class Startup { @@ -14,7 +14,11 @@ namespace Sample.Kafka services.AddCap(x => { x.UseEntityFramework(); - x.UseKafka("localhost:9092"); + x.UseRabbitMQ(y=> { + y.HostName = "192.168.2.206"; + y.UserName = "admin"; + y.Password = "123123"; + }); }); services.AddMvc(); diff --git a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs index e61ccee..0790458 100644 --- a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs +++ b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs @@ -42,7 +42,7 @@ namespace DotNetCore.CAP { throw new ArgumentNullException(nameof(Servers)); } - + MainConfig.Add("bootstrap.servers", Servers); MainConfig["queue.buffering.max.ms"] = "10"; diff --git a/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj b/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj index 0a8fe64..96f0191 100644 --- a/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj +++ b/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj @@ -3,16 +3,16 @@ - netstandard1.6 - 1.6.1 + netstandard2.0 DotNetCore.CAP.Kafka - DotNetCore.CAP.Kafka - $(PackageTargetFallback);dnxcore50 - false - false - false + $(PackageTags);Kafka - + + + NU1605 + NU1701 + + diff --git a/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs b/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs index ea2ee67..8bda50a 100644 --- a/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs +++ b/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs @@ -1,7 +1,4 @@ -using System; -using Microsoft.Extensions.Options; - -namespace DotNetCore.CAP.Kafka +namespace DotNetCore.CAP.Kafka { internal sealed class KafkaConsumerClientFactory : IConsumerClientFactory { diff --git a/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs b/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs index a3f4c80..8b455d7 100644 --- a/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs +++ b/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs @@ -12,13 +12,15 @@ namespace DotNetCore.CAP.Kafka private readonly ILogger _logger; private readonly KafkaOptions _kafkaOptions; - public PublishQueueExecutor(IStateChanger stateChanger, - KafkaOptions options, + public PublishQueueExecutor( + CapOptions options, + IStateChanger stateChanger, + KafkaOptions kafkaOptions, ILogger logger) - : base(stateChanger, logger) + : base(options, stateChanger, logger) { _logger = logger; - _kafkaOptions = options; + _kafkaOptions = kafkaOptions; } public override Task PublishAsync(string keyName, string content) diff --git a/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs b/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs index 5682e38..b9c1b0d 100644 --- a/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs @@ -21,6 +21,7 @@ namespace DotNetCore.CAP services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddTransient(); var mysqlOptions = new MySqlOptions(); @@ -28,17 +29,17 @@ namespace DotNetCore.CAP if (mysqlOptions.DbContextType != null) { - var provider = TempBuildService(services); - var dbContextObj = provider.GetService(mysqlOptions.DbContextType); - var dbContext = (DbContext)dbContextObj; - mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + services.AddSingleton(x => + { + var dbContext = (DbContext)x.GetService(mysqlOptions.DbContextType); + mysqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + return mysqlOptions; + }); + } + else + { + services.AddSingleton(mysqlOptions); } - services.AddSingleton(mysqlOptions); - } - - private IServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.MySql/CapPublisher.cs b/src/DotNetCore.CAP.MySql/CapPublisher.cs index 82476b3..0d6a1b3 100644 --- a/src/DotNetCore.CAP.MySql/CapPublisher.cs +++ b/src/DotNetCore.CAP.MySql/CapPublisher.cs @@ -2,34 +2,28 @@ using System.Data; using System.Threading.Tasks; using Dapper; -using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Models; -using DotNetCore.CAP.Processor; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; +using MySql.Data.MySqlClient; namespace DotNetCore.CAP.MySql { - public class CapPublisher : ICapPublisher + public class CapPublisher : CapPublisherBase, ICallbackPublisher { private readonly ILogger _logger; private readonly MySqlOptions _options; private readonly DbContext _dbContext; - protected bool IsCapOpenedTrans { get; set; } - - protected bool IsUsingEF { get; } - - protected IServiceProvider ServiceProvider { get; } - public CapPublisher(IServiceProvider provider, ILogger logger, MySqlOptions options) { ServiceProvider = provider; - _logger = logger; _options = options; + _logger = logger; if (_options.DbContextType != null) { @@ -38,160 +32,45 @@ namespace DotNetCore.CAP.MySql } } - public void Publish(string name, T contentObj) - { - CheckIsUsingEF(name); - - var content = Serialize(contentObj); - - PublishCore(name, content); - } - - public Task PublishAsync(string name, T contentObj) - { - CheckIsUsingEF(name); - - var content = Serialize(contentObj); - - return PublishCoreAsync(name, content); - } - - public void Publish(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) - { - CheckIsAdoNet(name); - - PrepareConnection(dbConnection, ref dbTransaction); - - var content = Serialize(contentObj); - - PublishWithTrans(name, content, dbConnection, dbTransaction); - } - - public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) - { - CheckIsAdoNet(name); - - PrepareConnection(dbConnection, ref dbTransaction); - - var content = Serialize(contentObj); - - return PublishWithTransAsync(name, content, dbConnection, dbTransaction); - } - - #region private methods - - private string Serialize(T obj) - { - string content = string.Empty; - if (Helper.IsComplexType(typeof(T))) - { - content = Helper.ToJson(obj); - } - else - { - content = obj?.ToString(); - } - return content; - } - - private void PrepareConnection(IDbConnection dbConnection, ref IDbTransaction dbTransaction) + protected override void PrepareConnectionForEF() { - if (dbConnection == null) - throw new ArgumentNullException(nameof(dbConnection)); - - if (dbConnection.State != ConnectionState.Open) - dbConnection.Open(); - - if (dbTransaction == null) + DbConnection = _dbContext.Database.GetDbConnection(); + var dbContextTransaction = _dbContext.Database.CurrentTransaction; + var dbTrans = dbContextTransaction?.GetDbTransaction(); + //DbTransaction is dispose in original + if (dbTrans?.Connection == null) { IsCapOpenedTrans = true; - dbTransaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + dbContextTransaction?.Dispose(); + dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbTrans = dbContextTransaction.GetDbTransaction(); } + DbTranasaction = dbTrans; } - private void CheckIsUsingEF(string name) + protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (!IsUsingEF) - throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + - " otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); - } + dbConnection.Execute(PrepareSql(), message, dbTransaction); - private void CheckIsAdoNet(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (IsUsingEF) - throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); } - private async Task PublishCoreAsync(string name, string content) + protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) { - var connection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) - { - IsCapOpenedTrans = true; - transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); - } - var dbTransaction = transaction.GetDbTransaction(); - await PublishWithTransAsync(name, content, connection, dbTransaction); - } + await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction); - private void PublishCore(string name, string content) - { - var connection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) - { - IsCapOpenedTrans = true; - transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); - } - var dbTransaction = transaction.GetDbTransaction(); - PublishWithTrans(name, content, connection, dbTransaction); + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); } - private async Task PublishWithTransAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + public async Task PublishAsync(CapPublishedMessage message) { - var message = new CapPublishedMessage - { - Name = name, - Content = content, - StatusName = StatusName.Scheduled - }; - await dbConnection.ExecuteAsync(PrepareSql(), message, transaction: dbTransaction); - - _logger.LogInformation("Message has been persisted in the database. name:" + name); - - if (IsCapOpenedTrans) + using (var conn = new MySqlConnection(_options.ConnectionString)) { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); + await conn.ExecuteAsync(PrepareSql(), message); } - - PublishQueuer.PulseEvent.Set(); } - private void PublishWithTrans(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) - { - var message = new CapPublishedMessage - { - Name = name, - Content = content, - StatusName = StatusName.Scheduled - }; - var count = dbConnection.Execute(PrepareSql(), message, transaction: dbTransaction); - - _logger.LogInformation("Message has been persisted in the database. name:" + name); - - if (IsCapOpenedTrans) - { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); - } - PublishQueuer.PulseEvent.Set(); - } + #region private methods private string PrepareSql() { diff --git a/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj b/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj index f9f2c51..00316dd 100644 --- a/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj +++ b/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj @@ -3,21 +3,16 @@ - netstandard1.6 + netstandard2.0 DotNetCore.CAP.MySql - DotNetCore.CAP.MySql - 1.6.1 - $(PackageTargetFallback);dnxcore50 - false - false - false + $(PackageTags);MySQL - - - + + + diff --git a/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs index b8e5922..3398dc0 100644 --- a/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs +++ b/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs @@ -15,7 +15,7 @@ namespace DotNetCore.CAP.MySql private const int MaxBatch = 1000; private readonly TimeSpan _delay = TimeSpan.FromSeconds(1); - private readonly TimeSpan _waitingInterval = TimeSpan.FromHours(2); + private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5); public DefaultAdditionalProcessor( IServiceProvider provider, diff --git a/src/DotNetCore.CAP.PostgreSql/CAP.EFOptions.cs b/src/DotNetCore.CAP.PostgreSql/CAP.EFOptions.cs new file mode 100644 index 0000000..12fc9bf --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/CAP.EFOptions.cs @@ -0,0 +1,18 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class EFOptions + { + public const string DefaultSchema = "cap"; + + /// + /// Gets or sets the schema to use when creating database objects. + /// Default is . + /// + public string Schema { get; set; } = DefaultSchema; + + internal Type DbContextType { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/CAP.Options.Extensions.cs b/src/DotNetCore.CAP.PostgreSql/CAP.Options.Extensions.cs new file mode 100644 index 0000000..1505f02 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/CAP.Options.Extensions.cs @@ -0,0 +1,49 @@ +using System; +using DotNetCore.CAP; +using Microsoft.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static class CapOptionsExtensions + { + public static CapOptions UsePostgreSql(this CapOptions options, string connectionString) + { + return options.UsePostgreSql(opt => + { + opt.ConnectionString = connectionString; + }); + } + + public static CapOptions UsePostgreSql(this CapOptions options, Action configure) + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + options.RegisterExtension(new PostgreSqlCapOptionsExtension(configure)); + + return options; + } + + public static CapOptions UseEntityFramework(this CapOptions options) + where TContext : DbContext + { + return options.UseEntityFramework(opt => + { + opt.DbContextType = typeof(TContext); + }); + } + + public static CapOptions UseEntityFramework(this CapOptions options, Action configure) + where TContext : DbContext + { + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + var efOptions = new EFOptions { DbContextType = typeof(TContext) }; + configure(efOptions); + + options.RegisterExtension(new PostgreSqlCapOptionsExtension(configure)); + + return options; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs new file mode 100644 index 0000000..e4381e0 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs @@ -0,0 +1,45 @@ +using System; +using DotNetCore.CAP.PostgreSql; +using DotNetCore.CAP.Processor; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + internal class PostgreSqlCapOptionsExtension : ICapOptionsExtension + { + private readonly Action _configure; + + public PostgreSqlCapOptionsExtension(Action configure) + { + _configure = configure; + } + + public void AddServices(IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddTransient(); + + var postgreSqlOptions = new PostgreSqlOptions(); + _configure(postgreSqlOptions); + + if (postgreSqlOptions.DbContextType != null) + { + services.AddSingleton(x => + { + var dbContext = (DbContext)x.GetService(postgreSqlOptions.DbContextType); + postgreSqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + return postgreSqlOptions; + }); + } + else + { + services.AddSingleton(postgreSqlOptions); + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlOptions.cs b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlOptions.cs new file mode 100644 index 0000000..9a95464 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlOptions.cs @@ -0,0 +1,11 @@ +// ReSharper disable once CheckNamespace +namespace DotNetCore.CAP +{ + public class PostgreSqlOptions : EFOptions + { + /// + /// Gets or sets the database's connection string that will be used to store database entities. + /// + public string ConnectionString { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs b/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs new file mode 100644 index 0000000..0f4e6c9 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs @@ -0,0 +1,82 @@ +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 ILogger _logger; + private readonly PostgreSqlOptions _options; + private readonly DbContext _dbContext; + + public CapPublisher(IServiceProvider provider, + ILogger logger, + PostgreSqlOptions options) + { + ServiceProvider = provider; + _options = options; + _logger = logger; + + if (_options.DbContextType != null) + { + IsUsingEF = true; + _dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType); + } + } + + 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(); + } + DbTranasaction = dbTrans; + } + + protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) + { + dbConnection.Execute(PrepareSql(), message, dbTransaction); + + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); + } + + protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) + { + await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction); + + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); + } + + public async Task PublishAsync(CapPublishedMessage message) + { + using (var conn = new NpgsqlConnection(_options.ConnectionString)) + { + await conn.ExecuteAsync(PrepareSql(), message); + } + } + + #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)"; + } + + #endregion private methods + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj b/src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj new file mode 100644 index 0000000..d3884ce --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj @@ -0,0 +1,22 @@ + + + + + + netstandard2.0 + DotNetCore.CAP.PostgreSql + $(PackageTags);PostgreSQL + + + + + + + + + + + + + + diff --git a/src/DotNetCore.CAP.PostgreSql/FetchedMessage.cs b/src/DotNetCore.CAP.PostgreSql/FetchedMessage.cs new file mode 100644 index 0000000..4abd0f3 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/FetchedMessage.cs @@ -0,0 +1,11 @@ +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.PostgreSql +{ + internal class FetchedMessage + { + public int MessageId { get; set; } + + public MessageType MessageType { get; set; } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs new file mode 100644 index 0000000..1c79f60 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Processor; +using Microsoft.Extensions.Logging; +using Npgsql; + +namespace DotNetCore.CAP.PostgreSql +{ + internal class DefaultAdditionalProcessor : IAdditionalProcessor + { + private readonly IServiceProvider _provider; + private readonly ILogger _logger; + private readonly PostgreSqlOptions _options; + + private const int MaxBatch = 1000; + private readonly TimeSpan _delay = TimeSpan.FromSeconds(1); + private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5); + + private static readonly string[] Tables = + { + "published","received" + }; + + public DefaultAdditionalProcessor( + IServiceProvider provider, + ILogger logger, + PostgreSqlOptions sqlServerOptions) + { + _logger = logger; + _provider = provider; + _options = sqlServerOptions; + } + + public async Task ProcessAsync(ProcessingContext context) + { + _logger.LogDebug("Collecting expired entities."); + + foreach (var table in Tables) + { + var removedCount = 0; + do + { + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + removedCount = await connection.ExecuteAsync($"DELETE FROM \"{_options.Schema}\".\"{table}\" WHERE \"ExpiresAt\" < @now AND \"Id\" IN (SELECT \"Id\" FROM \"{_options.Schema}\".\"{table}\" LIMIT @count);", + new { now = DateTime.Now, count = MaxBatch }); + } + + if (removedCount != 0) + { + await context.WaitAsync(_delay); + context.ThrowIfStopping(); + } + } while (removedCount != 0); + } + + await context.WaitAsync(_waitingInterval); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/PostgreSqlFetchedMessage.cs b/src/DotNetCore.CAP.PostgreSql/PostgreSqlFetchedMessage.cs new file mode 100644 index 0000000..7189595 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/PostgreSqlFetchedMessage.cs @@ -0,0 +1,74 @@ +using System; +using System.Data; +using System.Threading; +using Dapper; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP.PostgreSql +{ + public class PostgreSqlFetchedMessage : IFetchedMessage + { + private readonly IDbConnection _connection; + private readonly IDbTransaction _transaction; + private readonly Timer _timer; + private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromMinutes(1); + private readonly object _lockObject = new object(); + + public PostgreSqlFetchedMessage(int messageId, + MessageType type, + IDbConnection connection, + IDbTransaction transaction) + { + MessageId = messageId; + MessageType = type; + _connection = connection; + _transaction = transaction; + _timer = new Timer(ExecuteKeepAliveQuery, null, KeepAliveInterval, KeepAliveInterval); + } + + public int MessageId { get; } + + public MessageType MessageType { get; } + + public void RemoveFromQueue() + { + lock (_lockObject) + { + _transaction.Commit(); + } + } + + public void Requeue() + { + lock (_lockObject) + { + _transaction.Rollback(); + } + } + + public void Dispose() + { + lock (_lockObject) + { + _timer?.Dispose(); + _transaction.Dispose(); + _connection.Dispose(); + } + } + + private void ExecuteKeepAliveQuery(object obj) + { + lock (_lockObject) + { + try + { + _connection?.Execute("SELECT 1", _transaction); + } + catch + { + // ignored + } + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorage.cs b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorage.cs new file mode 100644 index 0000000..2704780 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorage.cs @@ -0,0 +1,67 @@ +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Npgsql; + +namespace DotNetCore.CAP.PostgreSql +{ + public class PostgreSqlStorage : IStorage + { + private readonly PostgreSqlOptions _options; + private readonly ILogger _logger; + + public PostgreSqlStorage(ILogger logger, PostgreSqlOptions options) + { + _options = options; + _logger = logger; + } + + public async Task InitializeAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) return; + + var sql = CreateDbTablesScript(_options.Schema); + + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + await connection.ExecuteAsync(sql); + } + _logger.LogDebug("Ensuring all create database tables script are applied."); + } + + protected virtual string CreateDbTablesScript(string schema) + { + var batchSql = $@" +CREATE SCHEMA IF NOT EXISTS ""{schema}""; + +CREATE TABLE IF NOT EXISTS ""{schema}"".""queue""( + ""MessageId"" int NOT NULL , + ""MessageType"" int NOT NULL +); + +CREATE TABLE IF NOT EXISTS ""{schema}"".""received""( + ""Id"" SERIAL PRIMARY KEY NOT NULL, + ""Name"" VARCHAR(200) NOT NULL, + ""Group"" VARCHAR(200) NULL, + ""Content"" TEXT NULL, + ""Retries"" INT NOT NULL, + ""Added"" TIMESTAMP NOT NULL, + ""ExpiresAt"" TIMESTAMP NULL, + ""StatusName"" VARCHAR(50) NOT NULL +); + +CREATE TABLE IF NOT EXISTS ""{schema}"".""published""( + ""Id"" SERIAL PRIMARY KEY NOT NULL, + ""Name"" VARCHAR(200) NOT NULL, + ""Content"" TEXT NULL, + ""Retries"" INT NOT NULL, + ""Added"" TIMESTAMP NOT NULL, + ""ExpiresAt"" TIMESTAMP NULL, + ""StatusName"" VARCHAR(50) NOT NULL +);"; + return batchSql; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageConnection.cs b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageConnection.cs new file mode 100644 index 0000000..3ddd3ab --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageConnection.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using Npgsql; + +namespace DotNetCore.CAP.PostgreSql +{ + public class PostgreSqlStorageConnection : IStorageConnection + { + private readonly PostgreSqlOptions _options; + + public PostgreSqlStorageConnection(PostgreSqlOptions options) + { + _options = options; + } + + public PostgreSqlOptions Options => _options; + + public IStorageTransaction CreateTransaction() + { + return new PostgreSqlStorageTransaction(this); + } + + public async Task GetPublishedMessageAsync(int id) + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"Id\"={id}"; + + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public Task FetchNextMessageAsync() + { + var sql = $@"DELETE FROM ""{_options.Schema}"".""queue"" WHERE ""MessageId"" = (SELECT ""MessageId"" FROM ""{_options.Schema}"".""queue"" FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *;"; + return FetchNextMessageCoreAsync(sql); + } + + public async Task GetNextPublishedMessageToBeEnqueuedAsync() + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;"; + + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public async Task> GetFailedPublishedMessages() + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;"; + + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryAsync(sql); + } + } + + // CapReceviedMessage + + public async Task StoreReceivedMessageAsync(CapReceivedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $"INSERT INTO \"{_options.Schema}\".\"received\"(\"Name\",\"Group\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; + + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + await connection.ExecuteAsync(sql, message); + } + } + + public async Task GetReceivedMessageAsync(int id) + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"Id\"={id}"; + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public async Task GetNextReceviedMessageToBeEnqueuedAsync() + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\" = '{StatusName.Scheduled}' FOR UPDATE SKIP LOCKED LIMIT 1;"; + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryFirstOrDefaultAsync(sql); + } + } + + public async Task> GetFailedReceviedMessages() + { + var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\"='{StatusName.Failed}' LIMIT 1000;"; + using (var connection = new NpgsqlConnection(_options.ConnectionString)) + { + return await connection.QueryAsync(sql); + } + } + + public void Dispose() + { + } + + private async Task FetchNextMessageCoreAsync(string sql, object args = null) + { + //here don't use `using` to dispose + var connection = new NpgsqlConnection(_options.ConnectionString); + await connection.OpenAsync(); + var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); + FetchedMessage fetchedMessage = null; + try + { + fetchedMessage = await connection.QueryFirstOrDefaultAsync(sql, args, transaction); + } + catch (NpgsqlException) + { + transaction.Dispose(); + throw; + } + + if (fetchedMessage == null) + { + transaction.Rollback(); + transaction.Dispose(); + connection.Dispose(); + return null; + } + + return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageTransaction.cs b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageTransaction.cs new file mode 100644 index 0000000..ac106d4 --- /dev/null +++ b/src/DotNetCore.CAP.PostgreSql/PostgreSqlStorageTransaction.cs @@ -0,0 +1,71 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Models; +using Npgsql; + +namespace DotNetCore.CAP.PostgreSql +{ + public class PostgreSqlStorageTransaction : IStorageTransaction, IDisposable + { + private readonly string _schema; + + private readonly IDbTransaction _dbTransaction; + private readonly IDbConnection _dbConnection; + + public PostgreSqlStorageTransaction(PostgreSqlStorageConnection connection) + { + var options = connection.Options; + _schema = options.Schema; + + _dbConnection = new NpgsqlConnection(options.ConnectionString); + _dbConnection.Open(); + _dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + } + + public void UpdateMessage(CapPublishedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $@"UPDATE ""{_schema}"".""published"" SET ""Retries""=@Retries,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;"; + _dbConnection.Execute(sql, message, _dbTransaction); + } + + public void UpdateMessage(CapReceivedMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + + var sql = $@"UPDATE ""{_schema}"".""received"" SET ""Retries""=@Retries,""ExpiresAt""=@ExpiresAt,""StatusName""=@StatusName WHERE ""Id""=@Id;"; + _dbConnection.Execute(sql, message, _dbTransaction); + } + + 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); + } + + public Task CommitAsync() + { + _dbTransaction.Commit(); + return Task.CompletedTask; + } + + public void Dispose() + { + _dbTransaction.Dispose(); + _dbConnection.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs index 4202570..22e2af2 100644 --- a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs @@ -21,6 +21,10 @@ namespace DotNetCore.CAP services.AddSingleton(options); services.AddSingleton(); + + services.AddSingleton(); + services.AddScoped(x => x.GetService().Rent()); + services.AddTransient(); } } diff --git a/src/DotNetCore.CAP.RabbitMQ/ConnectionPool.cs b/src/DotNetCore.CAP.RabbitMQ/ConnectionPool.cs new file mode 100644 index 0000000..83116bf --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/ConnectionPool.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using RabbitMQ.Client; + +namespace DotNetCore.CAP.RabbitMQ +{ + public class ConnectionPool : IConnectionPool, IDisposable + { + private const int DefaultPoolSize = 32; + + private readonly ConcurrentQueue _pool = new ConcurrentQueue(); + + private readonly Func _activator; + + private int _maxSize; + private int _count; + + public ConnectionPool(RabbitMQOptions options) + { + _maxSize = DefaultPoolSize; + + _activator = CreateActivator(options); + } + + private static Func CreateActivator(RabbitMQOptions options) + { + var factory = new ConnectionFactory() + { + HostName = options.HostName, + UserName = options.UserName, + Port = options.Port, + Password = options.Password, + VirtualHost = options.VirtualHost, + RequestedConnectionTimeout = options.RequestedConnectionTimeout, + SocketReadTimeout = options.SocketReadTimeout, + SocketWriteTimeout = options.SocketWriteTimeout + }; + + return () => factory.CreateConnection(); + } + + public virtual IConnection Rent() + { + if (_pool.TryDequeue(out IConnection connection)) + { + Interlocked.Decrement(ref _count); + + Debug.Assert(_count >= 0); + + return connection; + } + + connection = _activator(); + + return connection; + } + + public virtual bool Return(IConnection connection) + { + if (Interlocked.Increment(ref _count) <= _maxSize) + { + _pool.Enqueue(connection); + + return true; + } + + Interlocked.Decrement(ref _count); + + Debug.Assert(_maxSize == 0 || _pool.Count <= _maxSize); + + return false; + } + + IConnection IConnectionPool.Rent() => Rent(); + + bool IConnectionPool.Return(IConnection connection) => Return(connection); + + public void Dispose() + { + _maxSize = 0; + + IConnection context; + while (_pool.TryDequeue(out context)) + { + context.Dispose(); + } + } + } +} diff --git a/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj b/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj index 3a758b2..55a0deb 100644 --- a/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj +++ b/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj @@ -3,8 +3,9 @@ - netstandard1.6 - 1.6.1 + netstandard2.0 + DotNetCore.CAP.RabbitMQ + $(PackageTags);RabbitMQ diff --git a/src/DotNetCore.CAP.RabbitMQ/IConnectionPool.cs b/src/DotNetCore.CAP.RabbitMQ/IConnectionPool.cs new file mode 100644 index 0000000..9097f28 --- /dev/null +++ b/src/DotNetCore.CAP.RabbitMQ/IConnectionPool.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using RabbitMQ.Client; + +namespace DotNetCore.CAP.RabbitMQ +{ + public interface IConnectionPool + { + IConnection Rent(); + + bool Return(IConnection context); + } +} diff --git a/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs b/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs index 27b777b..791985d 100644 --- a/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs +++ b/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs @@ -3,7 +3,6 @@ using System.Text; using System.Threading.Tasks; using DotNetCore.CAP.Processor.States; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using RabbitMQ.Client; namespace DotNetCore.CAP.RabbitMQ @@ -11,35 +10,27 @@ namespace DotNetCore.CAP.RabbitMQ internal sealed class PublishQueueExecutor : BasePublishQueueExecutor { private readonly ILogger _logger; + private readonly IConnection _connection; private readonly RabbitMQOptions _rabbitMQOptions; - public PublishQueueExecutor(IStateChanger stateChanger, - RabbitMQOptions options, + public PublishQueueExecutor( + CapOptions options, + IStateChanger stateChanger, + IConnection connection, + RabbitMQOptions rabbitMQOptions, ILogger logger) - : base(stateChanger, logger) + : base(options, stateChanger, logger) { _logger = logger; - _rabbitMQOptions = options; + _connection = connection; + _rabbitMQOptions = rabbitMQOptions; } public override Task PublishAsync(string keyName, string content) { - var factory = new ConnectionFactory() - { - HostName = _rabbitMQOptions.HostName, - UserName = _rabbitMQOptions.UserName, - Port = _rabbitMQOptions.Port, - Password = _rabbitMQOptions.Password, - VirtualHost = _rabbitMQOptions.VirtualHost, - RequestedConnectionTimeout = _rabbitMQOptions.RequestedConnectionTimeout, - SocketReadTimeout = _rabbitMQOptions.SocketReadTimeout, - SocketWriteTimeout = _rabbitMQOptions.SocketWriteTimeout - }; - try { - using (var connection = factory.CreateConnection()) - using (var channel = connection.CreateModel()) + using (var channel = _connection.CreateModel()) { var body = Encoding.UTF8.GetBytes(content); diff --git a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs index 11d888a..a6720fd 100644 --- a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs +++ b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs @@ -14,7 +14,6 @@ namespace DotNetCore.CAP.RabbitMQ private readonly string _queueName; private readonly RabbitMQOptions _rabbitMQOptions; - private IConnectionFactory _connectionFactory; private IConnection _connection; private IModel _channel; private ulong _deliveryTag; @@ -23,9 +22,12 @@ namespace DotNetCore.CAP.RabbitMQ public event EventHandler OnError; - public RabbitMQConsumerClient(string queueName, RabbitMQOptions options) + public RabbitMQConsumerClient(string queueName, + IConnection connection, + RabbitMQOptions options) { _queueName = queueName; + _connection = connection; _rabbitMQOptions = options; _exchageName = options.TopicExchangeName; @@ -34,19 +36,6 @@ namespace DotNetCore.CAP.RabbitMQ private void InitClient() { - _connectionFactory = new ConnectionFactory() - { - HostName = _rabbitMQOptions.HostName, - UserName = _rabbitMQOptions.UserName, - Port = _rabbitMQOptions.Port, - Password = _rabbitMQOptions.Password, - VirtualHost = _rabbitMQOptions.VirtualHost, - RequestedConnectionTimeout = _rabbitMQOptions.RequestedConnectionTimeout, - SocketReadTimeout = _rabbitMQOptions.SocketReadTimeout, - SocketWriteTimeout = _rabbitMQOptions.SocketWriteTimeout - }; - - _connection = _connectionFactory.CreateConnection(); _channel = _connection.CreateModel(); _channel.ExchangeDeclare( diff --git a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs index fcd267d..5fc9d8f 100644 --- a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs +++ b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs @@ -1,19 +1,23 @@ using Microsoft.Extensions.Options; +using RabbitMQ.Client; namespace DotNetCore.CAP.RabbitMQ { internal sealed class RabbitMQConsumerClientFactory : IConsumerClientFactory { private readonly RabbitMQOptions _rabbitMQOptions; + private readonly IConnection _connection; - public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions) + + public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions, IConnection connection) { _rabbitMQOptions = rabbitMQOptions; + _connection = connection; } public IConsumerClient Create(string groupId) { - return new RabbitMQConsumerClient(groupId, _rabbitMQOptions); + return new RabbitMQConsumerClient(groupId, _connection, _rabbitMQOptions); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs index 2d1878e..3053d76 100644 --- a/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs @@ -21,24 +21,26 @@ namespace DotNetCore.CAP services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddTransient(); var sqlServerOptions = new SqlServerOptions(); + _configure(sqlServerOptions); if (sqlServerOptions.DbContextType != null) { - var provider = TempBuildService(services); - var dbContextObj = provider.GetService(sqlServerOptions.DbContextType); - var dbContext = (DbContext)dbContextObj; - sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + services.AddSingleton(x => + { + var dbContext = (DbContext)x.GetService(sqlServerOptions.DbContextType); + sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + return sqlServerOptions; + }); + } + else + { + services.AddSingleton(sqlServerOptions); } - services.AddSingleton(sqlServerOptions); - } - - private IServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CapPublisher.cs b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs index 674b7f7..18c17c2 100644 --- a/src/DotNetCore.CAP.SqlServer/CapPublisher.cs +++ b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs @@ -1,28 +1,22 @@ using System; using System.Data; +using System.Data.SqlClient; using System.Threading.Tasks; using Dapper; -using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Abstractions; using DotNetCore.CAP.Models; -using DotNetCore.CAP.Processor; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; namespace DotNetCore.CAP.SqlServer { - public class CapPublisher : ICapPublisher + public class CapPublisher : CapPublisherBase, ICallbackPublisher { private readonly ILogger _logger; private readonly SqlServerOptions _options; private readonly DbContext _dbContext; - protected bool IsCapOpenedTrans { get; set; } - - protected bool IsUsingEF { get; } - - protected IServiceProvider ServiceProvider { get; } - public CapPublisher(IServiceProvider provider, ILogger logger, SqlServerOptions options) @@ -38,158 +32,45 @@ namespace DotNetCore.CAP.SqlServer } } - public void Publish(string name, T contentObj) - { - CheckIsUsingEF(name); - - var content = Serialize(contentObj); - - PublishCore(name, content); - } - - public Task PublishAsync(string name, T contentObj) - { - CheckIsUsingEF(name); - - var content = Serialize(contentObj); - - return PublishCoreAsync(name, content); - } - - public void Publish(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) - { - CheckIsAdoNet(name); - PrepareConnection(dbConnection, ref dbTransaction); - - var content = Serialize(contentObj); - - PublishWithTrans(name, content, dbConnection, dbTransaction); - } - - public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) - { - CheckIsAdoNet(name); - PrepareConnection(dbConnection, ref dbTransaction); - - var content = Serialize(contentObj); - - return PublishWithTransAsync(name, content, dbConnection, dbTransaction); - } - - #region private methods - - private string Serialize(T obj) - { - string content = string.Empty; - if (Helper.IsComplexType(typeof(T))) - { - content = Helper.ToJson(obj); - } - else - { - content = obj.ToString(); - } - return content; - } - - private void PrepareConnection(IDbConnection dbConnection, ref IDbTransaction dbTransaction) + protected override void PrepareConnectionForEF() { - if (dbConnection == null) - throw new ArgumentNullException(nameof(dbConnection)); - - if (dbConnection.State != ConnectionState.Open) - dbConnection.Open(); - - if (dbTransaction == null) + DbConnection = _dbContext.Database.GetDbConnection(); + var dbContextTransaction = _dbContext.Database.CurrentTransaction; + var dbTrans = dbContextTransaction?.GetDbTransaction(); + //DbTransaction is dispose in original + if (dbTrans?.Connection == null) { IsCapOpenedTrans = true; - dbTransaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + dbContextTransaction?.Dispose(); + dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbTrans = dbContextTransaction.GetDbTransaction(); } + DbTranasaction = dbTrans; } - private void CheckIsUsingEF(string name) + protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (!IsUsingEF) - throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + - " otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); - } + dbConnection.Execute(PrepareSql(), message, dbTransaction); - private void CheckIsAdoNet(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (IsUsingEF) - throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); } - private async Task PublishCoreAsync(string name, string content) + protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) { - var connection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) - { - IsCapOpenedTrans = true; - transaction = await _dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted); - } - var dbTransaction = transaction.GetDbTransaction(); - await PublishWithTransAsync(name, content, connection, dbTransaction); - } + await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction); - private void PublishCore(string name, string content) - { - var connection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) - { - IsCapOpenedTrans = true; - transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); - } - var dbTransaction = transaction.GetDbTransaction(); - PublishWithTrans(name, content, connection, dbTransaction); + _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); } - private async Task PublishWithTransAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + public async Task PublishAsync(CapPublishedMessage message) { - var message = new CapPublishedMessage + using (var conn = new SqlConnection(_options.ConnectionString)) { - Name = name, - Content = content, - StatusName = StatusName.Scheduled - }; - await dbConnection.ExecuteAsync(PrepareSql(), message, transaction: dbTransaction); - - _logger.LogInformation("Message has been persisted in the database. name:" + name); - - if (IsCapOpenedTrans) - { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); + await conn.ExecuteAsync(PrepareSql(), message); } - - PublishQueuer.PulseEvent.Set(); } - private void PublishWithTrans(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) - { - var message = new CapPublishedMessage - { - Name = name, - Content = content, - StatusName = StatusName.Scheduled - }; - var count = dbConnection.Execute(PrepareSql(), message, transaction: dbTransaction); - - _logger.LogInformation("Message has been persisted in the database. name:" + name); - - if (IsCapOpenedTrans) - { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); - } - PublishQueuer.PulseEvent.Set(); - } + #region private methods private string PrepareSql() { diff --git a/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj index c2b12cb..f2c293b 100644 --- a/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj +++ b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj @@ -3,20 +3,16 @@ - netstandard1.6 + netstandard2.0 DotNetCore.CAP.SqlServer - DotNetCore.CAP.SqlServer - 1.6.1 - $(PackageTargetFallback);dnxcore50 - false - false - false + $(PackageTags);SQL Server - + - - + + + diff --git a/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs index 65490ab..7c1bac1 100644 --- a/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs +++ b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs @@ -15,7 +15,7 @@ namespace DotNetCore.CAP.SqlServer private const int MaxBatch = 1000; private readonly TimeSpan _delay = TimeSpan.FromSeconds(1); - private readonly TimeSpan _waitingInterval = TimeSpan.FromHours(2); + private readonly TimeSpan _waitingInterval = TimeSpan.FromMinutes(5); private static readonly string[] Tables = { diff --git a/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs b/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs index 57d7e5d..9ef0247 100644 --- a/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs +++ b/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs @@ -2,7 +2,6 @@ using System.Data.SqlClient; using System.Threading; using System.Threading.Tasks; using Dapper; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace DotNetCore.CAP.SqlServer diff --git a/src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs b/src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs new file mode 100644 index 0000000..2eeddc1 --- /dev/null +++ b/src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs @@ -0,0 +1,163 @@ +using System; +using System.Data; +using System.Threading.Tasks; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using DotNetCore.CAP.Processor; + +namespace DotNetCore.CAP.Abstractions +{ + public abstract class CapPublisherBase : ICapPublisher, IDisposable + { + protected IDbConnection DbConnection { get; set; } + protected IDbTransaction DbTranasaction { 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(string name, T contentObj, string callbackName = null) + { + CheckIsUsingEF(name); + PrepareConnectionForEF(); + + var content = Serialize(contentObj, callbackName); + + PublishWithTrans(name, content); + } + + public Task PublishAsync(string name, T contentObj, string callbackName = null) + { + CheckIsUsingEF(name); + PrepareConnectionForEF(); + + var content = Serialize(contentObj, callbackName); + + return PublishWithTransAsync(name, content); + } + + public void Publish(string name, T contentObj, IDbConnection dbConnection, + string callbackName = null, IDbTransaction dbTransaction = null) + { + CheckIsAdoNet(name); + PrepareConnectionForAdo(dbConnection, dbTransaction); + + var content = Serialize(contentObj, callbackName); + + PublishWithTrans(name, content); + } + + public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, + string callbackName = null, IDbTransaction dbTransaction = null) + { + CheckIsAdoNet(name); + PrepareConnectionForAdo(dbConnection, dbTransaction); + + var content = Serialize(contentObj, callbackName); + + return PublishWithTransAsync(name, content); + } + + protected abstract void PrepareConnectionForEF(); + + protected abstract void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message); + + protected abstract Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message); + + #region private methods + + private string Serialize(T obj, string callbackName = null) + { + var message = new Message(obj) + { + CallbackName = callbackName + }; + + return Helper.ToJson(message); + } + + private void PrepareConnectionForAdo(IDbConnection dbConnection, IDbTransaction dbTransaction) + { + DbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); + if (DbConnection.State != ConnectionState.Open) + { + IsCapOpenedConn = true; + DbConnection.Open(); + } + DbTranasaction = dbTransaction; + if (DbTranasaction == null) + { + IsCapOpenedTrans = true; + DbTranasaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + } + } + + private void CheckIsUsingEF(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (!IsUsingEF) + throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + + " otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); + } + + private void CheckIsAdoNet(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (IsUsingEF) + throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); + } + + private async Task PublishWithTransAsync(string name, string content) + { + var message = new CapPublishedMessage + { + Name = name, + Content = content, + StatusName = StatusName.Scheduled + }; + + await ExecuteAsync(DbConnection, DbTranasaction, message); + + ClosedCap(); + + PublishQueuer.PulseEvent.Set(); + } + + private void PublishWithTrans(string name, string content) + { + var message = new CapPublishedMessage + { + Name = name, + Content = content, + StatusName = StatusName.Scheduled + }; + + Execute(DbConnection, DbTranasaction, message); + + ClosedCap(); + + PublishQueuer.PulseEvent.Set(); + } + + private void ClosedCap() + { + if (IsCapOpenedTrans) + { + DbTranasaction.Commit(); + DbTranasaction.Dispose(); + } + if (IsCapOpenedConn) + { + DbConnection.Dispose(); + } + } + + public void Dispose() + { + DbTranasaction?.Dispose(); + DbConnection?.Dispose(); + } + + #endregion private methods + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/CAP.Options.cs b/src/DotNetCore.CAP/CAP.Options.cs index 0d17e67..d6cd441 100644 --- a/src/DotNetCore.CAP/CAP.Options.cs +++ b/src/DotNetCore.CAP/CAP.Options.cs @@ -20,27 +20,48 @@ namespace DotNetCore.CAP /// public const int DefaultQueueProcessorCount = 2; + /// + /// Default successed message expriation timespan, in seconds. + /// + public const int DefaultSuccessMessageExpirationAfter = 3600; + + /// + /// Failed message retry waiting interval. + /// + public const int DefaultFailedMessageWaitingInterval = 180; + public CapOptions() { PollingDelay = DefaultPollingDelay; QueueProcessorCount = DefaultQueueProcessorCount; + SuccessedMessageExpiredAfter = DefaultSuccessMessageExpirationAfter; + FailedMessageWaitingInterval = DefaultFailedMessageWaitingInterval; Extensions = new List(); } /// - /// Productor job polling delay time. Default is 15 sec. + /// Productor job polling delay time. + /// Default is 15 sec. /// public int PollingDelay { get; set; } /// /// Gets or sets the messages queue (Cap.Queue table) processor count. + /// Default is 2 processor. /// public int QueueProcessorCount { get; set; } /// - /// Failed messages polling delay time. Default is 3 min. + /// Sent or received successed message after timespan of due, then the message will be deleted at due time. + /// Dafault is 3600 seconds. + /// + public int SuccessedMessageExpiredAfter { get; set; } + + /// + /// Failed messages polling delay time. + /// Default is 180 seconds. /// - public int FailedMessageWaitingInterval { get; set; } = (int)TimeSpan.FromMinutes(3).TotalSeconds; + public int FailedMessageWaitingInterval { get; set; } /// /// We’ll invoke this call-back with message type,name,content when requeue failed message. diff --git a/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs b/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs index 085205e..06b8249 100644 --- a/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs +++ b/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs @@ -67,18 +67,20 @@ namespace Microsoft.Extensions.DependencyInjection private static void AddSubscribeServices(IServiceCollection services) { - var consumerListenerServices = new Dictionary(); + var consumerListenerServices = new List>(); foreach (var rejectedServices in services) { if (rejectedServices.ImplementationType != null && typeof(ICapSubscribe).IsAssignableFrom(rejectedServices.ImplementationType)) - - consumerListenerServices.Add(typeof(ICapSubscribe), rejectedServices.ImplementationType); + { + consumerListenerServices.Add(new KeyValuePair(typeof(ICapSubscribe), + rejectedServices.ImplementationType)); + } } foreach (var service in consumerListenerServices) { - services.AddSingleton(service.Key, service.Value); + services.AddTransient(service.Key, service.Value); } var types = Assembly.GetEntryAssembly().ExportedTypes; @@ -86,7 +88,7 @@ namespace Microsoft.Extensions.DependencyInjection { if (Helper.IsController(type.GetTypeInfo())) { - services.AddSingleton(typeof(object), type); + services.AddTransient(typeof(object), type); } } } diff --git a/src/DotNetCore.CAP/DotNetCore.CAP.csproj b/src/DotNetCore.CAP/DotNetCore.CAP.csproj index 2bf7ab4..e0e473f 100644 --- a/src/DotNetCore.CAP/DotNetCore.CAP.csproj +++ b/src/DotNetCore.CAP/DotNetCore.CAP.csproj @@ -3,26 +3,21 @@ - DotNetCore.CAP - netstandard1.6 - 1.6.1 - $(PackageTargetFallback);dnxcore50 - - - - False + netstandard2.0 + DotNetCore.CAP + $(PackageTags); - - - - - - + + + + + + - + diff --git a/src/DotNetCore.CAP/ICallbackPublisher.cs b/src/DotNetCore.CAP/ICallbackPublisher.cs new file mode 100644 index 0000000..1743b52 --- /dev/null +++ b/src/DotNetCore.CAP/ICallbackPublisher.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using DotNetCore.CAP.Models; + +namespace DotNetCore.CAP +{ + public interface ICallbackPublisher + { + Task PublishAsync(CapPublishedMessage obj); + } +} diff --git a/src/DotNetCore.CAP/ICapPublisher.cs b/src/DotNetCore.CAP/ICapPublisher.cs index a3a7210..fc05bed 100644 --- a/src/DotNetCore.CAP/ICapPublisher.cs +++ b/src/DotNetCore.CAP/ICapPublisher.cs @@ -18,7 +18,8 @@ namespace DotNetCore.CAP /// The type of conetent object. /// the topic name or exchange router key. /// message body content, that will be serialized of json. - Task PublishAsync(string name, T contentObj); + /// callback subscriber name + Task PublishAsync(string name, T contentObj, string callbackName = null); /// /// (EntityFramework) Publish a object message. @@ -30,24 +31,27 @@ namespace DotNetCore.CAP /// The type of conetent object. /// the topic name or exchange router key. /// message body content, that will be serialized of json. - void Publish(string name, T contentObj); + /// callback subscriber name + void Publish(string name, T contentObj, string callbackName = null); /// /// (ado.net) Asynchronous publish a object message. /// /// the topic name or exchange router key. /// message body content, that will be serialized of json. + /// callback subscriber name /// the connection of /// the transaction of - Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null); + Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, string callbackName = null, IDbTransaction dbTransaction = null); /// /// (ado.net) Publish a object message. /// /// the topic name or exchange router key. /// message body content, that will be serialized of json. + /// callback subscriber name /// the connection of /// the transaction of - void Publish(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null); + void Publish(string name, T contentObj, IDbConnection dbConnection, string callbackName = null, IDbTransaction dbTransaction = null); } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs b/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs index a032af6..e7274a8 100644 --- a/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs +++ b/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs @@ -10,12 +10,16 @@ namespace DotNetCore.CAP { public abstract class BasePublishQueueExecutor : IQueueExecutor { + private readonly CapOptions _options; private readonly IStateChanger _stateChanger; private readonly ILogger _logger; - protected BasePublishQueueExecutor(IStateChanger stateChanger, + protected BasePublishQueueExecutor( + CapOptions options, + IStateChanger stateChanger, ILogger logger) { + _options = options; _stateChanger = stateChanger; _logger = logger; } @@ -54,7 +58,7 @@ namespace DotNetCore.CAP } else { - newState = new SucceededState(); + newState = new SucceededState(_options.SuccessedMessageExpiredAfter); } await _stateChanger.ChangeStateAsync(message, newState, connection); diff --git a/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs b/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs index e8fd93c..a1f6956 100644 --- a/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs +++ b/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs @@ -15,16 +15,18 @@ namespace DotNetCore.CAP private readonly IConsumerInvokerFactory _consumerInvokerFactory; private readonly IStateChanger _stateChanger; private readonly ILogger _logger; - + private readonly CapOptions _options; private readonly MethodMatcherCache _selector; public SubscibeQueueExecutor( IStateChanger stateChanger, MethodMatcherCache selector, + CapOptions options, IConsumerInvokerFactory consumerInvokerFactory, ILogger logger) { _selector = selector; + _options = options; _consumerInvokerFactory = consumerInvokerFactory; _stateChanger = stateChanger; _logger = logger; @@ -62,7 +64,7 @@ namespace DotNetCore.CAP } else { - newState = new SucceededState(); + newState = new SucceededState(_options.SuccessedMessageExpiredAfter); } await _stateChanger.ChangeStateAsync(message, newState, connection); diff --git a/src/DotNetCore.CAP/Infrastructure/Helper.cs b/src/DotNetCore.CAP/Infrastructure/Helper.cs index a617e13..1f349d5 100644 --- a/src/DotNetCore.CAP/Infrastructure/Helper.cs +++ b/src/DotNetCore.CAP/Infrastructure/Helper.cs @@ -72,7 +72,26 @@ namespace DotNetCore.CAP.Infrastructure public static bool IsComplexType(Type type) { - return !TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); + return !CanConvertFromString(type); + } + + private static bool CanConvertFromString(Type destinationType) + { + destinationType = Nullable.GetUnderlyingType(destinationType) ?? destinationType; + return IsSimpleType(destinationType) || + TypeDescriptor.GetConverter(destinationType).CanConvertFrom(typeof(string)); + } + + private static bool IsSimpleType(Type type) + { + return type.GetTypeInfo().IsPrimitive || + type.Equals(typeof(decimal)) || + type.Equals(typeof(string)) || + type.Equals(typeof(DateTime)) || + type.Equals(typeof(Guid)) || + type.Equals(typeof(DateTimeOffset)) || + type.Equals(typeof(TimeSpan)) || + type.Equals(typeof(Uri)); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Infrastructure/ObjectId.cs b/src/DotNetCore.CAP/Infrastructure/ObjectId.cs new file mode 100644 index 0000000..23784fd --- /dev/null +++ b/src/DotNetCore.CAP/Infrastructure/ObjectId.cs @@ -0,0 +1,546 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace DotNetCore.CAP +{ + /// + /// Represents an ObjectId + /// + [Serializable] + public struct ObjectId : IComparable, IEquatable + { + // private static fields + private static readonly DateTime __unixEpoch; + + private static readonly long __dateTimeMaxValueMillisecondsSinceEpoch; + private static readonly long __dateTimeMinValueMillisecondsSinceEpoch; + private static ObjectId __emptyInstance = default(ObjectId); + private static int __staticMachine; + private static short __staticPid; + private static int __staticIncrement; // high byte will be masked out when generating new ObjectId + + private static uint[] _lookup32 = Enumerable.Range(0, 256).Select(i => + { + string s = i.ToString("x2"); + return ((uint)s[0]) + ((uint)s[1] << 16); + }).ToArray(); + + // we're using 14 bytes instead of 12 to hold the ObjectId in memory but unlike a byte[] there is no additional object on the heap + // the extra two bytes are not visible to anyone outside of this class and they buy us considerable simplification + // an additional advantage of this representation is that it will serialize to JSON without any 64 bit overflow problems + private int _timestamp; + + private int _machine; + private short _pid; + private int _increment; + + // static constructor + static ObjectId() + { + __unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + __dateTimeMaxValueMillisecondsSinceEpoch = (DateTime.MaxValue - __unixEpoch).Ticks / 10000; + __dateTimeMinValueMillisecondsSinceEpoch = (DateTime.MinValue - __unixEpoch).Ticks / 10000; + __staticMachine = GetMachineHash(); + __staticIncrement = (new Random()).Next(); + __staticPid = (short)GetCurrentProcessId(); + } + + // constructors + /// + /// Initializes a new instance of the ObjectId class. + /// + /// The bytes. + public ObjectId(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException("bytes"); + } + Unpack(bytes, out _timestamp, out _machine, out _pid, out _increment); + } + + /// + /// Initializes a new instance of the ObjectId class. + /// + /// The timestamp (expressed as a DateTime). + /// The machine hash. + /// The PID. + /// The increment. + public ObjectId(DateTime timestamp, int machine, short pid, int increment) + : this(GetTimestampFromDateTime(timestamp), machine, pid, increment) + { + } + + /// + /// Initializes a new instance of the ObjectId class. + /// + /// The timestamp. + /// The machine hash. + /// The PID. + /// The increment. + public ObjectId(int timestamp, int machine, short pid, int increment) + { + if ((machine & 0xff000000) != 0) + { + throw new ArgumentOutOfRangeException("machine", "The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); + } + if ((increment & 0xff000000) != 0) + { + throw new ArgumentOutOfRangeException("increment", "The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); + } + + _timestamp = timestamp; + _machine = machine; + _pid = pid; + _increment = increment; + } + + /// + /// Initializes a new instance of the ObjectId class. + /// + /// The value. + 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 + /// + /// Gets an instance of ObjectId where the value is empty. + /// + public static ObjectId Empty + { + get { return __emptyInstance; } + } + + // public properties + /// + /// Gets the timestamp. + /// + public int Timestamp + { + get { return _timestamp; } + } + + /// + /// Gets the machine. + /// + public int Machine + { + get { return _machine; } + } + + /// + /// Gets the PID. + /// + public short Pid + { + get { return _pid; } + } + + /// + /// Gets the increment. + /// + public int Increment + { + get { return _increment; } + } + + /// + /// Gets the creation time (derived from the timestamp). + /// + public DateTime CreationTime + { + get { return __unixEpoch.AddSeconds(_timestamp); } + } + + // public operators + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId + /// True if the first ObjectId is less than the second ObjectId. + public static bool operator <(ObjectId lhs, ObjectId rhs) + { + return lhs.CompareTo(rhs) < 0; + } + + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId + /// True if the first ObjectId is less than or equal to the second ObjectId. + public static bool operator <=(ObjectId lhs, ObjectId rhs) + { + return lhs.CompareTo(rhs) <= 0; + } + + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId. + /// True if the two ObjectIds are equal. + public static bool operator ==(ObjectId lhs, ObjectId rhs) + { + return lhs.Equals(rhs); + } + + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId. + /// True if the two ObjectIds are not equal. + public static bool operator !=(ObjectId lhs, ObjectId rhs) + { + return !(lhs == rhs); + } + + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId + /// True if the first ObjectId is greather than or equal to the second ObjectId. + public static bool operator >=(ObjectId lhs, ObjectId rhs) + { + return lhs.CompareTo(rhs) >= 0; + } + + /// + /// Compares two ObjectIds. + /// + /// The first ObjectId. + /// The other ObjectId + /// True if the first ObjectId is greather than the second ObjectId. + public static bool operator >(ObjectId lhs, ObjectId rhs) + { + return lhs.CompareTo(rhs) > 0; + } + + // public static methods + /// + /// Generates a new ObjectId with a unique value. + /// + /// An ObjectId. + public static ObjectId GenerateNewId() + { + return GenerateNewId(GetTimestampFromDateTime(DateTime.UtcNow)); + } + + /// + /// Generates a new ObjectId with a unique value (with the timestamp component based on a given DateTime). + /// + /// The timestamp component (expressed as a DateTime). + /// An ObjectId. + public static ObjectId GenerateNewId(DateTime timestamp) + { + return GenerateNewId(GetTimestampFromDateTime(timestamp)); + } + + /// + /// Generates a new ObjectId with a unique value (with the given timestamp). + /// + /// The timestamp component. + /// An ObjectId. + public static ObjectId GenerateNewId(int timestamp) + { + int increment = Interlocked.Increment(ref __staticIncrement) & 0x00ffffff; // only use low order 3 bytes + return new ObjectId(timestamp, __staticMachine, __staticPid, increment); + } + + /// + /// Generates a new ObjectId string with a unique value. + /// + /// The string value of the new generated ObjectId. + public static string GenerateNewStringId() + { + return GenerateNewId().ToString(); + } + + /// + /// Packs the components of an ObjectId into a byte array. + /// + /// The timestamp. + /// The machine hash. + /// The PID. + /// The increment. + /// A byte array. + public static byte[] Pack(int timestamp, int machine, short pid, int increment) + { + if ((machine & 0xff000000) != 0) + { + throw new ArgumentOutOfRangeException("machine", "The machine value must be between 0 and 16777215 (it must fit in 3 bytes)."); + } + if ((increment & 0xff000000) != 0) + { + throw new ArgumentOutOfRangeException("increment", "The increment value must be between 0 and 16777215 (it must fit in 3 bytes)."); + } + + byte[] bytes = new byte[12]; + bytes[0] = (byte)(timestamp >> 24); + bytes[1] = (byte)(timestamp >> 16); + bytes[2] = (byte)(timestamp >> 8); + bytes[3] = (byte)(timestamp); + bytes[4] = (byte)(machine >> 16); + bytes[5] = (byte)(machine >> 8); + bytes[6] = (byte)(machine); + bytes[7] = (byte)(pid >> 8); + bytes[8] = (byte)(pid); + bytes[9] = (byte)(increment >> 16); + bytes[10] = (byte)(increment >> 8); + bytes[11] = (byte)(increment); + return bytes; + } + + /// + /// Parses a string and creates a new ObjectId. + /// + /// The string value. + /// A ObjectId. + 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)); + } + + /// + /// Unpacks a byte array into the components of an ObjectId. + /// + /// A byte array. + /// The timestamp. + /// The machine hash. + /// The PID. + /// The increment. + 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 + /// + /// 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 + /// before throwing an exception requiring the try/catch at an even higher level that we don't necessarily control. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static int GetCurrentProcessId() + { + return Process.GetCurrentProcess().Id; + } + + private static int GetMachineHash() + { + var hostName = Environment.MachineName; // use instead of Dns.HostName so it will work offline + var md5 = MD5.Create(); + var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(hostName)); + return (hash[0] << 16) + (hash[1] << 8) + hash[2]; // use first 3 bytes of hash + } + + private static int GetTimestampFromDateTime(DateTime timestamp) + { + return (int)Math.Floor((ToUniversalTime(timestamp) - __unixEpoch).TotalSeconds); + } + + // public methods + /// + /// Compares this ObjectId to another ObjectId. + /// + /// The other ObjectId. + /// A 32-bit signed integer that indicates whether this ObjectId is less than, equal to, or greather than the other. + public int CompareTo(ObjectId other) + { + int r = _timestamp.CompareTo(other._timestamp); + if (r != 0) { return r; } + r = _machine.CompareTo(other._machine); + if (r != 0) { return r; } + r = _pid.CompareTo(other._pid); + if (r != 0) { return r; } + return _increment.CompareTo(other._increment); + } + + /// + /// Compares this ObjectId to another ObjectId. + /// + /// The other ObjectId. + /// True if the two ObjectIds are equal. + public bool Equals(ObjectId rhs) + { + return + _timestamp == rhs._timestamp && + _machine == rhs._machine && + _pid == rhs._pid && + _increment == rhs._increment; + } + + /// + /// Compares this ObjectId to another object. + /// + /// The other object. + /// True if the other object is an ObjectId and equal to this one. + public override bool Equals(object obj) + { + if (obj is ObjectId) + { + return Equals((ObjectId)obj); + } + else + { + return false; + } + } + + /// + /// Gets the hash code. + /// + /// The hash code. + public override int GetHashCode() + { + int hash = 17; + hash = 37 * hash + _timestamp.GetHashCode(); + hash = 37 * hash + _machine.GetHashCode(); + hash = 37 * hash + _pid.GetHashCode(); + hash = 37 * hash + _increment.GetHashCode(); + return hash; + } + + /// + /// Converts the ObjectId to a byte array. + /// + /// A byte array. + public byte[] ToByteArray() + { + return Pack(_timestamp, _machine, _pid, _increment); + } + + /// + /// Returns a string representation of the value. + /// + /// A string representation of the value. + public override string ToString() + { + return ToHexString(ToByteArray()); + } + + /// + /// Parses a hex string into its equivalent byte array. + /// + /// The hex string to parse. + /// The byte equivalent of the hex string. + 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"); + } + + byte[] arr = new byte[s.Length >> 1]; + + for (int i = 0; i < s.Length >> 1; ++i) + { + arr[i] = (byte)((GetHexVal(s[i << 1]) << 4) + (GetHexVal(s[(i << 1) + 1]))); + } + + return arr; + } + + /// + /// Converts a byte array to a hex string. + /// + /// The byte array. + /// A hex string. + public static string ToHexString(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException("bytes"); + } + var result = new char[bytes.Length * 2]; + for (int i = 0; i < bytes.Length; i++) + { + var val = _lookup32[bytes[i]]; + result[2 * i] = (char)val; + result[2 * i + 1] = (char)(val >> 16); + } + return new string(result); + } + + /// + /// Converts a DateTime to number of milliseconds since Unix epoch. + /// + /// A DateTime. + /// Number of seconds since Unix epoch. + public static long ToMillisecondsSinceEpoch(DateTime dateTime) + { + var utcDateTime = ToUniversalTime(dateTime); + return (utcDateTime - __unixEpoch).Ticks / 10000; + } + + /// + /// Converts a DateTime to UTC (with special handling for MinValue and MaxValue). + /// + /// A DateTime. + /// The DateTime in UTC. + public static DateTime ToUniversalTime(DateTime dateTime) + { + if (dateTime == DateTime.MinValue) + { + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); + } + else if (dateTime == DateTime.MaxValue) + { + return DateTime.SpecifyKind(DateTime.MaxValue, DateTimeKind.Utc); + } + else + { + return dateTime.ToUniversalTime(); + } + } + + private static int GetHexVal(char hex) + { + int val = (int)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 diff --git a/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs b/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs index 0921e1b..cdcb6d6 100644 --- a/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs +++ b/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs @@ -1,5 +1,6 @@ using System; using DotNetCore.CAP.Abstractions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DotNetCore.CAP.Internal @@ -22,12 +23,15 @@ namespace DotNetCore.CAP.Internal public IConsumerInvoker CreateInvoker(ConsumerContext consumerContext) { - var context = new ConsumerInvokerContext(consumerContext) + using (var scope = _serviceProvider.CreateScope()) { - Result = new DefaultConsumerInvoker(_logger, _serviceProvider, _modelBinderFactory, consumerContext) - }; + var context = new ConsumerInvokerContext(consumerContext) + { + Result = new DefaultConsumerInvoker(_logger, scope.ServiceProvider, _modelBinderFactory, consumerContext) + }; - return context.Result; + return context.Result; + } } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/ConsumerMethodExecutor.cs b/src/DotNetCore.CAP/Internal/ConsumerMethodExecutor.cs deleted file mode 100644 index b33cc71..0000000 --- a/src/DotNetCore.CAP/Internal/ConsumerMethodExecutor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; - -namespace DotNetCore.CAP.Internal -{ - public class ConsumerMethodExecutor - { - public static object[] PrepareArguments( - IDictionary actionParameters, - ObjectMethodExecutor actionMethodExecutor) - { - var declaredParameterInfos = actionMethodExecutor.MethodParameters; - var count = declaredParameterInfos.Length; - if (count == 0) - { - return null; - } - - var arguments = new object[count]; - for (var index = 0; index < count; index++) - { - var parameterInfo = declaredParameterInfos[index]; - object value; - - if (!actionParameters.TryGetValue(parameterInfo.Name, out value)) - { - value = actionMethodExecutor.GetDefaultValueForParameter(index); - } - - arguments[index] = value; - } - - return arguments; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/IConsumerInvoker.Default.cs b/src/DotNetCore.CAP/Internal/IConsumerInvoker.Default.cs index 3009f73..4118626 100644 --- a/src/DotNetCore.CAP/Internal/IConsumerInvoker.Default.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerInvoker.Default.cs @@ -1,7 +1,10 @@ using System; using System.Threading.Tasks; using DotNetCore.CAP.Abstractions; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; namespace DotNetCore.CAP.Internal @@ -21,49 +24,102 @@ namespace DotNetCore.CAP.Internal { _modelBinderFactory = modelBinderFactory; _serviceProvider = serviceProvider; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger; + _consumerContext = consumerContext; - _consumerContext = consumerContext ?? throw new ArgumentNullException(nameof(consumerContext)); _executor = ObjectMethodExecutor.Create(_consumerContext.ConsumerDescriptor.MethodInfo, _consumerContext.ConsumerDescriptor.ImplTypeInfo); } public async Task InvokeAsync() { - using (_logger.BeginScope("consumer invoker begin")) + _logger.LogDebug("Executing consumer Topic: {0}", _consumerContext.ConsumerDescriptor.MethodInfo.Name); + + var obj = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, + _consumerContext.ConsumerDescriptor.ImplTypeInfo.AsType()); + + var jsonConent = _consumerContext.DeliverMessage.Content; + var message = Helper.FromJson(jsonConent); + + object result = null; + if (_executor.MethodParameters.Length > 0) + { + result = await ExecuteWithParameterAsync(obj, message.Content.ToString()); + } + else { - _logger.LogDebug("Executing consumer Topic: {0}", _consumerContext.ConsumerDescriptor.MethodInfo.Name); + result = await ExecuteAsync(obj); + } - var obj = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, - _consumerContext.ConsumerDescriptor.ImplTypeInfo.AsType()); + if (!string.IsNullOrEmpty(message.CallbackName)) + { + await SentCallbackMessage(message.Id, message.CallbackName, result); + } + } - var value = _consumerContext.DeliverMessage.Content; - if (_executor.MethodParameters.Length > 0) + private async Task ExecuteAsync(object @class) + { + if (_executor.IsMethodAsync) + { + return await _executor.ExecuteAsync(@class); + } + else + { + return _executor.Execute(@class); + } + } + + private async Task ExecuteWithParameterAsync(object @class, string parameterString) + { + var firstParameter = _executor.MethodParameters[0]; + try + { + var binder = _modelBinderFactory.CreateBinder(firstParameter); + var bindResult = await binder.BindModelAsync(parameterString); + if (bindResult.IsSuccess) { - var firstParameter = _executor.MethodParameters[0]; - try + if (_executor.IsMethodAsync) { - var binder = _modelBinderFactory.CreateBinder(firstParameter); - var result = await binder.BindModelAsync(value); - if (result.IsSuccess) - { - _executor.Execute(obj, result.Model); - } - else - { - _logger.LogWarning($"Parameters:{firstParameter.Name} bind failed! the content is:" + value); - } + return await _executor.ExecuteAsync(@class, bindResult.Model); } - catch (FormatException ex) + else { - _logger.ModelBinderFormattingException(_executor.MethodInfo?.Name, firstParameter.Name, value, ex); + return _executor.Execute(@class, bindResult.Model); } } else { - _executor.Execute(obj); + throw new MethodBindException($"Parameters:{firstParameter.Name} bind failed! ParameterString is: {parameterString} "); } } + catch (FormatException ex) + { + _logger.ModelBinderFormattingException(_executor.MethodInfo?.Name, firstParameter.Name, parameterString, ex); + return null; + } + } + + private async Task SentCallbackMessage(string messageId, string topicName, object bodyObj) + { + var callbackMessage = new Message + { + Id = messageId, + Content = bodyObj + }; + + using (var scope = _serviceProvider.CreateScope()) + { + var provider = scope.ServiceProvider; + var publisher = provider.GetRequiredService(); + + var publishedMessage = new CapPublishedMessage + { + Name = topicName, + Content = Helper.ToJson(callbackMessage), + StatusName = StatusName.Scheduled + }; + await publisher.PublishAsync(publishedMessage); + } } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs b/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs index c25f7c6..369e01e 100644 --- a/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs +++ b/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs @@ -20,7 +20,9 @@ namespace DotNetCore.CAP.Internal try { var type = _parameterInfo.ParameterType; + var value = Helper.FromJson(content, type); + return Task.FromResult(ModelBindingResult.Success(value)); } catch (Exception) diff --git a/src/DotNetCore.CAP/Internal/MethodBindException.cs b/src/DotNetCore.CAP/Internal/MethodBindException.cs new file mode 100644 index 0000000..63eb454 --- /dev/null +++ b/src/DotNetCore.CAP/Internal/MethodBindException.cs @@ -0,0 +1,20 @@ +using System; + +namespace DotNetCore.CAP.Internal +{ + [Serializable] + public class MethodBindException : Exception + { + public MethodBindException() + { + } + + public MethodBindException(string message) : base(message) + { + } + + public MethodBindException(string message, Exception inner) : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/ModelAttributes.cs b/src/DotNetCore.CAP/Internal/ModelAttributes.cs deleted file mode 100644 index 756f654..0000000 --- a/src/DotNetCore.CAP/Internal/ModelAttributes.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace DotNetCore.CAP.Internal -{ - /// - /// Provides access to the combined list of attributes associated a or property. - /// - public class ModelAttributes - { - /// - /// Creates a new for a . - /// - /// The set of attributes for the . - public ModelAttributes(IEnumerable typeAttributes) - { - Attributes = typeAttributes?.ToArray() ?? throw new ArgumentNullException(nameof(typeAttributes)); - TypeAttributes = Attributes; - } - - /// - /// Creates a new for a property. - /// - /// The set of attributes for the property. - /// - /// The set of attributes for the property's . See . - /// - public ModelAttributes(IEnumerable propertyAttributes, IEnumerable typeAttributes) - { - PropertyAttributes = propertyAttributes?.ToArray() - ?? throw new ArgumentNullException(nameof(propertyAttributes)); - - TypeAttributes = typeAttributes?.ToArray() - ?? throw new ArgumentNullException(nameof(typeAttributes)); - - Attributes = PropertyAttributes.Concat(TypeAttributes).ToArray(); - } - - /// - /// Gets the set of all attributes. If this instance represents the attributes for a property, the attributes - /// on the property definition are before those on the property's . - /// - public IReadOnlyList Attributes { get; } - - /// - /// Gets the set of attributes on the property, or null if this instance represents the attributes - /// for a . - /// - public IReadOnlyList PropertyAttributes { get; } - - /// - /// Gets the set of attributes on the . If this instance represents a property, - /// then contains attributes retrieved from - /// . - /// - public IReadOnlyList TypeAttributes { get; } - - /// - /// Gets the attributes for the given . - /// - /// The in which caller found . - /// - /// A for which attributes need to be resolved. - /// - /// A instance with the attributes of the property. - public static ModelAttributes GetAttributesForProperty(Type type, PropertyInfo property) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (property == null) - { - throw new ArgumentNullException(nameof(property)); - } - - var propertyAttributes = property.GetCustomAttributes(); - var typeAttributes = property.PropertyType.GetTypeInfo().GetCustomAttributes(); - - return new ModelAttributes(propertyAttributes, typeAttributes); - } - - /// - /// Gets the attributes for the given . - /// - /// The for which attributes need to be resolved. - /// - /// A instance with the attributes of the . - public static ModelAttributes GetAttributesForType(Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - var attributes = type.GetTypeInfo().GetCustomAttributes(); - - return new ModelAttributes(attributes); - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs deleted file mode 100644 index cb0f99d..0000000 --- a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; - -namespace DotNetCore.CAP.Internal -{ - public class ObjectMethodExecutor - { - private readonly object[] _parameterDefaultValues; - private readonly ConsumerMethodExecutorAsync _executorAsync; - private readonly ConsumerMethodExecutor _executor; - - private static readonly MethodInfo _convertOfTMethod = - typeof(ObjectMethodExecutor).GetRuntimeMethods() - .Single(methodInfo => methodInfo.Name == nameof(Convert)); - - private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - MethodInfo = methodInfo ?? throw new ArgumentNullException(nameof(methodInfo)); - TargetTypeInfo = targetTypeInfo; - MethodParameters = methodInfo.GetParameters(); - MethodReturnType = methodInfo.ReturnType; - IsMethodAsync = typeof(Task).IsAssignableFrom(MethodReturnType); - TaskGenericType = IsMethodAsync ? GetTaskInnerTypeOrNull(MethodReturnType) : null; - - if (IsMethodAsync && TaskGenericType != null) - { - _executor = GetExecutor(methodInfo, targetTypeInfo); - _executorAsync = GetExecutorAsync(TaskGenericType, methodInfo, targetTypeInfo); - } - else - { - _executor = GetExecutor(methodInfo, targetTypeInfo); - } - - _parameterDefaultValues = GetParameterDefaultValues(MethodParameters); - } - - private delegate Task ConsumerMethodExecutorAsync(object target, object[] parameters); - - private delegate object ConsumerMethodExecutor(object target, object[] parameters); - - private delegate void VoidActionExecutor(object target, object[] parameters); - - public MethodInfo MethodInfo { get; } - - public ParameterInfo[] MethodParameters { get; } - - public TypeInfo TargetTypeInfo { get; } - - public Type TaskGenericType { get; } - - // This field is made internal set because it is set in unit tests. - public Type MethodReturnType { get; internal set; } - - public bool IsMethodAsync { get; } - - //public bool IsTypeAssignableFromIActionResult { get; } - - public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - var executor = new ObjectMethodExecutor(methodInfo, targetTypeInfo); - return executor; - } - - public Task ExecuteAsync(object target, params object[] parameters) - { - return _executorAsync(target, parameters); - } - - public object Execute(object target, params object[] parameters) - { - return _executor(target, parameters); - } - - public object GetDefaultValueForParameter(int index) - { - if (index < 0 || index > MethodParameters.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return _parameterDefaultValues[index]; - } - - private static ConsumerMethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) - { - // Parameters to executor - var targetParameter = Expression.Parameter(typeof(object), "target"); - var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); - - // Build parameter list - var parameters = new List(); - var paramInfos = methodInfo.GetParameters(); - for (int i = 0; i < paramInfos.Length; i++) - { - var paramInfo = paramInfos[i]; - var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); - var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); - - // valueCast is "(Ti) parameters[i]" - parameters.Add(valueCast); - } - - // Call method - var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); - var methodCall = Expression.Call(instanceCast, methodInfo, parameters); - - // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)" - // Create function - if (methodCall.Type == typeof(void)) - { - var lambda = Expression.Lambda(methodCall, targetParameter, parametersParameter); - var voidExecutor = lambda.Compile(); - return WrapVoidAction(voidExecutor); - } - else - { - // must coerce methodCall to match ActionExecutor signature - var castMethodCall = Expression.Convert(methodCall, typeof(object)); - var lambda = - Expression.Lambda(castMethodCall, targetParameter, parametersParameter); - return lambda.Compile(); - } - } - - private static ConsumerMethodExecutor WrapVoidAction(VoidActionExecutor executor) - { - return delegate (object target, object[] parameters) - { - executor(target, parameters); - return null; - }; - } - - private static ConsumerMethodExecutorAsync GetExecutorAsync( - Type taskInnerType, - MethodInfo methodInfo, - TypeInfo targetTypeInfo) - { - // Parameters to executor - var targetParameter = Expression.Parameter(typeof(object), "target"); - var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); - - // Build parameter list - var parameters = new List(); - var paramInfos = methodInfo.GetParameters(); - for (int i = 0; i < paramInfos.Length; i++) - { - var paramInfo = paramInfos[i]; - var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); - var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); - - // valueCast is "(Ti) parameters[i]" - parameters.Add(valueCast); - } - - // Call method - var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); - var methodCall = Expression.Call(instanceCast, methodInfo, parameters); - - var coerceMethodCall = GetCoerceMethodCallExpression(taskInnerType, methodCall, methodInfo); - - var lambda = Expression.Lambda(coerceMethodCall, - targetParameter, parametersParameter); - - return lambda.Compile(); - } - - // We need to CoerceResult as the object value returned from methodInfo.Invoke has to be cast to a Task. - // This is necessary to enable calling await on the returned task. - // i.e we need to write the following var result = await (Task)mInfo.Invoke. - // Returning Task enables us to await on the result. - private static Expression GetCoerceMethodCallExpression( - Type taskValueType, - MethodCallExpression methodCall, - MethodInfo methodInfo) - { - var castMethodCall = Expression.Convert(methodCall, typeof(object)); - var genericMethodInfo = _convertOfTMethod.MakeGenericMethod(taskValueType); - var genericMethodCall = Expression.Call(null, genericMethodInfo, castMethodCall); - var convertedResult = Expression.Convert(genericMethodCall, typeof(Task)); - return convertedResult; - } - - /// - /// Cast Task of T to Task of object - /// - private static async Task CastToObject(Task task) - { - return (object)await task; - } - - private static Type GetTaskInnerTypeOrNull(Type type) - { - var genericType = ExtractGenericInterface(type, typeof(Task<>)); - - return genericType?.GenericTypeArguments[0]; - } - - public static Type ExtractGenericInterface(Type queryType, Type interfaceType) - { - if (queryType == null) - { - throw new ArgumentNullException(nameof(queryType)); - } - - if (interfaceType == null) - { - throw new ArgumentNullException(nameof(interfaceType)); - } - - if (IsGenericInstantiation(queryType, interfaceType)) - { - // queryType matches (i.e. is a closed generic type created from) the open generic type. - return queryType; - } - - // Otherwise check all interfaces the type implements for a match. - // - If multiple different generic instantiations exists, we want the most derived one. - // - If that doesn't break the tie, then we sort alphabetically so that it's deterministic. - // - // We do this by looking at interfaces on the type, and recursing to the base type - // if we don't find any matches. - return GetGenericInstantiation(queryType, interfaceType); - } - - private static bool IsGenericInstantiation(Type candidate, Type interfaceType) - { - return - candidate.GetTypeInfo().IsGenericType && - candidate.GetGenericTypeDefinition() == interfaceType; - } - - private static Type GetGenericInstantiation(Type queryType, Type interfaceType) - { - Type bestMatch = null; - var interfaces = queryType.GetInterfaces(); - foreach (var @interface in interfaces) - { - if (IsGenericInstantiation(@interface, interfaceType)) - { - if (bestMatch == null) - { - bestMatch = @interface; - } - else if (StringComparer.Ordinal.Compare(@interface.FullName, bestMatch.FullName) < 0) - { - bestMatch = @interface; - } - else - { - // There are two matches at this level of the class hierarchy, but @interface is after - // bestMatch in the sort order. - } - } - } - - if (bestMatch != null) - { - return bestMatch; - } - - // BaseType will be null for object and interfaces, which means we've reached 'bottom'. - var baseType = queryType?.GetTypeInfo().BaseType; - if (baseType == null) - { - return null; - } - else - { - return GetGenericInstantiation(baseType, interfaceType); - } - } - - private static Task Convert(object taskAsObject) - { - var task = (Task)taskAsObject; - return CastToObject(task); - } - - private static object[] GetParameterDefaultValues(ParameterInfo[] parameters) - { - var values = new object[parameters.Length]; - - for (var i = 0; i < parameters.Length; i++) - { - var parameterInfo = parameters[i]; - object defaultValue; - - if (parameterInfo.HasDefaultValue) - { - defaultValue = parameterInfo.DefaultValue; - } - else - { - var defaultValueAttribute = parameterInfo - .GetCustomAttribute(inherit: false); - - if (defaultValueAttribute?.Value == null) - { - defaultValue = parameterInfo.ParameterType.GetTypeInfo().IsValueType - ? Activator.CreateInstance(parameterInfo.ParameterType) - : null; - } - else - { - defaultValue = defaultValueAttribute.Value; - } - } - - values[i] = defaultValue; - } - - return values; - } - } -} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/AwaitableInfo.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/AwaitableInfo.cs new file mode 100644 index 0000000..431b83a --- /dev/null +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/AwaitableInfo.cs @@ -0,0 +1,127 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + internal struct AwaitableInfo + { + public Type AwaiterType { get; } + public PropertyInfo AwaiterIsCompletedProperty { get; } + public MethodInfo AwaiterGetResultMethod { get; } + public MethodInfo AwaiterOnCompletedMethod { get; } + public MethodInfo AwaiterUnsafeOnCompletedMethod { get; } + public Type ResultType { get; } + public MethodInfo GetAwaiterMethod { get; } + + public AwaitableInfo( + Type awaiterType, + PropertyInfo awaiterIsCompletedProperty, + MethodInfo awaiterGetResultMethod, + MethodInfo awaiterOnCompletedMethod, + MethodInfo awaiterUnsafeOnCompletedMethod, + Type resultType, + MethodInfo getAwaiterMethod) + { + AwaiterType = awaiterType; + AwaiterIsCompletedProperty = awaiterIsCompletedProperty; + AwaiterGetResultMethod = awaiterGetResultMethod; + AwaiterOnCompletedMethod = awaiterOnCompletedMethod; + AwaiterUnsafeOnCompletedMethod = awaiterUnsafeOnCompletedMethod; + ResultType = resultType; + GetAwaiterMethod = getAwaiterMethod; + } + + public static bool IsTypeAwaitable(Type type, out AwaitableInfo awaitableInfo) + { + // Based on Roslyn code: http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/ISymbolExtensions.cs,db4d48ba694b9347 + + // Awaitable must have method matching "object GetAwaiter()" + var getAwaiterMethod = type.GetRuntimeMethods().FirstOrDefault(m => + m.Name.Equals("GetAwaiter", StringComparison.OrdinalIgnoreCase) + && m.GetParameters().Length == 0 + && m.ReturnType != null); + if (getAwaiterMethod == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + var awaiterType = getAwaiterMethod.ReturnType; + + // Awaiter must have property matching "bool IsCompleted { get; }" + var isCompletedProperty = awaiterType.GetRuntimeProperties().FirstOrDefault(p => + p.Name.Equals("IsCompleted", StringComparison.OrdinalIgnoreCase) + && p.PropertyType == typeof(bool) + && p.GetMethod != null); + if (isCompletedProperty == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + // Awaiter must implement INotifyCompletion + var awaiterInterfaces = awaiterType.GetInterfaces(); + var implementsINotifyCompletion = awaiterInterfaces.Any(t => t == typeof(INotifyCompletion)); + if (!implementsINotifyCompletion) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + // INotifyCompletion supplies a method matching "void OnCompleted(Action action)" + var iNotifyCompletionMap = awaiterType + .GetTypeInfo() + .GetRuntimeInterfaceMap(typeof(INotifyCompletion)); + var onCompletedMethod = iNotifyCompletionMap.InterfaceMethods.Single(m => + m.Name.Equals("OnCompleted", StringComparison.OrdinalIgnoreCase) + && m.ReturnType == typeof(void) + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(Action)); + + // Awaiter optionally implements ICriticalNotifyCompletion + var implementsICriticalNotifyCompletion = awaiterInterfaces.Any(t => t == typeof(ICriticalNotifyCompletion)); + MethodInfo unsafeOnCompletedMethod; + if (implementsICriticalNotifyCompletion) + { + // ICriticalNotifyCompletion supplies a method matching "void UnsafeOnCompleted(Action action)" + var iCriticalNotifyCompletionMap = awaiterType + .GetTypeInfo() + .GetRuntimeInterfaceMap(typeof(ICriticalNotifyCompletion)); + unsafeOnCompletedMethod = iCriticalNotifyCompletionMap.InterfaceMethods.Single(m => + m.Name.Equals("UnsafeOnCompleted", StringComparison.OrdinalIgnoreCase) + && m.ReturnType == typeof(void) + && m.GetParameters().Length == 1 + && m.GetParameters()[0].ParameterType == typeof(Action)); + } + else + { + unsafeOnCompletedMethod = null; + } + + // Awaiter must have method matching "void GetResult" or "T GetResult()" + var getResultMethod = awaiterType.GetRuntimeMethods().FirstOrDefault(m => + m.Name.Equals("GetResult") + && m.GetParameters().Length == 0); + if (getResultMethod == null) + { + awaitableInfo = default(AwaitableInfo); + return false; + } + + awaitableInfo = new AwaitableInfo( + awaiterType, + isCompletedProperty, + getResultMethod, + onCompletedMethod, + unsafeOnCompletedMethod, + getResultMethod.ReturnType, + getAwaiterMethod); + return true; + } + } +} diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/CoercedAwaitableInfo.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/CoercedAwaitableInfo.cs new file mode 100644 index 0000000..4e48ef0 --- /dev/null +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/CoercedAwaitableInfo.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; + +namespace Microsoft.Extensions.Internal +{ + internal struct CoercedAwaitableInfo + { + public AwaitableInfo AwaitableInfo { get; } + public Expression CoercerExpression { get; } + public Type CoercerResultType { get; } + public bool RequiresCoercion => CoercerExpression != null; + + public CoercedAwaitableInfo(AwaitableInfo awaitableInfo) + { + AwaitableInfo = awaitableInfo; + CoercerExpression = null; + CoercerResultType = null; + } + + public CoercedAwaitableInfo(Expression coercerExpression, Type coercerResultType, AwaitableInfo coercedAwaitableInfo) + { + CoercerExpression = coercerExpression; + CoercerResultType = coercerResultType; + AwaitableInfo = coercedAwaitableInfo; + } + + public static bool IsTypeAwaitable(Type type, out CoercedAwaitableInfo info) + { + if (AwaitableInfo.IsTypeAwaitable(type, out var directlyAwaitableInfo)) + { + info = new CoercedAwaitableInfo(directlyAwaitableInfo); + return true; + } + + // It's not directly awaitable, but maybe we can coerce it. + // Currently we support coercing FSharpAsync. + if (ObjectMethodExecutorFSharpSupport.TryBuildCoercerFromFSharpAsyncToAwaitable(type, + out var coercerExpression, + out var coercerResultType)) + { + if (AwaitableInfo.IsTypeAwaitable(coercerResultType, out var coercedAwaitableInfo)) + { + info = new CoercedAwaitableInfo(coercerExpression, coercerResultType, coercedAwaitableInfo); + return true; + } + } + + info = default(CoercedAwaitableInfo); + return false; + } + } +} diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutor.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutor.cs new file mode 100644 index 0000000..b2025d6 --- /dev/null +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutor.cs @@ -0,0 +1,340 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + internal class ObjectMethodExecutor + { + private readonly object[] _parameterDefaultValues; + private readonly MethodExecutorAsync _executorAsync; + private readonly MethodExecutor _executor; + + private static readonly ConstructorInfo _objectMethodExecutorAwaitableConstructor = + typeof(ObjectMethodExecutorAwaitable).GetConstructor(new[] { + typeof(object), // customAwaitable + typeof(Func), // getAwaiterMethod + typeof(Func), // isCompletedMethod + typeof(Func), // getResultMethod + typeof(Action), // onCompletedMethod + typeof(Action) // unsafeOnCompletedMethod + }); + + private ObjectMethodExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues) + { + if (methodInfo == null) + { + throw new ArgumentNullException(nameof(methodInfo)); + } + + MethodInfo = methodInfo; + MethodParameters = methodInfo.GetParameters(); + TargetTypeInfo = targetTypeInfo; + MethodReturnType = methodInfo.ReturnType; + + var isAwaitable = CoercedAwaitableInfo.IsTypeAwaitable(MethodReturnType, out var coercedAwaitableInfo); + + IsMethodAsync = isAwaitable; + AsyncResultType = isAwaitable ? coercedAwaitableInfo.AwaitableInfo.ResultType : null; + + // Upstream code may prefer to use the sync-executor even for async methods, because if it knows + // that the result is a specific Task where T is known, then it can directly cast to that type + // and await it without the extra heap allocations involved in the _executorAsync code path. + _executor = GetExecutor(methodInfo, targetTypeInfo); + + if (IsMethodAsync) + { + _executorAsync = GetExecutorAsync(methodInfo, targetTypeInfo, coercedAwaitableInfo); + } + + _parameterDefaultValues = parameterDefaultValues; + } + + private delegate ObjectMethodExecutorAwaitable MethodExecutorAsync(object target, params object[] parameters); + + private delegate object MethodExecutor(object target, params object[] parameters); + + private delegate void VoidMethodExecutor(object target, object[] parameters); + + public MethodInfo MethodInfo { get; } + + public ParameterInfo[] MethodParameters { get; } + + public TypeInfo TargetTypeInfo { get; } + + public Type AsyncResultType { get; } + + // This field is made internal set because it is set in unit tests. + public Type MethodReturnType { get; internal set; } + + public bool IsMethodAsync { get; } + + public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + return new ObjectMethodExecutor(methodInfo, targetTypeInfo, null); + } + + public static ObjectMethodExecutor Create(MethodInfo methodInfo, TypeInfo targetTypeInfo, object[] parameterDefaultValues) + { + if (parameterDefaultValues == null) + { + throw new ArgumentNullException(nameof(parameterDefaultValues)); + } + + return new ObjectMethodExecutor(methodInfo, targetTypeInfo, parameterDefaultValues); + } + + /// + /// Executes the configured method on . This can be used whether or not + /// the configured method is asynchronous. + /// + /// + /// Even if the target method is asynchronous, it's desirable to invoke it using Execute rather than + /// ExecuteAsync if you know at compile time what the return type is, because then you can directly + /// "await" that value (via a cast), and then the generated code will be able to reference the + /// resulting awaitable as a value-typed variable. If you use ExecuteAsync instead, the generated + /// code will have to treat the resulting awaitable as a boxed object, because it doesn't know at + /// compile time what type it would be. + /// + /// The object whose method is to be executed. + /// Parameters to pass to the method. + /// The method return value. + public object Execute(object target, params object[] parameters) + { + return _executor(target, parameters); + } + + /// + /// Executes the configured method on . This can only be used if the configured + /// method is asynchronous. + /// + /// + /// If you don't know at compile time the type of the method's returned awaitable, you can use ExecuteAsync, + /// which supplies an awaitable-of-object. This always works, but can incur several extra heap allocations + /// as compared with using Execute and then using "await" on the result value typecasted to the known + /// awaitable type. The possible extra heap allocations are for: + /// + /// 1. The custom awaitable (though usually there's a heap allocation for this anyway, since normally + /// it's a reference type, and you normally create a new instance per call). + /// 2. The custom awaiter (whether or not it's a value type, since if it's not, you need a new instance + /// of it, and if it is, it will have to be boxed so the calling code can reference it as an object). + /// 3. The async result value, if it's a value type (it has to be boxed as an object, since the calling + /// code doesn't know what type it's going to be). + /// + /// The object whose method is to be executed. + /// Parameters to pass to the method. + /// An object that you can "await" to get the method return value. + public ObjectMethodExecutorAwaitable ExecuteAsync(object target, params object[] parameters) + { + return _executorAsync(target, parameters); + } + + public object GetDefaultValueForParameter(int index) + { + if (_parameterDefaultValues == null) + { + throw new InvalidOperationException($"Cannot call {nameof(GetDefaultValueForParameter)}, because no parameter default values were supplied."); + } + + if (index < 0 || index > MethodParameters.Length - 1) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return _parameterDefaultValues[index]; + } + + private static MethodExecutor GetExecutor(MethodInfo methodInfo, TypeInfo targetTypeInfo) + { + // Parameters to executor + var targetParameter = Expression.Parameter(typeof(object), "target"); + var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + var parameters = new List(); + var paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + var paramInfo = paramInfos[i]; + var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); + var methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // methodCall is "((Ttarget) target) method((T0) parameters[0], (T1) parameters[1], ...)" + // Create function + if (methodCall.Type == typeof(void)) + { + var lambda = Expression.Lambda(methodCall, targetParameter, parametersParameter); + var voidExecutor = lambda.Compile(); + return WrapVoidMethod(voidExecutor); + } + else + { + // must coerce methodCall to match ActionExecutor signature + var castMethodCall = Expression.Convert(methodCall, typeof(object)); + var lambda = Expression.Lambda(castMethodCall, targetParameter, parametersParameter); + return lambda.Compile(); + } + } + + private static MethodExecutor WrapVoidMethod(VoidMethodExecutor executor) + { + return delegate (object target, object[] parameters) + { + executor(target, parameters); + return null; + }; + } + + private static MethodExecutorAsync GetExecutorAsync( + MethodInfo methodInfo, + TypeInfo targetTypeInfo, + CoercedAwaitableInfo coercedAwaitableInfo) + { + // Parameters to executor + var targetParameter = Expression.Parameter(typeof(object), "target"); + var parametersParameter = Expression.Parameter(typeof(object[]), "parameters"); + + // Build parameter list + var parameters = new List(); + var paramInfos = methodInfo.GetParameters(); + for (int i = 0; i < paramInfos.Length; i++) + { + var paramInfo = paramInfos[i]; + var valueObj = Expression.ArrayIndex(parametersParameter, Expression.Constant(i)); + var valueCast = Expression.Convert(valueObj, paramInfo.ParameterType); + + // valueCast is "(Ti) parameters[i]" + parameters.Add(valueCast); + } + + // Call method + var instanceCast = Expression.Convert(targetParameter, targetTypeInfo.AsType()); + var methodCall = Expression.Call(instanceCast, methodInfo, parameters); + + // Using the method return value, construct an ObjectMethodExecutorAwaitable based on + // the info we have about its implementation of the awaitable pattern. Note that all + // the funcs/actions we construct here are precompiled, so that only one instance of + // each is preserved throughout the lifetime of the ObjectMethodExecutor. + + // var getAwaiterFunc = (object awaitable) => + // (object)((CustomAwaitableType)awaitable).GetAwaiter(); + var customAwaitableParam = Expression.Parameter(typeof(object), "awaitable"); + var awaitableInfo = coercedAwaitableInfo.AwaitableInfo; + var postCoercionMethodReturnType = coercedAwaitableInfo.CoercerResultType ?? methodInfo.ReturnType; + var getAwaiterFunc = Expression.Lambda>( + Expression.Convert( + Expression.Call( + Expression.Convert(customAwaitableParam, postCoercionMethodReturnType), + awaitableInfo.GetAwaiterMethod), + typeof(object)), + customAwaitableParam).Compile(); + + // var isCompletedFunc = (object awaiter) => + // ((CustomAwaiterType)awaiter).IsCompleted; + var isCompletedParam = Expression.Parameter(typeof(object), "awaiter"); + var isCompletedFunc = Expression.Lambda>( + Expression.MakeMemberAccess( + Expression.Convert(isCompletedParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterIsCompletedProperty), + isCompletedParam).Compile(); + + var getResultParam = Expression.Parameter(typeof(object), "awaiter"); + Func getResultFunc; + if (awaitableInfo.ResultType == typeof(void)) + { + // var getResultFunc = (object awaiter) => + // { + // ((CustomAwaiterType)awaiter).GetResult(); // We need to invoke this to surface any exceptions + // return (object)null; + // }; + getResultFunc = Expression.Lambda>( + Expression.Block( + Expression.Call( + Expression.Convert(getResultParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterGetResultMethod), + Expression.Constant(null) + ), + getResultParam).Compile(); + } + else + { + // var getResultFunc = (object awaiter) => + // (object)((CustomAwaiterType)awaiter).GetResult(); + getResultFunc = Expression.Lambda>( + Expression.Convert( + Expression.Call( + Expression.Convert(getResultParam, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterGetResultMethod), + typeof(object)), + getResultParam).Compile(); + } + + // var onCompletedFunc = (object awaiter, Action continuation) => { + // ((CustomAwaiterType)awaiter).OnCompleted(continuation); + // }; + var onCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); + var onCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); + var onCompletedFunc = Expression.Lambda>( + Expression.Call( + Expression.Convert(onCompletedParam1, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterOnCompletedMethod, + onCompletedParam2), + onCompletedParam1, + onCompletedParam2).Compile(); + + Action unsafeOnCompletedFunc = null; + if (awaitableInfo.AwaiterUnsafeOnCompletedMethod != null) + { + // var unsafeOnCompletedFunc = (object awaiter, Action continuation) => { + // ((CustomAwaiterType)awaiter).UnsafeOnCompleted(continuation); + // }; + var unsafeOnCompletedParam1 = Expression.Parameter(typeof(object), "awaiter"); + var unsafeOnCompletedParam2 = Expression.Parameter(typeof(Action), "continuation"); + unsafeOnCompletedFunc = Expression.Lambda>( + Expression.Call( + Expression.Convert(unsafeOnCompletedParam1, awaitableInfo.AwaiterType), + awaitableInfo.AwaiterUnsafeOnCompletedMethod, + unsafeOnCompletedParam2), + unsafeOnCompletedParam1, + unsafeOnCompletedParam2).Compile(); + } + + // If we need to pass the method call result through a coercer function to get an + // awaitable, then do so. + var coercedMethodCall = coercedAwaitableInfo.RequiresCoercion + ? Expression.Invoke(coercedAwaitableInfo.CoercerExpression, methodCall) + : (Expression)methodCall; + + // return new ObjectMethodExecutorAwaitable( + // (object)coercedMethodCall, + // getAwaiterFunc, + // isCompletedFunc, + // getResultFunc, + // onCompletedFunc, + // unsafeOnCompletedFunc); + var returnValueExpression = Expression.New( + _objectMethodExecutorAwaitableConstructor, + Expression.Convert(coercedMethodCall, typeof(object)), + Expression.Constant(getAwaiterFunc), + Expression.Constant(isCompletedFunc), + Expression.Constant(getResultFunc), + Expression.Constant(onCompletedFunc), + Expression.Constant(unsafeOnCompletedFunc, typeof(Action))); + + var lambda = Expression.Lambda(returnValueExpression, targetParameter, parametersParameter); + return lambda.Compile(); + } + } +} diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs new file mode 100644 index 0000000..7509b86 --- /dev/null +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorAwaitable.cs @@ -0,0 +1,114 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Provides a common awaitable structure that can + /// return, regardless of whether the underlying value is a System.Task, an FSharpAsync, or an + /// application-defined custom awaitable. + /// + internal struct ObjectMethodExecutorAwaitable + { + private readonly object _customAwaitable; + private readonly Func _getAwaiterMethod; + private readonly Func _isCompletedMethod; + private readonly Func _getResultMethod; + private readonly Action _onCompletedMethod; + private readonly Action _unsafeOnCompletedMethod; + + // Perf note: since we're requiring the customAwaitable to be supplied here as an object, + // this will trigger a further allocation if it was a value type (i.e., to box it). We can't + // fix this by making the customAwaitable type generic, because the calling code typically + // does not know the type of the awaitable/awaiter at compile-time anyway. + // + // However, we could fix it by not passing the customAwaitable here at all, and instead + // passing a func that maps directly from the target object (e.g., controller instance), + // target method (e.g., action method info), and params array to the custom awaiter in the + // GetAwaiter() method below. In effect, by delaying the actual method call until the + // upstream code calls GetAwaiter on this ObjectMethodExecutorAwaitable instance. + // This optimization is not currently implemented because: + // [1] It would make no difference when the awaitable was an object type, which is + // by far the most common scenario (e.g., System.Task). + // [2] It would be complex - we'd need some kind of object pool to track all the parameter + // arrays until we needed to use them in GetAwaiter(). + // We can reconsider this in the future if there's a need to optimize for ValueTask + // or other value-typed awaitables. + + public ObjectMethodExecutorAwaitable( + object customAwaitable, + Func getAwaiterMethod, + Func isCompletedMethod, + Func getResultMethod, + Action onCompletedMethod, + Action unsafeOnCompletedMethod) + { + _customAwaitable = customAwaitable; + _getAwaiterMethod = getAwaiterMethod; + _isCompletedMethod = isCompletedMethod; + _getResultMethod = getResultMethod; + _onCompletedMethod = onCompletedMethod; + _unsafeOnCompletedMethod = unsafeOnCompletedMethod; + } + + public Awaiter GetAwaiter() + { + var customAwaiter = _getAwaiterMethod(_customAwaitable); + return new Awaiter(customAwaiter, _isCompletedMethod, _getResultMethod, _onCompletedMethod, _unsafeOnCompletedMethod); + } + + public struct Awaiter : ICriticalNotifyCompletion + { + private readonly object _customAwaiter; + private readonly Func _isCompletedMethod; + private readonly Func _getResultMethod; + private readonly Action _onCompletedMethod; + private readonly Action _unsafeOnCompletedMethod; + + public Awaiter( + object customAwaiter, + Func isCompletedMethod, + Func getResultMethod, + Action onCompletedMethod, + Action unsafeOnCompletedMethod) + { + _customAwaiter = customAwaiter; + _isCompletedMethod = isCompletedMethod; + _getResultMethod = getResultMethod; + _onCompletedMethod = onCompletedMethod; + _unsafeOnCompletedMethod = unsafeOnCompletedMethod; + } + + public bool IsCompleted => _isCompletedMethod(_customAwaiter); + + public object GetResult() => _getResultMethod(_customAwaiter); + + public void OnCompleted(Action continuation) + { + _onCompletedMethod(_customAwaiter, continuation); + } + + public void UnsafeOnCompleted(Action continuation) + { + // If the underlying awaitable implements ICriticalNotifyCompletion, use its UnsafeOnCompleted. + // If not, fall back on using its OnCompleted. + // + // Why this is safe: + // - Implementing ICriticalNotifyCompletion is a way of saying the caller can choose whether it + // needs the execution context to be preserved (which it signals by calling OnCompleted), or + // that it doesn't (which it signals by calling UnsafeOnCompleted). Obviously it's faster *not* + // to preserve and restore the context, so we prefer that where possible. + // - If a caller doesn't need the execution context to be preserved and hence calls UnsafeOnCompleted, + // there's no harm in preserving it anyway - it's just a bit of wasted cost. That's what will happen + // if a caller sees that the proxy implements ICriticalNotifyCompletion but the proxy chooses to + // pass the call on to the underlying awaitable's OnCompleted method. + + var underlyingMethodToUse = _unsafeOnCompletedMethod ?? _onCompletedMethod; + underlyingMethodToUse(_customAwaiter, continuation); + } + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs new file mode 100644 index 0000000..2198c0c --- /dev/null +++ b/src/DotNetCore.CAP/Internal/ObjectMethodExecutor/ObjectMethodExecutorFSharpSupport.cs @@ -0,0 +1,151 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Helper for detecting whether a given type is FSharpAsync`1, and if so, supplying + /// an for mapping instances of that type to a C# awaitable. + /// + /// + /// The main design goal here is to avoid taking a compile-time dependency on + /// FSharp.Core.dll, because non-F# applications wouldn't use it. So all the references + /// to FSharp types have to be constructed dynamically at runtime. + /// + internal static class ObjectMethodExecutorFSharpSupport + { + private static object _fsharpValuesCacheLock = new object(); + private static Assembly _fsharpCoreAssembly; + private static MethodInfo _fsharpAsyncStartAsTaskGenericMethod; + private static PropertyInfo _fsharpOptionOfTaskCreationOptionsNoneProperty; + private static PropertyInfo _fsharpOptionOfCancellationTokenNoneProperty; + + public static bool TryBuildCoercerFromFSharpAsyncToAwaitable( + Type possibleFSharpAsyncType, + out Expression coerceToAwaitableExpression, + out Type awaitableType) + { + var methodReturnGenericType = possibleFSharpAsyncType.IsGenericType + ? possibleFSharpAsyncType.GetGenericTypeDefinition() + : null; + + if (!IsFSharpAsyncOpenGenericType(methodReturnGenericType)) + { + coerceToAwaitableExpression = null; + awaitableType = null; + return false; + } + + var awaiterResultType = possibleFSharpAsyncType.GetGenericArguments().Single(); + awaitableType = typeof(Task<>).MakeGenericType(awaiterResultType); + + // coerceToAwaitableExpression = (object fsharpAsync) => + // { + // return (object)FSharpAsync.StartAsTask( + // (Microsoft.FSharp.Control.FSharpAsync)fsharpAsync, + // FSharpOption.None, + // FSharpOption.None); + // }; + var startAsTaskClosedMethod = _fsharpAsyncStartAsTaskGenericMethod + .MakeGenericMethod(awaiterResultType); + var coerceToAwaitableParam = Expression.Parameter(typeof(object)); + coerceToAwaitableExpression = Expression.Lambda( + Expression.Convert( + Expression.Call( + startAsTaskClosedMethod, + Expression.Convert(coerceToAwaitableParam, possibleFSharpAsyncType), + Expression.MakeMemberAccess(null, _fsharpOptionOfTaskCreationOptionsNoneProperty), + Expression.MakeMemberAccess(null, _fsharpOptionOfCancellationTokenNoneProperty)), + typeof(object)), + coerceToAwaitableParam); + + return true; + } + + private static bool IsFSharpAsyncOpenGenericType(Type possibleFSharpAsyncGenericType) + { + var typeFullName = possibleFSharpAsyncGenericType?.FullName; + if (!string.Equals(typeFullName, "Microsoft.FSharp.Control.FSharpAsync`1", StringComparison.Ordinal)) + { + return false; + } + + lock (_fsharpValuesCacheLock) + { + if (_fsharpCoreAssembly != null) + { + // Since we've already found the real FSharpAsync.Core assembly, we just have + // to check that the supplied FSharpAsync`1 type is the one from that assembly. + return possibleFSharpAsyncGenericType.Assembly == _fsharpCoreAssembly; + } + else + { + // We'll keep trying to find the FSharp types/values each time any type called + // FSharpAsync`1 is supplied. + return TryPopulateFSharpValueCaches(possibleFSharpAsyncGenericType); + } + } + } + + private static bool TryPopulateFSharpValueCaches(Type possibleFSharpAsyncGenericType) + { + var assembly = possibleFSharpAsyncGenericType.Assembly; + var fsharpOptionType = assembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); + var fsharpAsyncType = assembly.GetType("Microsoft.FSharp.Control.FSharpAsync"); + + if (fsharpOptionType == null || fsharpAsyncType == null) + { + return false; + } + + // Get a reference to FSharpOption.None + var fsharpOptionOfTaskCreationOptionsType = fsharpOptionType + .MakeGenericType(typeof(TaskCreationOptions)); + _fsharpOptionOfTaskCreationOptionsNoneProperty = fsharpOptionOfTaskCreationOptionsType + .GetTypeInfo() + .GetRuntimeProperty("None"); + + // Get a reference to FSharpOption.None + var fsharpOptionOfCancellationTokenType = fsharpOptionType + .MakeGenericType(typeof(CancellationToken)); + _fsharpOptionOfCancellationTokenNoneProperty = fsharpOptionOfCancellationTokenType + .GetTypeInfo() + .GetRuntimeProperty("None"); + + // Get a reference to FSharpAsync.StartAsTask<> + var fsharpAsyncMethods = fsharpAsyncType + .GetRuntimeMethods() + .Where(m => m.Name.Equals("StartAsTask", StringComparison.Ordinal)); + foreach (var candidateMethodInfo in fsharpAsyncMethods) + { + var parameters = candidateMethodInfo.GetParameters(); + if (parameters.Length == 3 + && TypesHaveSameIdentity(parameters[0].ParameterType, possibleFSharpAsyncGenericType) + && parameters[1].ParameterType == fsharpOptionOfTaskCreationOptionsType + && parameters[2].ParameterType == fsharpOptionOfCancellationTokenType) + { + // This really does look like the correct method (and hence assembly). + _fsharpAsyncStartAsTaskGenericMethod = candidateMethodInfo; + _fsharpCoreAssembly = assembly; + break; + } + } + + return _fsharpCoreAssembly != null; + } + + private static bool TypesHaveSameIdentity(Type type1, Type type2) + { + return type1.Assembly == type2.Assembly + && string.Equals(type1.Namespace, type2.Namespace, StringComparison.Ordinal) + && string.Equals(type1.Name, type2.Name, StringComparison.Ordinal); + } + } +} diff --git a/src/DotNetCore.CAP/Models/CapPublishedMessage.cs b/src/DotNetCore.CAP/Models/CapPublishedMessage.cs index c23f24d..e02fcb2 100644 --- a/src/DotNetCore.CAP/Models/CapPublishedMessage.cs +++ b/src/DotNetCore.CAP/Models/CapPublishedMessage.cs @@ -34,5 +34,10 @@ namespace DotNetCore.CAP.Models public int Retries { get; set; } public string StatusName { get; set; } + + public override string ToString() + { + return "name:" + Name + ", content:" + Content; + } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Models/CapReceivedMessage.cs b/src/DotNetCore.CAP/Models/CapReceivedMessage.cs index 069e4d0..8efe24b 100644 --- a/src/DotNetCore.CAP/Models/CapReceivedMessage.cs +++ b/src/DotNetCore.CAP/Models/CapReceivedMessage.cs @@ -47,5 +47,10 @@ namespace DotNetCore.CAP.Models Content = Content }; } + + public override string ToString() + { + return "name:" + Name + ", content:" + Content; + } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP/Models/Message.cs b/src/DotNetCore.CAP/Models/Message.cs new file mode 100644 index 0000000..f55262a --- /dev/null +++ b/src/DotNetCore.CAP/Models/Message.cs @@ -0,0 +1,26 @@ +using System; + +namespace DotNetCore.CAP.Models +{ + public class Message + { + public string Id { get; set; } + + public DateTime Timestamp { get; set; } + + public object Content { get; set; } + + public string CallbackName { get; set; } + + public Message() + { + Id = ObjectId.GenerateNewStringId(); + Timestamp = DateTime.Now; + } + + public Message(object content) : this() + { + Content = content; + } + } +} \ No newline at end of file diff --git a/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs b/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs index 294591c..0a3f240 100644 --- a/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs +++ b/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs @@ -7,10 +7,20 @@ namespace DotNetCore.CAP.Processor.States { public const string StateName = "Succeeded"; - public TimeSpan? ExpiresAfter => TimeSpan.FromHours(1); + public TimeSpan? ExpiresAfter { get; private set; } public string Name => StateName; + public SucceededState() + { + ExpiresAfter = TimeSpan.FromHours(1); + } + + public SucceededState(int ExpireAfterSeconds) + { + ExpiresAfter = TimeSpan.FromSeconds(ExpireAfterSeconds); + } + public void Apply(CapPublishedMessage message, IStorageTransaction transaction) { } diff --git a/test/DotNetCore.CAP.MySql.Test/DatabaseTestHost.cs b/test/DotNetCore.CAP.MySql.Test/DatabaseTestHost.cs index bd858e5..1fac1b6 100644 --- a/test/DotNetCore.CAP.MySql.Test/DatabaseTestHost.cs +++ b/test/DotNetCore.CAP.MySql.Test/DatabaseTestHost.cs @@ -1,4 +1,3 @@ -using System.Data; using System.Threading; using Dapper; using Microsoft.EntityFrameworkCore; diff --git a/test/DotNetCore.CAP.MySql.Test/DotNetCore.CAP.MySql.Test.csproj b/test/DotNetCore.CAP.MySql.Test/DotNetCore.CAP.MySql.Test.csproj index cf89b39..be4dae5 100644 --- a/test/DotNetCore.CAP.MySql.Test/DotNetCore.CAP.MySql.Test.csproj +++ b/test/DotNetCore.CAP.MySql.Test/DotNetCore.CAP.MySql.Test.csproj @@ -1,22 +1,13 @@  - netcoreapp1.1 + netcoreapp2.0 true DotNetCore.CAP.MySql.Test DotNetCore.CAP.MySql.Test true - $(PackageTargetFallback);dnxcore50;portable-net451+win8 - 1.1.1 - false - false - false - - - - @@ -24,23 +15,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + \ No newline at end of file diff --git a/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs b/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs index b83588e..b3df31f 100644 --- a/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs +++ b/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs @@ -85,7 +85,6 @@ namespace DotNetCore.CAP.MySql.Test [Fact] public async Task GetReceivedMessageAsync_Test() { - var sql = $@" INSERT INTO `cap.received`(`Name`,`Group`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`) VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);SELECT @@IDENTITY;"; @@ -129,6 +128,5 @@ namespace DotNetCore.CAP.MySql.Test Assert.Equal("MySqlStorageConnectionTest", message.Name); Assert.Equal("mygroup", message.Group); } - } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.MySql.Test/MySqlStorageTest.cs b/test/DotNetCore.CAP.MySql.Test/MySqlStorageTest.cs index 9286929..715cb5c 100644 --- a/test/DotNetCore.CAP.MySql.Test/MySqlStorageTest.cs +++ b/test/DotNetCore.CAP.MySql.Test/MySqlStorageTest.cs @@ -1,5 +1,5 @@ -using Xunit; -using Dapper; +using Dapper; +using Xunit; namespace DotNetCore.CAP.MySql.Test { @@ -9,7 +9,6 @@ namespace DotNetCore.CAP.MySql.Test private readonly string _dbName; private readonly string _masterDbConnectionString; - public MySqlStorageTest() { _dbName = ConnectionUtil.GetDatabaseName(); @@ -29,36 +28,12 @@ namespace DotNetCore.CAP.MySql.Test } } - [Fact] - public void DatabaseTable_Published_IsExists() - { - var tableName = "cap.published"; - using (var connection = ConnectionUtil.CreateConnection(_masterDbConnectionString)) - { - var sql = $"SELECT TABLE_NAME FROM `TABLES` WHERE TABLE_SCHEMA='{_dbName}' AND TABLE_NAME = '{tableName}'"; - var result = connection.QueryFirstOrDefault(sql); - Assert.NotNull(result); - Assert.Equal(tableName, result); - } - } - - [Fact] - public void DatabaseTable_Queue_IsExists() - { - var tableName = "cap.queue"; - using (var connection = ConnectionUtil.CreateConnection(_masterDbConnectionString)) - { - var sql = $"SELECT TABLE_NAME FROM `TABLES` WHERE TABLE_SCHEMA='{_dbName}' AND TABLE_NAME = '{tableName}'"; - var result = connection.QueryFirstOrDefault(sql); - Assert.NotNull(result); - Assert.Equal(tableName, result); - } - } - - [Fact] - public void DatabaseTable_Received_IsExists() + [Theory] + [InlineData("cap.published")] + [InlineData("cap.queue")] + [InlineData("cap.received")] + public void DatabaseTable_IsExists(string tableName) { - var tableName = "cap.received"; using (var connection = ConnectionUtil.CreateConnection(_masterDbConnectionString)) { var sql = $"SELECT TABLE_NAME FROM `TABLES` WHERE TABLE_SCHEMA='{_dbName}' AND TABLE_NAME = '{tableName}'"; @@ -68,4 +43,4 @@ namespace DotNetCore.CAP.MySql.Test } } } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.MySql.Test/TestHost.cs b/test/DotNetCore.CAP.MySql.Test/TestHost.cs index c8290ad..06dcba9 100644 --- a/test/DotNetCore.CAP.MySql.Test/TestHost.cs +++ b/test/DotNetCore.CAP.MySql.Test/TestHost.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace DotNetCore.CAP.MySql.Test diff --git a/test/DotNetCore.CAP.PostgreSql.Test/ConnectionUtil.cs b/test/DotNetCore.CAP.PostgreSql.Test/ConnectionUtil.cs new file mode 100644 index 0000000..b293529 --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/ConnectionUtil.cs @@ -0,0 +1,47 @@ +using System; +using Npgsql; + +namespace DotNetCore.CAP.PostgreSql.Test +{ + public static class ConnectionUtil + { + private const string DatabaseVariable = "Cap_PostgreSql_DatabaseName"; + private const string ConnectionStringTemplateVariable = "Cap_PostgreSql_ConnectionStringTemplate"; + + private const string MasterDatabaseName = "postgres"; + private const string DefaultDatabaseName = @"DotNetCore.CAP.PostgreSql.Test"; + + private const string DefaultConnectionStringTemplate = + @"Server=localhost;Database={0};UserId=postgres;Password=123123;"; + + public static string GetDatabaseName() + { + return Environment.GetEnvironmentVariable(DatabaseVariable) ?? DefaultDatabaseName; + } + + public static string GetMasterConnectionString() + { + return string.Format(GetConnectionStringTemplate(), MasterDatabaseName); + } + + public static string GetConnectionString() + { + return string.Format(GetConnectionStringTemplate(), GetDatabaseName()); + } + + private static string GetConnectionStringTemplate() + { + return + Environment.GetEnvironmentVariable(ConnectionStringTemplateVariable) ?? + DefaultConnectionStringTemplate; + } + + public static NpgsqlConnection CreateConnection(string connectionString = null) + { + connectionString = connectionString ?? GetConnectionString(); + var connection = new NpgsqlConnection(connectionString); + connection.Open(); + return connection; + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.PostgreSql.Test/DatabaseTestHost.cs b/test/DotNetCore.CAP.PostgreSql.Test/DatabaseTestHost.cs new file mode 100644 index 0000000..66ae8e1 --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/DatabaseTestHost.cs @@ -0,0 +1,67 @@ +using System.Threading; +using Dapper; +using Microsoft.EntityFrameworkCore; + +namespace DotNetCore.CAP.PostgreSql.Test +{ + public abstract class DatabaseTestHost : TestHost + { + private static bool _sqlObjectInstalled; + public static object _lock = new object(); + + protected override void PostBuildServices() + { + base.PostBuildServices(); + lock (_lock) + { + if (!_sqlObjectInstalled) + { + InitializeDatabase(); + } + } + } + + public override void Dispose() + { + DeleteAllData(); + base.Dispose(); + } + + private void InitializeDatabase() + { + using (CreateScope()) + { + var storage = GetService(); + var token = new CancellationTokenSource().Token; + CreateDatabase(); + storage.InitializeAsync(token).GetAwaiter().GetResult(); + _sqlObjectInstalled = true; + } + } + + private void CreateDatabase() + { + var masterConn = ConnectionUtil.GetMasterConnectionString(); + var databaseName = ConnectionUtil.GetDatabaseName(); + using (var connection = ConnectionUtil.CreateConnection(masterConn)) + { + connection.Execute($@" +DROP DATABASE IF EXISTS ""{databaseName}""; +CREATE DATABASE ""{databaseName}"";"); + } + } + + private void DeleteAllData() + { + var conn = ConnectionUtil.GetConnectionString(); + + using (var connection = ConnectionUtil.CreateConnection(conn)) + { + connection.Execute($@" +TRUNCATE TABLE ""cap"".""published""; +TRUNCATE TABLE ""cap"".""received""; +TRUNCATE TABLE ""cap"".""queue"";"); + } + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.PostgreSql.Test/DotNetCore.CAP.PostgreSql.Test.csproj b/test/DotNetCore.CAP.PostgreSql.Test/DotNetCore.CAP.PostgreSql.Test.csproj new file mode 100644 index 0000000..0f116c8 --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/DotNetCore.CAP.PostgreSql.Test.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + + + diff --git a/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageConnectionTest.cs b/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageConnectionTest.cs new file mode 100644 index 0000000..df68d0b --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageConnectionTest.cs @@ -0,0 +1,132 @@ +using System; +using System.Threading.Tasks; +using Dapper; +using DotNetCore.CAP.Infrastructure; +using DotNetCore.CAP.Models; +using Xunit; + +namespace DotNetCore.CAP.PostgreSql.Test +{ + [Collection("postgresql")] + public class PostgreSqlStorageConnectionTest : DatabaseTestHost + { + private PostgreSqlStorageConnection _storage; + + public PostgreSqlStorageConnectionTest() + { + var options = GetService(); + _storage = new PostgreSqlStorageConnection(options); + } + + [Fact] + 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 publishMessage = new CapPublishedMessage + { + Name = "PostgreSqlStorageConnectionTest", + Content = "", + StatusName = StatusName.Scheduled + }; + var insertedId = default(int); + using (var connection = ConnectionUtil.CreateConnection()) + { + insertedId = connection.QueryFirst(sql, publishMessage); + } + var message = await _storage.GetPublishedMessageAsync(insertedId); + Assert.NotNull(message); + Assert.Equal("PostgreSqlStorageConnectionTest", message.Name); + Assert.Equal(StatusName.Scheduled, message.StatusName); + } + + [Fact] + public async Task FetchNextMessageAsync_Test() + { + var sql = @"INSERT INTO ""cap"".""queue""(""MessageId"",""MessageType"") VALUES(@MessageId,@MessageType);"; + var queue = new CapQueue + { + MessageId = 3333, + MessageType = MessageType.Publish + }; + using (var connection = ConnectionUtil.CreateConnection()) + { + connection.Execute(sql, queue); + } + var fetchedMessage = await _storage.FetchNextMessageAsync(); + fetchedMessage.Dispose(); + Assert.NotNull(fetchedMessage); + Assert.Equal(MessageType.Publish, fetchedMessage.MessageType); + Assert.Equal(3333, fetchedMessage.MessageId); + } + + [Fact] + public async Task StoreReceivedMessageAsync_Test() + { + var receivedMessage = new CapReceivedMessage + { + Name = "PostgreSqlStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + + Exception exception = null; + try + { + await _storage.StoreReceivedMessageAsync(receivedMessage); + } + catch (Exception ex) + { + exception = ex; + } + Assert.Null(exception); + } + + [Fact] + public async Task GetReceivedMessageAsync_Test() + { + var sql = $@" + INSERT INTO ""cap"".""received""(""Name"",""Group"",""Content"",""Retries"",""Added"",""ExpiresAt"",""StatusName"") + VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName) RETURNING ""Id"";"; + var receivedMessage = new CapReceivedMessage + { + Name = "PostgreSqlStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + var insertedId = default(int); + using (var connection = ConnectionUtil.CreateConnection()) + { + insertedId = connection.QueryFirst(sql, receivedMessage); + } + + var message = await _storage.GetReceivedMessageAsync(insertedId); + + Assert.NotNull(message); + Assert.Equal(StatusName.Scheduled, message.StatusName); + Assert.Equal("PostgreSqlStorageConnectionTest", message.Name); + Assert.Equal("mygroup", message.Group); + } + + [Fact] + public async Task GetNextReceviedMessageToBeEnqueuedAsync_Test() + { + var receivedMessage = new CapReceivedMessage + { + Name = "PostgreSqlStorageConnectionTest", + Content = "", + Group = "mygroup", + StatusName = StatusName.Scheduled + }; + await _storage.StoreReceivedMessageAsync(receivedMessage); + + var message = await _storage.GetNextReceviedMessageToBeEnqueuedAsync(); + + Assert.NotNull(message); + Assert.Equal(StatusName.Scheduled, message.StatusName); + Assert.Equal("PostgreSqlStorageConnectionTest", message.Name); + Assert.Equal("mygroup", message.Group); + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageTest.cs b/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageTest.cs new file mode 100644 index 0000000..c4f5748 --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/PostgreSqlStorageTest.cs @@ -0,0 +1,47 @@ +using Dapper; +using Xunit; + +namespace DotNetCore.CAP.PostgreSql.Test +{ + [Collection("postgresql")] + public class SqlServerStorageTest : DatabaseTestHost + { + private readonly string _dbName; + private readonly string _masterDbConnectionString; + private readonly string _dbConnectionString; + + public SqlServerStorageTest() + { + _dbName = ConnectionUtil.GetDatabaseName(); + _masterDbConnectionString = ConnectionUtil.GetMasterConnectionString(); + _dbConnectionString = ConnectionUtil.GetConnectionString(); + } + + [Fact] + public void Database_IsExists() + { + using (var connection = ConnectionUtil.CreateConnection(_masterDbConnectionString)) + { + var databaseName = ConnectionUtil.GetDatabaseName(); + var sql = $@"select * from pg_database where datname = '{databaseName}'"; + var result = connection.QueryFirstOrDefault(sql); + Assert.NotNull(result); + Assert.True(databaseName.Equals(result, System.StringComparison.CurrentCultureIgnoreCase)); + } + } + + [Theory] + [InlineData("cap.published")] + [InlineData("cap.queue")] + [InlineData("cap.received")] + public void DatabaseTable_IsExists(string tableName) + { + using (var connection = ConnectionUtil.CreateConnection(_dbConnectionString)) + { + var sql = $"SELECT to_regclass('{tableName}') is not null;"; + var result = connection.QueryFirstOrDefault(sql); + Assert.True(result); + } + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.PostgreSql.Test/TestHost.cs b/test/DotNetCore.CAP.PostgreSql.Test/TestHost.cs new file mode 100644 index 0000000..4bff8cb --- /dev/null +++ b/test/DotNetCore.CAP.PostgreSql.Test/TestHost.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace DotNetCore.CAP.PostgreSql.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 PostgreSqlOptions { ConnectionString = _connectionString }); + services.AddSingleton(); + + _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().CreateScope(); + return scope; + } + + public T GetService() => Provider.GetService(); + + public T Ensure(ref T service) + where T : class + => service ?? (service = GetService()); + + 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 diff --git a/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs b/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs index d759384..5bf5a7d 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/DatabaseTestHost.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.SqlClient; using System.Threading; using Dapper; using Microsoft.EntityFrameworkCore; @@ -54,21 +55,20 @@ CREATE DATABASE [{databaseName}];"); private void DeleteAllData() { - using (CreateScope()) + var conn = ConnectionUtil.GetConnectionString(); + using (var connection = new SqlConnection(conn)) { - var context = GetService(); - - var commands = new[] - { + var commands = new[] { "DISABLE TRIGGER ALL ON ?", "ALTER TABLE ? NOCHECK CONSTRAINT ALL", "DELETE FROM ?", "ALTER TABLE ? CHECK CONSTRAINT ALL", "ENABLE TRIGGER ALL ON ?" }; + foreach (var command in commands) { - context.Database.GetDbConnection().Execute( + connection.Execute( "sp_MSforeachtable", new { command1 = command }, commandType: CommandType.StoredProcedure); diff --git a/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj b/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj index 177d825..1a8cc9a 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj +++ b/test/DotNetCore.CAP.SqlServer.Test/DotNetCore.CAP.SqlServer.Test.csproj @@ -1,48 +1,30 @@  - netcoreapp1.1 - true - DotNetCore.CAP.SqlServer.Test - DotNetCore.CAP.SqlServer.Test - true - $(PackageTargetFallback);dnxcore50;portable-net451+win8 - 1.1.1 - false - false - false + netcoreapp2.0 + false - - - - - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/test/DotNetCore.CAP.SqlServer.Test/Properties/AssemblyInfo.cs b/test/DotNetCore.CAP.SqlServer.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index a995715..0000000 --- a/test/DotNetCore.CAP.SqlServer.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DotNetCore.CAP.EntityFrameworkCore.Test")] -[assembly: AssemblyTrademark("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("7442c942-1ddc-40e4-8f1b-654e721eaa45")] \ No newline at end of file diff --git a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs index bd0bab4..b081d1a 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs @@ -85,7 +85,6 @@ namespace DotNetCore.CAP.SqlServer.Test [Fact] public async Task GetReceivedMessageAsync_Test() { - var sql = $@" INSERT INTO [Cap].[Received]([Name],[Group],[Content],[Retries],[Added],[ExpiresAt],[StatusName]) OUTPUT INSERTED.Id VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; @@ -129,6 +128,5 @@ namespace DotNetCore.CAP.SqlServer.Test Assert.Equal("SqlServerStorageConnectionTest", message.Name); Assert.Equal("mygroup", message.Group); } - } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs index 38fb6c1..af5fc41 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageTest.cs @@ -1,5 +1,5 @@ -using Xunit; -using Dapper; +using Dapper; +using Xunit; namespace DotNetCore.CAP.SqlServer.Test { @@ -14,58 +14,31 @@ namespace DotNetCore.CAP.SqlServer.Test { var databaseName = ConnectionUtil.GetDatabaseName(); var sql = $@" -IF EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}') +IF EXISTS (SELECT * FROM sysdatabases WHERE name = N'{databaseName}') SELECT 'True' ELSE SELECT 'False'"; var result = connection.QueryFirst(sql); - Assert.Equal(true, result); + Assert.True(result); } } - [Fact] - public void DatabaseTable_Published_IsExists() - { - using (var connection = ConnectionUtil.CreateConnection()) - { - var sql = @" -IF OBJECT_ID(N'[Cap].[Published]',N'U') IS NOT NULL -SELECT 'True' -ELSE -SELECT 'False'"; - var result = connection.QueryFirst(sql); - Assert.Equal(true, result); - } - } - - [Fact] - public void DatabaseTable_Queue_IsExists() + [Theory] + [InlineData("[Cap].[Published]")] + [InlineData("[Cap].[Queue]")] + [InlineData("[Cap].[Received]")] + public void DatabaseTable_IsExists(string tableName) { using (var connection = ConnectionUtil.CreateConnection()) { - var sql = @" -IF OBJECT_ID(N'[Cap].[Queue]',N'U') IS NOT NULL -SELECT 'True' -ELSE -SELECT 'False'"; - var result = connection.QueryFirst(sql); - Assert.Equal(true, result); - } - } - - [Fact] - public void DatabaseTable_Received_IsExists() - { - using (var connection = ConnectionUtil.CreateConnection()) - { - var sql = @" -IF OBJECT_ID(N'[Cap].[Received]',N'U') IS NOT NULL + var sql = $@" +IF OBJECT_ID(N'{tableName}',N'U') IS NOT NULL SELECT 'True' ELSE SELECT 'False'"; var result = connection.QueryFirst(sql); - Assert.Equal(true, result); + Assert.True(result); } } } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs b/test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs deleted file mode 100644 index d59bdf1..0000000 --- a/test/DotNetCore.CAP.SqlServer.Test/TestDbContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace DotNetCore.CAP.SqlServer.Test -{ - public class TestDbContext : DbContext - { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - var connectionString = ConnectionUtil.GetConnectionString(); - optionsBuilder.UseSqlServer(connectionString); - } - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs b/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs index 31cbfd1..d8618af 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/TestHost.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace DotNetCore.CAP.SqlServer.Test @@ -31,7 +30,6 @@ namespace DotNetCore.CAP.SqlServer.Test _connectionString = ConnectionUtil.GetConnectionString(); services.AddSingleton(new SqlServerOptions { ConnectionString = _connectionString }); services.AddSingleton(); - services.AddDbContext(options => options.UseSqlServer(_connectionString)); _services = services; } diff --git a/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs b/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs index 0bd3c8f..a151d72 100644 --- a/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs +++ b/test/DotNetCore.CAP.Test/CAP.BuilderTest.cs @@ -1,8 +1,8 @@ using System; +using System.Data; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; -using System.Data; namespace DotNetCore.CAP.Test { @@ -36,7 +36,6 @@ namespace DotNetCore.CAP.Test Assert.NotNull(markService); } - [Fact] public void CanOverridePublishService() { @@ -61,47 +60,47 @@ namespace DotNetCore.CAP.Test private class MyProducerService : ICapPublisher { - public void Publish(string name, string content) + public void Publish(string name, T contentObj, string callbackName = null) { throw new NotImplementedException(); } - public void Publish(string name, T contentObj) + public void Publish(string name, T contentObj, IDbConnection dbConnection, string callbackName = null, IDbTransaction dbTransaction = null) { throw new NotImplementedException(); } - public void Publish(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction = null) + public Task PublishAsync(string topic, string content) { throw new NotImplementedException(); } - public void Publish(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) + public Task PublishAsync(string topic, T contentObj) { throw new NotImplementedException(); } - public Task PublishAsync(string topic, string content) + public Task PublishAsync(string topic, string content, IDbConnection dbConnection) { throw new NotImplementedException(); } - public Task PublishAsync(string topic, T contentObj) + public Task PublishAsync(string topic, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) { throw new NotImplementedException(); } - public Task PublishAsync(string topic, string content, IDbConnection dbConnection) + public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) { throw new NotImplementedException(); } - public Task PublishAsync(string topic, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + public Task PublishAsync(string name, T contentObj, string callbackName = null) { throw new NotImplementedException(); } - public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) + public Task PublishAsync(string name, T contentObj, IDbConnection dbConnection, string callbackName = null, IDbTransaction dbTransaction = null) { throw new NotImplementedException(); } diff --git a/test/DotNetCore.CAP.Test/ConsumerInvokerFactoryTest.cs b/test/DotNetCore.CAP.Test/ConsumerInvokerFactoryTest.cs new file mode 100644 index 0000000..bf070fe --- /dev/null +++ b/test/DotNetCore.CAP.Test/ConsumerInvokerFactoryTest.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using System.Reflection; +using DotNetCore.CAP.Abstractions; +using DotNetCore.CAP.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class ConsumerInvokerFactoryTest + { + private IConsumerInvokerFactory consumerInvokerFactory; + + public ConsumerInvokerFactoryTest() + { + var services = new ServiceCollection(); + services.AddLogging(); + var provider = services.BuildServiceProvider(); + var logFactory = provider.GetRequiredService(); + var binder = new ModelBinderFactory(); + + consumerInvokerFactory = new ConsumerInvokerFactory(logFactory, binder, provider); + } + + [Fact] + public void CreateInvokerTest() + { + var methodInfo = typeof(Sample).GetRuntimeMethods() + .Single(x => x.Name == nameof(Sample.ThrowException)); + + var description = new ConsumerExecutorDescriptor + { + MethodInfo = methodInfo, + ImplTypeInfo = typeof(Sample).GetTypeInfo() + }; + var messageContext = new MessageContext(); + + var context = new ConsumerContext(description, messageContext); + + var invoker = consumerInvokerFactory.CreateInvoker(context); + + Assert.NotNull(invoker); + } + + [Theory] + [InlineData(nameof(Sample.ThrowException))] + [InlineData(nameof(Sample.AsyncMethod))] + public async void InvokeMethodTest(string methodName) + { + var methodInfo = typeof(Sample).GetRuntimeMethods() + .Single(x => x.Name == methodName); + + var description = new ConsumerExecutorDescriptor + { + MethodInfo = methodInfo, + ImplTypeInfo = typeof(Sample).GetTypeInfo() + }; + var messageContext = new MessageContext(); + + var context = new ConsumerContext(description, messageContext); + + var invoker = consumerInvokerFactory.CreateInvoker(context); + + await Assert.ThrowsAsync(typeof(Exception), async () => + { + await invoker.InvokeAsync(); + }); + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs index 1fc5ca8..5ed56e1 100644 --- a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs +++ b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using DotNetCore.CAP.Abstractions; -using DotNetCore.CAP.Internal; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -20,7 +17,7 @@ namespace DotNetCore.CAP.Test services.AddScoped(); services.AddScoped(); services.AddLogging(); - services.AddCap(x=> { }); + services.AddCap(x => { }); _provider = services.BuildServiceProvider(); } @@ -42,7 +39,7 @@ namespace DotNetCore.CAP.Test Assert.NotNull(bestCandidates); Assert.NotNull(bestCandidates.MethodInfo); - Assert.Equal(bestCandidates.MethodInfo.ReturnType, typeof(Task)); + Assert.Equal(typeof(Task), bestCandidates.MethodInfo.ReturnType); } } diff --git a/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj b/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj index 81b5e2d..b4d7bae 100644 --- a/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj +++ b/test/DotNetCore.CAP.Test/DotNetCore.CAP.Test.csproj @@ -1,36 +1,25 @@  - netcoreapp1.1 + netcoreapp2.0 true DotNetCore.CAP.Test - DotNetCore.CAP.Test true - $(PackageTargetFallback);dnxcore50;portable-net451+win8 - 1.1.1 - - - - - + - - - - - - + + + + + + - - - - diff --git a/test/DotNetCore.CAP.Test/ModelBinderFactoryTest.cs b/test/DotNetCore.CAP.Test/ModelBinderFactoryTest.cs new file mode 100644 index 0000000..946db42 --- /dev/null +++ b/test/DotNetCore.CAP.Test/ModelBinderFactoryTest.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Reflection; +using DotNetCore.CAP.Internal; +using Xunit; + +namespace DotNetCore.CAP.Test +{ + public class ModelBinderFactoryTest + { + private IModelBinderFactory _factory; + + public ModelBinderFactoryTest() + { + _factory = new ModelBinderFactory(); + } + + [Theory] + [InlineData(nameof(Sample.DateTimeParam))] + [InlineData(nameof(Sample.StringParam))] + [InlineData(nameof(Sample.IntegerParam))] + [InlineData(nameof(Sample.GuidParam))] + [InlineData(nameof(Sample.UriParam))] + public void CreateSimpleTypeBinderTest(string methodName) + { + var methodInfo = typeof(Sample).GetRuntimeMethods().Single(x => x.Name == methodName); + var binder = _factory.CreateBinder(methodInfo.GetParameters()[0]); + Assert.NotNull(binder); + Assert.True(binder is SimpleTypeModelBinder); + Assert.False(binder is ComplexTypeModelBinder); + } + + [Theory] + [InlineData(nameof(Sample.ComplexTypeParam))] + public void CreateComplexTypeBinderTest(string methodName) + { + var methodInfo = typeof(Sample).GetRuntimeMethods().Single(x => x.Name == methodName); + var binder = _factory.CreateBinder(methodInfo.GetParameters()[0]); + Assert.NotNull(binder); + Assert.False(binder is SimpleTypeModelBinder); + Assert.True(binder is ComplexTypeModelBinder); + } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/ObjectMethodExecutorTest.cs b/test/DotNetCore.CAP.Test/ObjectMethodExecutorTest.cs deleted file mode 100644 index 69fb54d..0000000 --- a/test/DotNetCore.CAP.Test/ObjectMethodExecutorTest.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Reflection; -using DotNetCore.CAP.Internal; -using Xunit; - -namespace DotNetCore.CAP.Test -{ - public class ObjectMethodExecutorTest - { - [Fact] - public void CanCreateInstance() - { - var testClass = new MethodExecutorClass(); - var methodInfo = testClass.GetType().GetMethod("Foo"); - - var executor = ObjectMethodExecutor.Create(methodInfo, typeof(MethodExecutorClass).GetTypeInfo()); - - Assert.NotNull(executor); - } - - [Fact] - public void CanExcuteMethodWithNoParameters() - { - var testClass = new MethodExecutorClass(); - var methodInfo = testClass.GetType().GetMethod("GetThree"); - - var executor = ObjectMethodExecutor.Create(methodInfo, typeof(MethodExecutorClass).GetTypeInfo()); - - Assert.NotNull(executor); - - var objResult = executor.Execute(testClass, null); - - Assert.Equal(3, objResult); - } - - [Fact] - public void CanExcuteMethodWithParameters() - { - var testClass = new MethodExecutorClass(); - var methodInfo = testClass.GetType().GetMethod("Add"); - - var executor = ObjectMethodExecutor.Create(methodInfo, typeof(MethodExecutorClass).GetTypeInfo()); - - Assert.NotNull(executor); - - var objResult = executor.Execute(testClass, 1, 2); - - Assert.Equal(3, objResult); - } - - - [Fact] - public void CanGetExcuteMethodDefaultValue() - { - var testClass = new MethodExecutorClass(); - var methodInfo = testClass.GetType().GetMethod("WithDefaultValue"); - - var executor = ObjectMethodExecutor.Create(methodInfo, typeof(MethodExecutorClass).GetTypeInfo()); - - var objResult = executor.GetDefaultValueForParameter(0); - Assert.Equal("aaa", objResult); - - var objResult2 = executor.GetDefaultValueForParameter(1); - Assert.Equal("bbb", objResult2); - } - } - - public class MethodExecutorClass - { - public void Foo() - { - } - - public int GetThree() - { - return 3; - } - - public int Add(int a, int b) - { - return a + b; - } - - public void WithDefaultValue(string aaa = "aaa", string bbb = "bbb") - { - } - } -} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs b/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs index 7ff1d19..8ccdbf0 100644 --- a/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs +++ b/test/DotNetCore.CAP.Test/Processor/DefaultDispatcherTest.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -using DotNetCore.CAP.Infrastructure; using DotNetCore.CAP.Models; using DotNetCore.CAP.Processor; -using DotNetCore.CAP.Processor.States; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; @@ -64,12 +60,12 @@ namespace DotNetCore.CAP.Test public async Task ProcessAsync() { // Arrange - var job = new CapPublishedMessage { - + var job = new CapPublishedMessage + { }; - var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.MessageId == 42 && fj.MessageType == MessageType.Publish )); - + var mockFetchedJob = Mock.Get(Mock.Of(fj => fj.MessageId == 42 && fj.MessageType == MessageType.Publish)); + _mockStorageConnection .Setup(m => m.FetchNextMessageAsync()) .ReturnsAsync(mockFetchedJob.Object).Verifiable(); @@ -84,7 +80,7 @@ namespace DotNetCore.CAP.Test await fixture.ProcessAsync(_context); // Assert - _mockStorageConnection.VerifyAll(); + _mockStorageConnection.VerifyAll(); } private DefaultDispatcher Create() diff --git a/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs b/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs index 51101bd..9ff5208 100644 --- a/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs +++ b/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs @@ -25,7 +25,7 @@ namespace DotNetCore.CAP.Test fixture.ChangeState(message, state, mockTransaction.Object); // Assert - Assert.Equal(message.StatusName, "s"); + Assert.Equal("s", message.StatusName); Assert.Null(message.ExpiresAt); Mock.Get(state).Verify(s => s.Apply(message, mockTransaction.Object), Times.Once); mockTransaction.Verify(t => t.UpdateMessage(message), Times.Once); @@ -48,7 +48,7 @@ namespace DotNetCore.CAP.Test fixture.ChangeState(message, state, mockTransaction.Object); // Assert - Assert.Equal(message.StatusName, "s"); + Assert.Equal("s", message.StatusName); Assert.NotNull(message.ExpiresAt); mockTransaction.Verify(t => t.UpdateMessage(message), Times.Once); mockTransaction.Verify(t => t.CommitAsync(), Times.Never); @@ -56,4 +56,4 @@ namespace DotNetCore.CAP.Test private StateChanger Create() => new StateChanger(); } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs b/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs index f1bf1e3..7292832 100644 --- a/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs +++ b/test/DotNetCore.CAP.Test/QueueExecutorFactoryTest.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -39,10 +37,7 @@ namespace DotNetCore.CAP.Test Assert.NotNull(queueExecutorFactory); var publishExecutor = queueExecutorFactory.GetInstance(Models.MessageType.Publish); - Assert.Equal(null, publishExecutor); + Assert.Null(publishExecutor); } - - - } -} +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/Sample.cs b/test/DotNetCore.CAP.Test/Sample.cs new file mode 100644 index 0000000..f043298 --- /dev/null +++ b/test/DotNetCore.CAP.Test/Sample.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; + +namespace DotNetCore.CAP.Test +{ + public class Sample + { + public void DateTimeParam(DateTime dateTime) + { + } + + public void StringParam(string @string) + { + } + + public void GuidParam(Guid guid) + { + } + + public void UriParam(Uri uri) + { + } + + public void IntegerParam(int @int) + { + } + + public void ComplexTypeParam(ComplexType complexType) + { + } + + public void ThrowException() + { + throw new Exception(); + } + + public async Task AsyncMethod() + { + await Task.FromResult(3); + throw new Exception(); + } + } + + public class ComplexType + { + public DateTime Time { get; set; } + + public string String { get; set; } + + public Guid Guid { get; set; } + + public Person Person { get; set; } + } + + public class Person + { + public int Age { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs b/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs index 54e2052..684101a 100644 --- a/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs +++ b/test/DotNetCore.CAP.Test/SubscribeFinderTest.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using DotNetCore.CAP.Abstractions; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -22,7 +20,6 @@ namespace DotNetCore.CAP.Test [Fact] public void CanFindControllers() { - } [Fact] @@ -36,7 +33,6 @@ namespace DotNetCore.CAP.Test public class HomeController { - } public interface ITestService { } @@ -46,7 +42,6 @@ namespace DotNetCore.CAP.Test [CapSubscribe("test")] public void Index() { - } } @@ -56,4 +51,4 @@ namespace DotNetCore.CAP.Test { } } -} +} \ No newline at end of file diff --git a/test/Shared/MessageManagerTestBase.cs b/test/Shared/MessageManagerTestBase.cs deleted file mode 100644 index 21f641a..0000000 --- a/test/Shared/MessageManagerTestBase.cs +++ /dev/null @@ -1,113 +0,0 @@ -//using System; -//using System.Threading.Tasks; -//using DotNetCore.CAP.Infrastructure; -//using DotNetCore.CAP.Models; -//using Microsoft.AspNetCore.Http; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Logging; -//using Xunit; - -//namespace DotNetCore.CAP.Test -//{ -// public abstract class MessageManagerTestBase -// { -// private const string NullValue = "(null)"; - -// protected virtual bool ShouldSkipDbTests() -// { -// return false; -// } - -// protected virtual void SetupMessageServices(IServiceCollection services, object context = null) -// { -// services.AddSingleton(); -// services.AddCap(); -// AddMessageStore(services, context); - -// services.AddSingleton>(new TestLogger()); -// } - -// protected virtual ICapMessageStore CreateManager(object context = null, IServiceCollection services = null, -// Action configureServices = null) -// { -// if (services == null) -// { -// services = new ServiceCollection(); -// } -// if (context == null) -// { -// context = CreateTestContext(); -// } -// SetupMessageServices(services, context); - -// configureServices?.Invoke(services); - -// return services.BuildServiceProvider().GetService(); -// } - -// protected abstract object CreateTestContext(); - -// protected abstract CapSentMessage CreateTestSentMessage(string content = ""); -// protected abstract CapReceivedMessage CreateTestReceivedMessage(string content = ""); - -// protected abstract void AddMessageStore(IServiceCollection services, object context = null); - -// [Fact] -// public async Task CanDeleteSentMessage() -// { -// if (ShouldSkipDbTests()) -// { -// return; -// } - -// var manager = CreateManager(); -// var message = CreateTestSentMessage(); -// var operateResult = await manager.StoreSentMessageAsync(message); -// Assert.NotNull(operateResult); -// Assert.True(operateResult.Succeeded); - -// // operateResult = await manager.RemoveSentMessageAsync(message); -// // Assert.NotNull(operateResult); -// // Assert.True(operateResult.Succeeded); -// } - -// //[Fact] -// //public async Task CanUpdateReceivedMessage() -// //{ -// // if (ShouldSkipDbTests()) -// // { -// // return; -// // } - -// // var manager = CreateManager(); -// // var message = CreateTestReceivedMessage(); -// // // var operateResult = await manager.StoreReceivedMessageAsync(message); -// // // Assert.NotNull(operateResult); -// // // Assert.True(operateResult.Succeeded); - -// // // message.StatusName = StatusName.Processing; -// // // operateResult = await manager.UpdateReceivedMessageAsync(message); -// // // Assert.NotNull(operateResult); -// // // Assert.True(operateResult.Succeeded); -// //} - -// [Fact] -// public async Task CanGetNextSendMessage() -// { -// if (ShouldSkipDbTests()) -// { -// return; -// } -// var manager = CreateManager(); -// var message = CreateTestSentMessage(); - -// var operateResult = await manager.StoreSentMessageAsync(message); -// Assert.NotNull(operateResult); -// Assert.True(operateResult.Succeeded); - -// // var storeMessage = await manager.GetNextSentMessageToBeEnqueuedAsync(); - -// // Assert.Equal(message, storeMessage); -// } -// } -//} \ No newline at end of file diff --git a/test/Shared/TestConsistencyMessage.cs b/test/Shared/TestConsistencyMessage.cs deleted file mode 100644 index 30b92e7..0000000 --- a/test/Shared/TestConsistencyMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; -using DotNetCore.CAP.Infrastructure; - -namespace DotNetCore.CAP.Test -{ -} \ No newline at end of file diff --git a/test/Shared/TestLogger.cs b/test/Shared/TestLogger.cs deleted file mode 100644 index e41c9fa..0000000 --- a/test/Shared/TestLogger.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace DotNetCore.CAP.Test -{ - public interface ITestLogger - { - IList LogMessages { get; } - } - - public class TestLogger : ILogger, ITestLogger - { - public IList LogMessages { get; } = new List(); - - public IDisposable BeginScope(TState state) - { - LogMessages.Add(state?.ToString()); - return null; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func formatter) - { - if (formatter == null) - { - LogMessages.Add(state.ToString()); - } - else - { - LogMessages.Add(formatter(state, exception)); - } - } - } -} \ No newline at end of file