diff --git a/.gitignore b/.gitignore index 96cda33bf..af4f8ef4a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,9 @@ bin/ /.idea/.idea.CAP /.idea/.idea.CAP /.idea -Properties \ No newline at end of file +Properties +/pack.bat +/src/DotNetCore.CAP/project.json +/src/DotNetCore.CAP/packages.config +/src/DotNetCore.CAP/DotNetCore.CAP.Net47.csproj +/NuGet.config diff --git a/.travis.yml b/.travis.yml index ec4e8c601..52f2ebc2a 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 b7e380d3a..c81828bde 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.26730.0 +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,10 +54,14 @@ 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 Debug|Any CPU = Debug|Any CPU @@ -116,12 +111,19 @@ Global {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} @@ -133,6 +135,8 @@ Global {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} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/README.md b/README.md index 72fe766b6..5e6ce5fbf 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/appveyor.yml b/appveyor.yml index 4073398c5..41854997e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,15 @@ version: '{build}' -os: Visual Studio 2017 Preview +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 91b52b031..e70f9966d 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/util.cake b/build/util.cake index 4a18e876d..db42eb7cb 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 a5d60c8af..6c03b29d5 100644 --- a/build/version.props +++ b/build/version.props @@ -1,8 +1,8 @@ - 1 - 2 - 0 + 2 + 0 + 1 $(VersionMajor).$(VersionMinor).$(VersionPatch) diff --git a/samples/Sample.Kafka.SqlServer/AppDbContext.cs b/samples/Sample.Kafka.SqlServer/AppDbContext.cs deleted file mode 100644 index 53cecb7e1..000000000 --- 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 61b511a8a..000000000 --- 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.Kafka.SqlServer/Program.cs b/samples/Sample.Kafka.SqlServer/Program.cs deleted file mode 100644 index 37d308962..000000000 --- a/samples/Sample.Kafka.SqlServer/Program.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; - -namespace Sample.Kafka -{ - public class Program - { - public static void Main(string[] args) - { - var config = new ConfigurationBuilder() - .AddCommandLine(args) - .AddEnvironmentVariables("ASPNETCORE_") - .Build(); - - var host = new WebHostBuilder() - .UseConfiguration(config) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .Build(); - - host.Run(); - } - } -} \ No newline at end of file diff --git a/samples/Sample.RabbitMQ.MySql/AppDbContext.cs b/samples/Sample.RabbitMQ.MySql/AppDbContext.cs index 5a60da70b..cf3c96db0 100644 --- a/samples/Sample.RabbitMQ.MySql/AppDbContext.cs +++ b/samples/Sample.RabbitMQ.MySql/AppDbContext.cs @@ -10,7 +10,8 @@ public class AppDbContext : DbContext { 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 eec782e3e..01385204e 100644 --- a/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs +++ b/samples/Sample.RabbitMQ.MySql/Controllers/ValuesController.cs @@ -23,7 +23,16 @@ public ValuesController(AppDbContext dbContext, ICapPublisher capPublisher) [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 @@ public async Task PublishMessageWithTransaction() using (var trans = await _dbContext.Database.BeginTransactionAsync()) { await _capBus.PublishAsync("sample.kafka.sqlserver", ""); + trans.Commit(); } return Ok(); @@ -41,10 +51,9 @@ public async Task PublishMessageWithTransaction() [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 e90171fff..1c67eb063 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 5a3d92fb1..19b1eac46 100644 --- a/samples/Sample.RabbitMQ.MySql/Startup.cs +++ b/samples/Sample.RabbitMQ.MySql/Startup.cs @@ -18,7 +18,11 @@ public void ConfigureServices(IServiceCollection services) 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 000000000..4fefe5ce2 --- /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 000000000..10ea6d15d --- /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 000000000..3cb075517 --- /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 000000000..7dba0be68 --- /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 000000000..ac7ead0cb --- /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 000000000..0607bb085 --- /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 000000000..09b1df362 --- /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.RabbitMQ.SqlServer/Program.cs b/samples/Sample.RabbitMQ.SqlServer/Program.cs new file mode 100644 index 000000000..ed4ff0972 --- /dev/null +++ b/samples/Sample.RabbitMQ.SqlServer/Program.cs @@ -0,0 +1,37 @@ +using System.IO; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace Sample.RabbitMQ.SqlServer +{ + public class Program + { + + //var config = new ConfigurationBuilder() + // .AddCommandLine(args) + // .AddEnvironmentVariables("ASPNETCORE_") + // .Build(); + + //var host = new WebHostBuilder() + // .UseConfiguration(config) + // .UseKestrel() + // .UseContentRoot(Directory.GetCurrentDirectory()) + // .UseIISIntegration() + // .UseStartup() + // .Build(); + + //host.Run(); + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + + } +} \ No newline at end of file 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 675a95b22..2f9fdaf6c 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 08291c5e6..e6819ea9c 100644 --- a/samples/Sample.Kafka.SqlServer/Startup.cs +++ b/samples/Sample.RabbitMQ.SqlServer/Startup.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Sample.Kafka +namespace Sample.RabbitMQ.SqlServer { public class Startup { @@ -14,7 +14,11 @@ public void ConfigureServices(IServiceCollection services) 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.KafkaCapOptionsExtension.cs b/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs index 8d9bf9860..5c9b6ba98 100644 --- a/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.Kafka/CAP.KafkaCapOptionsExtension.cs @@ -21,7 +21,7 @@ public void AddServices(IServiceCollection services) services.AddSingleton(kafkaOptions); services.AddSingleton(); - services.AddTransient(); + services.AddSingleton(); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs index e61ccee93..07904583d 100644 --- a/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs +++ b/src/DotNetCore.CAP.Kafka/CAP.KafkaOptions.cs @@ -42,7 +42,7 @@ internal IEnumerable> AskafkaConfig() { 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 89bd201c3..96f0191df 100644 --- a/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj +++ b/src/DotNetCore.CAP.Kafka/DotNetCore.CAP.Kafka.csproj @@ -3,11 +3,16 @@ - netstandard1.6; + netstandard2.0 DotNetCore.CAP.Kafka $(PackageTags);Kafka - + + + NU1605 + NU1701 + + diff --git a/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs b/src/DotNetCore.CAP.Kafka/KafkaConsumerClientFactory.cs index ea2ee67cd..8bda50aab 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 a3f4c80c4..8b455d747 100644 --- a/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs +++ b/src/DotNetCore.CAP.Kafka/PublishQueueExecutor.cs @@ -12,13 +12,15 @@ internal class PublishQueueExecutor : BasePublishQueueExecutor 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 aa825a2d2..b9c1b0db8 100644 --- a/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.MySql/CAP.MySqlCapOptionsExtension.cs @@ -21,6 +21,7 @@ public void AddServices(IServiceCollection services) services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddTransient(); var mysqlOptions = new MySqlOptions(); @@ -28,24 +29,17 @@ public void AddServices(IServiceCollection services) 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); - } - -#if NETSTANDARD1_6 - private IServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); - } -#else - private ServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); } -#endif } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.MySql/CapPublisher.cs b/src/DotNetCore.CAP.MySql/CapPublisher.cs index 6b8476aea..0d6a1b311 100644 --- a/src/DotNetCore.CAP.MySql/CapPublisher.cs +++ b/src/DotNetCore.CAP.MySql/CapPublisher.cs @@ -7,10 +7,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; +using MySql.Data.MySqlClient; namespace DotNetCore.CAP.MySql { - public class CapPublisher : CapPublisherBase + public class CapPublisher : CapPublisherBase, ICallbackPublisher { private readonly ILogger _logger; private readonly MySqlOptions _options; @@ -34,13 +35,17 @@ public CapPublisher(IServiceProvider provider, protected override void PrepareConnectionForEF() { DbConnection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) + var dbContextTransaction = _dbContext.Database.CurrentTransaction; + var dbTrans = dbContextTransaction?.GetDbTransaction(); + //DbTransaction is dispose in original + if (dbTrans?.Connection == null) { IsCapOpenedTrans = true; - transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbContextTransaction?.Dispose(); + dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbTrans = dbContextTransaction.GetDbTransaction(); } - DbTranasaction = transaction.GetDbTransaction(); + DbTranasaction = dbTrans; } protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) @@ -53,19 +58,25 @@ protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTra 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()); } - #region private methods + public async Task PublishAsync(CapPublishedMessage message) + { + using (var conn = new MySqlConnection(_options.ConnectionString)) + { + await conn.ExecuteAsync(PrepareSql(), message); + } + } + + #region private methods private string PrepareSql() { return $"INSERT INTO `{_options.TableNamePrefix}.published` (`Name`,`Content`,`Retries`,`Added`,`ExpiresAt`,`StatusName`)VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)"; } - - #endregion private methods } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj b/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj index d750aafdd..00316dd80 100644 --- a/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj +++ b/src/DotNetCore.CAP.MySql/DotNetCore.CAP.MySql.csproj @@ -3,20 +3,16 @@ - netstandard1.6;netstandard2.0; + netstandard2.0 DotNetCore.CAP.MySql $(PackageTags);MySQL - - TRACE;DEBUG - - - - - + + + diff --git a/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs index b8e5922af..3398dc009 100644 --- a/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs +++ b/src/DotNetCore.CAP.MySql/IAdditionalProcessor.Default.cs @@ -15,7 +15,7 @@ internal class DefaultAdditionalProcessor : IAdditionalProcessor 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.PostgreSqlCapOptionsExtension.cs b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs index d3d8b1408..e4381e041 100644 --- a/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.PostgreSql/CAP.PostgreSqlCapOptionsExtension.cs @@ -1,6 +1,6 @@ using System; -using DotNetCore.CAP.Processor; using DotNetCore.CAP.PostgreSql; +using DotNetCore.CAP.Processor; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -21,6 +21,7 @@ public void AddServices(IServiceCollection services) services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddTransient(); var postgreSqlOptions = new PostgreSqlOptions(); @@ -28,24 +29,17 @@ public void AddServices(IServiceCollection services) if (postgreSqlOptions.DbContextType != null) { - var provider = TempBuildService(services); - var dbContextObj = provider.GetService(postgreSqlOptions.DbContextType); - var dbContext = (DbContext)dbContextObj; - postgreSqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + services.AddSingleton(x => + { + var dbContext = (DbContext)x.GetService(postgreSqlOptions.DbContextType); + postgreSqlOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + return postgreSqlOptions; + }); + } + else + { + services.AddSingleton(postgreSqlOptions); } - services.AddSingleton(postgreSqlOptions); - } - -#if NETSTANDARD1_6 - private IServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); - } -#else - private ServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); } -#endif } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs b/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs index f141b1e6a..0f4e6c920 100644 --- a/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs +++ b/src/DotNetCore.CAP.PostgreSql/CapPublisher.cs @@ -2,15 +2,16 @@ using System.Data; using System.Threading.Tasks; using Dapper; -using DotNetCore.CAP.Models; 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 + public class CapPublisher : CapPublisherBase, ICallbackPublisher { private readonly ILogger _logger; private readonly PostgreSqlOptions _options; @@ -27,20 +28,24 @@ public CapPublisher(IServiceProvider provider, if (_options.DbContextType != null) { IsUsingEF = true; - _dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType); + _dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType); } } protected override void PrepareConnectionForEF() { DbConnection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) + var dbContextTransaction = _dbContext.Database.CurrentTransaction; + var dbTrans = dbContextTransaction?.GetDbTransaction(); + //DbTransaction is dispose in original + if (dbTrans?.Connection == null) { IsCapOpenedTrans = true; - transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbContextTransaction?.Dispose(); + dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbTrans = dbContextTransaction.GetDbTransaction(); } - DbTranasaction = transaction.GetDbTransaction(); + DbTranasaction = dbTrans; } protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) @@ -57,9 +62,21 @@ protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransa _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 index a49961ead..d3884cecf 100644 --- a/src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj +++ b/src/DotNetCore.CAP.PostgreSql/DotNetCore.CAP.PostgreSql.csproj @@ -3,19 +3,15 @@ - netstandard1.6;netstandard2.0; + netstandard2.0 DotNetCore.CAP.PostgreSql $(PackageTags);PostgreSQL - - TRACE;DEBUG - - - - + + diff --git a/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs index d832d0e97..1c79f60e7 100644 --- a/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs +++ b/src/DotNetCore.CAP.PostgreSql/IAdditionalProcessor.Default.cs @@ -15,7 +15,7 @@ internal class DefaultAdditionalProcessor : IAdditionalProcessor 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 = { @@ -44,7 +44,7 @@ public async Task ProcessAsync(ProcessingContext context) 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 }); + new { now = DateTime.Now, count = MaxBatch }); } if (removedCount != 0) diff --git a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs index 420257006..abc6d2bd1 100644 --- a/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.RabbitMQ/CAP.RabbitMQCapOptionsExtension.cs @@ -21,7 +21,10 @@ public void AddServices(IServiceCollection services) services.AddSingleton(options); services.AddSingleton(); - services.AddTransient(); + + services.AddSingleton(); + + services.AddSingleton(); } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/ConnectionPool.cs b/src/DotNetCore.CAP.RabbitMQ/ConnectionPool.cs new file mode 100644 index 000000000..5db81e7d3 --- /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 = 15; + + 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 52e784469..55a0debea 100644 --- a/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj +++ b/src/DotNetCore.CAP.RabbitMQ/DotNetCore.CAP.RabbitMQ.csproj @@ -3,7 +3,7 @@ - netstandard1.6; + 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 000000000..9097f2819 --- /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 27b777b61..0a4750136 100644 --- a/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs +++ b/src/DotNetCore.CAP.RabbitMQ/PublishQueueExecutor.cs @@ -3,7 +3,6 @@ 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,34 +10,28 @@ namespace DotNetCore.CAP.RabbitMQ internal sealed class PublishQueueExecutor : BasePublishQueueExecutor { private readonly ILogger _logger; + private readonly ConnectionPool _connectionPool; private readonly RabbitMQOptions _rabbitMQOptions; - public PublishQueueExecutor(IStateChanger stateChanger, - RabbitMQOptions options, + public PublishQueueExecutor( + CapOptions options, + IStateChanger stateChanger, + ConnectionPool connectionPool, + RabbitMQOptions rabbitMQOptions, ILogger logger) - : base(stateChanger, logger) + : base(options, stateChanger, logger) { _logger = logger; - _rabbitMQOptions = options; + _connectionPool = connectionPool; + _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 - }; + var connection = _connectionPool.Rent(); try { - using (var connection = factory.CreateConnection()) using (var channel = connection.CreateModel()) { var body = Encoding.UTF8.GetBytes(content); @@ -64,6 +57,10 @@ public override Task PublishAsync(string keyName, string content) Description = ex.Message })); } + finally + { + _connectionPool.Return(connection); + } } } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs index 11d888abb..0172c2be0 100644 --- a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs +++ b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClient.cs @@ -14,8 +14,7 @@ internal sealed class RabbitMQConsumerClient : IConsumerClient private readonly string _queueName; private readonly RabbitMQOptions _rabbitMQOptions; - private IConnectionFactory _connectionFactory; - private IConnection _connection; + private ConnectionPool _connectionPool; private IModel _channel; private ulong _deliveryTag; @@ -23,9 +22,12 @@ internal sealed class RabbitMQConsumerClient : IConsumerClient public event EventHandler OnError; - public RabbitMQConsumerClient(string queueName, RabbitMQOptions options) + public RabbitMQConsumerClient(string queueName, + ConnectionPool connectionPool, + RabbitMQOptions options) { _queueName = queueName; + _connectionPool = connectionPool; _rabbitMQOptions = options; _exchageName = options.TopicExchangeName; @@ -34,20 +36,9 @@ public RabbitMQConsumerClient(string queueName, RabbitMQOptions options) 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 - }; + var connection = _connectionPool.Rent(); - _connection = _connectionFactory.CreateConnection(); - _channel = _connection.CreateModel(); + _channel = connection.CreateModel(); _channel.ExchangeDeclare( exchange: _exchageName, @@ -60,6 +51,8 @@ private void InitClient() exclusive: false, autoDelete: false, arguments: arguments); + + _connectionPool.Return(connection); } public void Subscribe(IEnumerable topics) @@ -92,7 +85,6 @@ public void Commit() public void Dispose() { _channel.Dispose(); - _connection.Dispose(); } private void OnConsumerReceived(object sender, BasicDeliverEventArgs e) diff --git a/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs b/src/DotNetCore.CAP.RabbitMQ/RabbitMQConsumerClientFactory.cs index fcd267dac..753fc0565 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 ConnectionPool _connectionPool; - public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions) + + public RabbitMQConsumerClientFactory(RabbitMQOptions rabbitMQOptions, ConnectionPool pool) { _rabbitMQOptions = rabbitMQOptions; + _connectionPool = pool; } public IConsumerClient Create(string groupId) { - return new RabbitMQConsumerClient(groupId, _rabbitMQOptions); + return new RabbitMQConsumerClient(groupId, _connectionPool, _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 f36a4bc1b..f928031b3 100644 --- a/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs +++ b/src/DotNetCore.CAP.SqlServer/CAP.SqlServerCapOptionsExtension.cs @@ -19,34 +19,36 @@ public SqlServerCapOptionsExtension(Action configure) public void AddServices(IServiceCollection services) { services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); + AddSqlServerOptions(services); + } + private void AddSqlServerOptions(IServiceCollection services) + { 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 => + { + using (var scope = x.CreateScope()) + { + var provider = scope.ServiceProvider; + var dbContext = (DbContext)provider.GetService(sqlServerOptions.DbContextType); + sqlServerOptions.ConnectionString = dbContext.Database.GetDbConnection().ConnectionString; + return sqlServerOptions; + } + }); + } + else + { + services.AddSingleton(sqlServerOptions); } - services.AddSingleton(sqlServerOptions); - } - -#if NETSTANDARD1_6 - private IServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); - } -#else - private ServiceProvider TempBuildService(IServiceCollection services) - { - return services.BuildServiceProvider(); } -#endif - } } \ No newline at end of file diff --git a/src/DotNetCore.CAP.SqlServer/CapPublisher.cs b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs index 3acddb269..18c17c2ef 100644 --- a/src/DotNetCore.CAP.SqlServer/CapPublisher.cs +++ b/src/DotNetCore.CAP.SqlServer/CapPublisher.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Data.SqlClient; using System.Threading.Tasks; using Dapper; using DotNetCore.CAP.Abstractions; @@ -10,7 +11,7 @@ namespace DotNetCore.CAP.SqlServer { - public class CapPublisher : CapPublisherBase + public class CapPublisher : CapPublisherBase, ICallbackPublisher { private readonly ILogger _logger; private readonly SqlServerOptions _options; @@ -34,13 +35,17 @@ public CapPublisher(IServiceProvider provider, protected override void PrepareConnectionForEF() { DbConnection = _dbContext.Database.GetDbConnection(); - var transaction = _dbContext.Database.CurrentTransaction; - if (transaction == null) + var dbContextTransaction = _dbContext.Database.CurrentTransaction; + var dbTrans = dbContextTransaction?.GetDbTransaction(); + //DbTransaction is dispose in original + if (dbTrans?.Connection == null) { IsCapOpenedTrans = true; - transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbContextTransaction?.Dispose(); + dbContextTransaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); + dbTrans = dbContextTransaction.GetDbTransaction(); } - DbTranasaction = transaction.GetDbTransaction(); + DbTranasaction = dbTrans; } protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) @@ -57,6 +62,14 @@ protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransa _logger.LogInformation("Published Message has been persisted in the database. name:" + message.ToString()); } + public async Task PublishAsync(CapPublishedMessage message) + { + using (var conn = new SqlConnection(_options.ConnectionString)) + { + await conn.ExecuteAsync(PrepareSql(), message); + } + } + #region private methods private string PrepareSql() @@ -64,7 +77,6 @@ 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.SqlServer/DotNetCore.CAP.SqlServer.csproj b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj index 7592ae4a2..f2c293b27 100644 --- a/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj +++ b/src/DotNetCore.CAP.SqlServer/DotNetCore.CAP.SqlServer.csproj @@ -3,19 +3,16 @@ - netstandard1.6;netstandard2.0; + netstandard2.0 DotNetCore.CAP.SqlServer $(PackageTags);SQL Server - - TRACE;DEBUG - - - - + + + diff --git a/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs index 65490ab4b..7c1bac1c8 100644 --- a/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs +++ b/src/DotNetCore.CAP.SqlServer/IAdditionalProcessor.Default.cs @@ -15,7 +15,7 @@ public class DefaultAdditionalProcessor : IAdditionalProcessor 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 57d7e5daf..9ef024797 100644 --- a/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs +++ b/src/DotNetCore.CAP.SqlServer/SqlServerStorage.cs @@ -2,7 +2,6 @@ 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 index 257f881cc..2eeddc1f7 100644 --- a/src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs +++ b/src/DotNetCore.CAP/Abstractions/CapPublisherBase.cs @@ -7,52 +7,55 @@ namespace DotNetCore.CAP.Abstractions { - public abstract class CapPublisherBase : ICapPublisher + 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) + + public void Publish(string name, T contentObj, string callbackName = null) { CheckIsUsingEF(name); PrepareConnectionForEF(); - var content = Serialize(contentObj); + var content = Serialize(contentObj, callbackName); - PublishWithTrans(name, content, DbConnection, DbTranasaction); + PublishWithTrans(name, content); } - public Task PublishAsync(string name, T contentObj) + public Task PublishAsync(string name, T contentObj, string callbackName = null) { CheckIsUsingEF(name); PrepareConnectionForEF(); - var content = Serialize(contentObj); + var content = Serialize(contentObj, callbackName); - return PublishWithTransAsync(name, content, DbConnection, DbTranasaction); + return PublishWithTransAsync(name, content); } - public void Publish(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) + public void Publish(string name, T contentObj, IDbConnection dbConnection, + string callbackName = null, IDbTransaction dbTransaction = null) { CheckIsAdoNet(name); - PrepareConnectionForAdo(dbConnection, ref dbTransaction); + PrepareConnectionForAdo(dbConnection, dbTransaction); - var content = Serialize(contentObj); + var content = Serialize(contentObj, callbackName); - PublishWithTrans(name, content, dbConnection, dbTransaction); + PublishWithTrans(name, content); } - 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) { CheckIsAdoNet(name); - PrepareConnectionForAdo(dbConnection, ref dbTransaction); + PrepareConnectionForAdo(dbConnection, dbTransaction); - var content = Serialize(contentObj); + var content = Serialize(contentObj, callbackName); - return PublishWithTransAsync(name, content, dbConnection, dbTransaction); + return PublishWithTransAsync(name, content); } protected abstract void PrepareConnectionForEF(); @@ -63,32 +66,29 @@ public Task PublishAsync(string name, T contentObj, IDbConnection dbConnectio #region private methods - private string Serialize(T obj) + private string Serialize(T obj, string callbackName = null) { - string content = string.Empty; - if (Helper.IsComplexType(typeof(T))) - { - content = Helper.ToJson(obj); - } - else + var message = new Message(obj) { - content = obj.ToString(); - } - return content; + CallbackName = callbackName + }; + + return Helper.ToJson(message); } - private void PrepareConnectionForAdo(IDbConnection dbConnection, ref IDbTransaction dbTransaction) + private void PrepareConnectionForAdo(IDbConnection dbConnection, IDbTransaction dbTransaction) { - if (dbConnection == null) - throw new ArgumentNullException(nameof(dbConnection)); - - if (dbConnection.State != ConnectionState.Open) - dbConnection.Open(); - - if (dbTransaction == null) + DbConnection = dbConnection ?? throw new ArgumentNullException(nameof(dbConnection)); + if (DbConnection.State != ConnectionState.Open) + { + IsCapOpenedConn = true; + DbConnection.Open(); + } + DbTranasaction = dbTransaction; + if (DbTranasaction == null) { IsCapOpenedTrans = true; - dbTransaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); + DbTranasaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); } } @@ -107,7 +107,7 @@ private void CheckIsAdoNet(string name) 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, IDbConnection dbConnection, IDbTransaction dbTransaction) + private async Task PublishWithTransAsync(string name, string content) { var message = new CapPublishedMessage { @@ -116,18 +116,14 @@ private async Task PublishWithTransAsync(string name, string content, IDbConnect StatusName = StatusName.Scheduled }; - await ExecuteAsync(dbConnection, dbTransaction, message); + await ExecuteAsync(DbConnection, DbTranasaction, message); + + ClosedCap(); - if (IsCapOpenedTrans) - { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); - } PublishQueuer.PulseEvent.Set(); } - private void PublishWithTrans(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) + private void PublishWithTrans(string name, string content) { var message = new CapPublishedMessage { @@ -136,17 +132,32 @@ private void PublishWithTrans(string name, string content, IDbConnection dbConne StatusName = StatusName.Scheduled }; - Execute(dbConnection, dbTransaction, message); + Execute(DbConnection, DbTranasaction, message); + + ClosedCap(); + + PublishQueuer.PulseEvent.Set(); + } + private void ClosedCap() + { if (IsCapOpenedTrans) { - dbTransaction.Commit(); - dbTransaction.Dispose(); - dbConnection.Dispose(); + DbTranasaction.Commit(); + DbTranasaction.Dispose(); } - PublishQueuer.PulseEvent.Set(); + 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/Abstractions/IConsumerServiceSelector.cs b/src/DotNetCore.CAP/Abstractions/IConsumerServiceSelector.cs index b9e1e0372..e7985d780 100644 --- a/src/DotNetCore.CAP/Abstractions/IConsumerServiceSelector.cs +++ b/src/DotNetCore.CAP/Abstractions/IConsumerServiceSelector.cs @@ -11,10 +11,9 @@ public interface IConsumerServiceSelector /// /// Selects a set of candidates for the current message associated with /// . - /// - /// . + /// /// A set of candidates or null. - IReadOnlyList SelectCandidates(IServiceProvider provider); + IReadOnlyList SelectCandidates(); /// /// Selects the best candidate from for the diff --git a/src/DotNetCore.CAP/CAP.Options.cs b/src/DotNetCore.CAP/CAP.Options.cs index 0d17e6769..d6cd44120 100644 --- a/src/DotNetCore.CAP/CAP.Options.cs +++ b/src/DotNetCore.CAP/CAP.Options.cs @@ -20,27 +20,48 @@ public class CapOptions /// 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 085205e7b..51f645baa 100644 --- a/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs +++ b/src/DotNetCore.CAP/CAP.ServiceCollectionExtensions.cs @@ -67,27 +67,20 @@ public static CapBuilder AddCap( 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); - } - - var types = Assembly.GetEntryAssembly().ExportedTypes; - foreach (var type in types) - { - if (Helper.IsController(type.GetTypeInfo())) - { - services.AddSingleton(typeof(object), type); - } + services.AddTransient(service.Key, service.Value); } } } diff --git a/src/DotNetCore.CAP/DotNetCore.CAP.csproj b/src/DotNetCore.CAP/DotNetCore.CAP.csproj index 8b9c66076..e0e473f77 100644 --- a/src/DotNetCore.CAP/DotNetCore.CAP.csproj +++ b/src/DotNetCore.CAP/DotNetCore.CAP.csproj @@ -3,28 +3,21 @@ - netstandard1.6;netstandard2.0; + netstandard2.0 DotNetCore.CAP $(PackageTags); - - - - + + + + - - - - - + + - - - - diff --git a/src/DotNetCore.CAP/IBootstrapper.Default.cs b/src/DotNetCore.CAP/IBootstrapper.Default.cs index 195916b5b..e4a75d5fd 100644 --- a/src/DotNetCore.CAP/IBootstrapper.Default.cs +++ b/src/DotNetCore.CAP/IBootstrapper.Default.cs @@ -25,14 +25,13 @@ public DefaultBootstrapper( IOptions options, IStorage storage, IApplicationLifetime appLifetime, - IServiceProvider provider) + IEnumerable servers) { _logger = logger; _appLifetime = appLifetime; Options = options.Value; Storage = storage; - Provider = provider; - Servers = Provider.GetServices(); + Servers = servers; _cts = new CancellationTokenSource(); _ctsRegistration = appLifetime.ApplicationStopping.Register(() => @@ -55,8 +54,6 @@ public DefaultBootstrapper( protected IEnumerable Servers { get; } - public IServiceProvider Provider { get; private set; } - public Task BootstrapAsync() { return (_bootstrappingTask = BootstrapTaskAsync()); diff --git a/src/DotNetCore.CAP/ICallbackPublisher.cs b/src/DotNetCore.CAP/ICallbackPublisher.cs new file mode 100644 index 000000000..1743b52c4 --- /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 a3a721031..fc05bedff 100644 --- a/src/DotNetCore.CAP/ICapPublisher.cs +++ b/src/DotNetCore.CAP/ICapPublisher.cs @@ -18,7 +18,8 @@ public interface ICapPublisher /// 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 @@ public interface ICapPublisher /// 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/IConsumerHandler.Default.cs b/src/DotNetCore.CAP/IConsumerHandler.Default.cs index 2990e8b0c..4e69e12a9 100644 --- a/src/DotNetCore.CAP/IConsumerHandler.Default.cs +++ b/src/DotNetCore.CAP/IConsumerHandler.Default.cs @@ -47,7 +47,7 @@ public ConsumerHandler( public void Start() { - var groupingMatchs = _selector.GetCandidatesMethodsOfGroupNameGrouped(_serviceProvider); + var groupingMatchs = _selector.GetCandidatesMethodsOfGroupNameGrouped(); foreach (var matchGroup in groupingMatchs) { diff --git a/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs b/src/DotNetCore.CAP/IQueueExecutor.Publish.Base.cs index a032af6c6..e7274a8b2 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 @@ public async Task ExecuteAsync(IStorageConnection connection, IFe } 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 e8fd93cdb..a1f69566e 100644 --- a/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs +++ b/src/DotNetCore.CAP/IQueueExecutor.Subscibe.cs @@ -15,16 +15,18 @@ public class SubscibeQueueExecutor : IQueueExecutor 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 @@ public async Task ExecuteAsync(IStorageConnection connection, IFe } else { - newState = new SucceededState(); + newState = new SucceededState(_options.SuccessedMessageExpiredAfter); } await _stateChanger.ChangeStateAsync(message, newState, connection); diff --git a/src/DotNetCore.CAP/Infrastructure/ObjectId.cs b/src/DotNetCore.CAP/Infrastructure/ObjectId.cs new file mode 100644 index 000000000..23784fdc5 --- /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 0921e1b3b..1d2f5df0d 100644 --- a/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs +++ b/src/DotNetCore.CAP/Internal/ConsumerInvokerFactory.cs @@ -24,7 +24,8 @@ public IConsumerInvoker CreateInvoker(ConsumerContext consumerContext) { var context = new ConsumerInvokerContext(consumerContext) { - Result = new DefaultConsumerInvoker(_logger, _serviceProvider, _modelBinderFactory, consumerContext) + Result = new DefaultConsumerInvoker(_logger, _serviceProvider, + _modelBinderFactory, consumerContext) }; return context.Result; diff --git a/src/DotNetCore.CAP/Internal/ConsumerMethodExecutor.cs b/src/DotNetCore.CAP/Internal/ConsumerMethodExecutor.cs deleted file mode 100644 index b33cc7117..000000000 --- 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 3009f7363..0f408beaa 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,106 @@ public DefaultConsumerInvoker(ILogger logger, { _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); + + using (var scope = _serviceProvider.CreateScope()) { - _logger.LogDebug("Executing consumer Topic: {0}", _consumerContext.ConsumerDescriptor.MethodInfo.Name); + var provider = scope.ServiceProvider; + var serviceType = _consumerContext.ConsumerDescriptor.ImplTypeInfo.AsType(); + var obj = ActivatorUtilities.GetServiceOrCreateInstance(provider, serviceType); - var obj = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, - _consumerContext.ConsumerDescriptor.ImplTypeInfo.AsType()); + var jsonConent = _consumerContext.DeliverMessage.Content; + var message = Helper.FromJson(jsonConent); - var value = _consumerContext.DeliverMessage.Content; + object result = null; if (_executor.MethodParameters.Length > 0) { - var firstParameter = _executor.MethodParameters[0]; - try + result = await ExecuteWithParameterAsync(obj, message.Content.ToString()); + } + else + { + result = await ExecuteAsync(obj); + } + + if (!string.IsNullOrEmpty(message.CallbackName)) + { + await SentCallbackMessage(message.Id, message.CallbackName, result); + } + } + } + + 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) + { + 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/IConsumerServiceSelector.Default.cs b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs index 4746b5c4e..9dd0d187e 100644 --- a/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs @@ -33,13 +33,13 @@ public ConsumerExecutorDescriptor SelectBestCandidate(string key, return executeDescriptor.FirstOrDefault(x => x.Attribute.Name == key); } - public IReadOnlyList SelectCandidates(IServiceProvider provider) + public IReadOnlyList SelectCandidates() { var executorDescriptorList = new List(); - executorDescriptorList.AddRange(FindConsumersFromInterfaceTypes(provider)); + executorDescriptorList.AddRange(FindConsumersFromInterfaceTypes(_serviceProvider)); - executorDescriptorList.AddRange(FindConsumersFromControllerTypes(provider)); + executorDescriptorList.AddRange(FindConsumersFromControllerTypes(_serviceProvider)); return executorDescriptorList; } @@ -48,35 +48,38 @@ private static IEnumerable FindConsumersFromInterfac IServiceProvider provider) { var executorDescriptorList = new List(); - - var consumerServices = provider.GetServices(); - foreach (var service in consumerServices) + + using (var scoped = provider.CreateScope()) { - var typeInfo = service.GetType().GetTypeInfo(); - if (!typeof(ICapSubscribe).GetTypeInfo().IsAssignableFrom(typeInfo)) + var scopedProvider = scoped.ServiceProvider; + var consumerServices = scopedProvider.GetServices(); + foreach (var service in consumerServices) { - continue; - } + var typeInfo = service.GetType().GetTypeInfo(); + if (!typeof(ICapSubscribe).GetTypeInfo().IsAssignableFrom(typeInfo)) + { + continue; + } - executorDescriptorList.AddRange(GetTopicAttributesDescription(typeInfo)); + executorDescriptorList.AddRange(GetTopicAttributesDescription(typeInfo)); + } + return executorDescriptorList; } - return executorDescriptorList; } private static IEnumerable FindConsumersFromControllerTypes( IServiceProvider provider) { var executorDescriptorList = new List(); - // at cap startup time, find all Controller into the DI container,the type is object. - var controllers = provider.GetServices(); - foreach (var controller in controllers) - { - var typeInfo = controller.GetType().GetTypeInfo(); - //double check - if (!Helper.IsController(typeInfo)) continue; - - executorDescriptorList.AddRange(GetTopicAttributesDescription(typeInfo)); + var types = Assembly.GetEntryAssembly().ExportedTypes; + foreach (var type in types) + { + var typeInfo = type.GetTypeInfo(); + if (Helper.IsController(typeInfo)) + { + executorDescriptorList.AddRange(GetTopicAttributesDescription(typeInfo)); + } } return executorDescriptorList; diff --git a/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs b/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs index c25f7c671..369e01ea5 100644 --- a/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs +++ b/src/DotNetCore.CAP/Internal/IModelBinder.ComplexType.cs @@ -20,7 +20,9 @@ public Task BindModelAsync(string content) 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 000000000..63eb454bd --- /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/MethodMatcherCache.cs b/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs index 332158b85..db59fcc8f 100644 --- a/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs +++ b/src/DotNetCore.CAP/Internal/MethodMatcherCache.cs @@ -22,12 +22,11 @@ public MethodMatcherCache(IConsumerServiceSelector selector) /// Get a dictionary of candidates.In the dictionary, /// the Key is the CAPSubscribeAttribute Group, the Value for the current Group of candidates /// - /// - public ConcurrentDictionary> GetCandidatesMethodsOfGroupNameGrouped(IServiceProvider provider) + public ConcurrentDictionary> GetCandidatesMethodsOfGroupNameGrouped() { if (Entries.Count != 0) return Entries; - var executorCollection = _selector.SelectCandidates(provider); + var executorCollection = _selector.SelectCandidates(); var groupedCandidates = executorCollection.GroupBy(x => x.Attribute.Group); diff --git a/src/DotNetCore.CAP/Internal/ModelAttributes.cs b/src/DotNetCore.CAP/Internal/ModelAttributes.cs deleted file mode 100644 index 756f654be..000000000 --- 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 cb0f99de0..000000000 --- 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 000000000..431b83a6e --- /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 000000000..4e48ef09a --- /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 000000000..b2025d678 --- /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 000000000..7509b86b2 --- /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 000000000..2198c0ce4 --- /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/Message.cs b/src/DotNetCore.CAP/Models/Message.cs new file mode 100644 index 000000000..f55262a30 --- /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 294591ca7..0a3f2401b 100644 --- a/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs +++ b/src/DotNetCore.CAP/Processor/States/IState.Succeeded.cs @@ -7,10 +7,20 @@ public class SucceededState : IState { 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 bd858e5bb..1fac1b65a 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 cf89b391b..be4dae5a3 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 b83588e40..b3df31f47 100644 --- a/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs +++ b/test/DotNetCore.CAP.MySql.Test/MySqlStorageConnectionTest.cs @@ -85,7 +85,6 @@ public async Task StoreReceivedMessageAsync_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 @@ public async Task GetNextReceviedMessageToBeEnqueuedAsync_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 9286929dd..715cb5cc3 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 @@ public class MySqlStorageTest : DatabaseTestHost private readonly string _dbName; private readonly string _masterDbConnectionString; - public MySqlStorageTest() { _dbName = ConnectionUtil.GetDatabaseName(); @@ -29,36 +28,12 @@ public void Database_IsExists() } } - [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 @@ public void DatabaseTable_Received_IsExists() } } } -} +} \ 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 c8290ad62..06dcba911 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 000000000..b29352914 --- /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 000000000..66ae8e1e2 --- /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 000000000..0f116c838 --- /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 000000000..df68d0be6 --- /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 000000000..c4f5748f5 --- /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 000000000..4bff8cb15 --- /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 d75938438..5bf5a7dbf 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 @@ IF NOT EXISTS (SELECT * FROM sysdatabases WHERE name = N'{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 177d825a7..1a8cc9a1c 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 a99571506..000000000 --- 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 bd0bab4cd..b081d1ac8 100644 --- a/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs +++ b/test/DotNetCore.CAP.SqlServer.Test/SqlServerStorageConnectionTest.cs @@ -85,7 +85,6 @@ public async Task StoreReceivedMessageAsync_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 @@ public async Task GetNextReceviedMessageToBeEnqueuedAsync_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 38fb6c160..af5fc414e 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 @@ public void Database_IsExists() { 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 d59bdf114..000000000 --- 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 31cbfd179..d8618afe9 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 @@ private void CreateServiceCollection() _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 0bd3c8f01..a151d7234 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 @@ public void CanAddCapService() Assert.NotNull(markService); } - [Fact] public void CanOverridePublishService() { @@ -61,47 +60,47 @@ public void CanResolveCapOptions() 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 000000000..bf070fe8e --- /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 1fc5ca823..7704e88c9 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 @@ public ConsumerServiceSelectorTest() services.AddScoped(); services.AddScoped(); services.AddLogging(); - services.AddCap(x=> { }); + services.AddCap(x => { }); _provider = services.BuildServiceProvider(); } @@ -28,7 +25,7 @@ public ConsumerServiceSelectorTest() public void CanFindAllConsumerService() { var selector = _provider.GetRequiredService(); - var candidates = selector.SelectCandidates(_provider); + var candidates = selector.SelectCandidates(); Assert.Equal(2, candidates.Count); } @@ -37,12 +34,12 @@ public void CanFindAllConsumerService() public void CanFindSpecifiedTopic() { var selector = _provider.GetRequiredService(); - var candidates = selector.SelectCandidates(_provider); + var candidates = selector.SelectCandidates(); var bestCandidates = selector.SelectBestCandidate("Candidates.Foo", candidates); 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 81b5e2db6..b4d7bae6a 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 000000000..946db42ff --- /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 69fb54d8b..000000000 --- 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 7ff1d19c1..8ccdbf043 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 @@ public async void ProcessAsync_CancellationTokenCancelled_ThrowsImmediately() 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 @@ public async Task ProcessAsync() 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 51101bdb5..9ff5208eb 100644 --- a/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs +++ b/test/DotNetCore.CAP.Test/Processor/StateChangerTest.cs @@ -25,7 +25,7 @@ public void ChangeState() 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 @@ public void ChangeState_ExpiresAfter() 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 @@ public void ChangeState_ExpiresAfter() 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 f1bf1e35b..729283247 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 @@ public void CanGetSubscribeExector() 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 000000000..f04329841 --- /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 54e2052bc..684101abb 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 @@ public SubscribeFinderTest() [Fact] public void CanFindControllers() { - } [Fact] @@ -36,7 +33,6 @@ public void CanFindSubscribeService() public class HomeController { - } public interface ITestService { } @@ -46,7 +42,6 @@ public class TestService : ITestService, ICapSubscribe [CapSubscribe("test")] public void Index() { - } } @@ -56,4 +51,4 @@ public CapSubscribeAttribute(string name) : base(name) { } } -} +} \ No newline at end of file diff --git a/test/Shared/MessageManagerTestBase.cs b/test/Shared/MessageManagerTestBase.cs deleted file mode 100644 index 21f641a49..000000000 --- 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 30b92e73c..000000000 --- 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 e41c9fa21..000000000 --- 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