@@ -1,8 +1,8 @@ | |||||
<Project> | <Project> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<VersionMajor>1</VersionMajor> | <VersionMajor>1</VersionMajor> | ||||
<VersionMinor>1</VersionMinor> | |||||
<VersionPatch>1</VersionPatch> | |||||
<VersionMinor>2</VersionMinor> | |||||
<VersionPatch>0</VersionPatch> | |||||
<VersionQuality></VersionQuality> | <VersionQuality></VersionQuality> | ||||
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> | <VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -0,0 +1,18 @@ | |||||
using System; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
public class EFOptions | |||||
{ | |||||
public const string DefaultSchema = "cap"; | |||||
/// <summary> | |||||
/// Gets or sets the schema to use when creating database objects. | |||||
/// Default is <see cref="DefaultSchema"/>. | |||||
/// </summary> | |||||
public string Schema { get; set; } = DefaultSchema; | |||||
public Type DbContextType { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,49 @@ | |||||
using System; | |||||
using DotNetCore.CAP; | |||||
using Microsoft.EntityFrameworkCore; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace Microsoft.Extensions.DependencyInjection | |||||
{ | |||||
public static class CapOptionsExtensions | |||||
{ | |||||
public static CapOptions UsePostgreSql(this CapOptions options, string connectionString) | |||||
{ | |||||
return options.UsePostgreSql(opt => | |||||
{ | |||||
opt.ConnectionString = connectionString; | |||||
}); | |||||
} | |||||
public static CapOptions UsePostgreSql(this CapOptions options, Action<PostgreSqlOptions> configure) | |||||
{ | |||||
if (configure == null) throw new ArgumentNullException(nameof(configure)); | |||||
options.RegisterExtension(new PostgreSqlCapOptionsExtension(configure)); | |||||
return options; | |||||
} | |||||
public static CapOptions UseEntityFramework<TContext>(this CapOptions options) | |||||
where TContext : DbContext | |||||
{ | |||||
return options.UseEntityFramework<TContext>(opt => | |||||
{ | |||||
opt.DbContextType = typeof(TContext); | |||||
}); | |||||
} | |||||
public static CapOptions UseEntityFramework<TContext>(this CapOptions options, Action<EFOptions> configure) | |||||
where TContext : DbContext | |||||
{ | |||||
if (configure == null) throw new ArgumentNullException(nameof(configure)); | |||||
var efOptions = new EFOptions { DbContextType = typeof(TContext) }; | |||||
configure(efOptions); | |||||
options.RegisterExtension(new PostgreSqlCapOptionsExtension(configure)); | |||||
return options; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,52 @@ | |||||
using System; | |||||
using DotNetCore.CAP.Processor; | |||||
using DotNetCore.CAP.PostgreSql; | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
internal class PostgreSqlCapOptionsExtension : ICapOptionsExtension | |||||
{ | |||||
private readonly Action<PostgreSqlOptions> _configure; | |||||
public PostgreSqlCapOptionsExtension(Action<PostgreSqlOptions> configure) | |||||
{ | |||||
_configure = configure; | |||||
} | |||||
public void AddServices(IServiceCollection services) | |||||
{ | |||||
services.AddSingleton<IStorage, PostgreSqlStorage>(); | |||||
services.AddScoped<IStorageConnection, PostgreSqlStorageConnection>(); | |||||
services.AddScoped<ICapPublisher, CapPublisher>(); | |||||
services.AddTransient<IAdditionalProcessor, DefaultAdditionalProcessor>(); | |||||
var postgreSqlOptions = new PostgreSqlOptions(); | |||||
_configure(postgreSqlOptions); | |||||
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(postgreSqlOptions); | |||||
} | |||||
#if NETSTANDARD1_6 | |||||
private IServiceProvider TempBuildService(IServiceCollection services) | |||||
{ | |||||
return services.BuildServiceProvider(); | |||||
} | |||||
#else | |||||
private ServiceProvider TempBuildService(IServiceCollection services) | |||||
{ | |||||
return services.BuildServiceProvider(); | |||||
} | |||||
#endif | |||||
} | |||||
} |
@@ -0,0 +1,11 @@ | |||||
// ReSharper disable once CheckNamespace | |||||
namespace DotNetCore.CAP | |||||
{ | |||||
public class PostgreSqlOptions : EFOptions | |||||
{ | |||||
/// <summary> | |||||
/// Gets or sets the database's connection string that will be used to store database entities. | |||||
/// </summary> | |||||
public string ConnectionString { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,78 @@ | |||||
using System; | |||||
using System.Data; | |||||
using System.Threading.Tasks; | |||||
using Dapper; | |||||
using DotNetCore.CAP.Models; | |||||
using DotNetCore.CAP.Abstractions; | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.EntityFrameworkCore.Storage; | |||||
using Microsoft.Extensions.Logging; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class CapPublisher : CapPublisherBase | |||||
{ | |||||
private readonly ILogger _logger; | |||||
private readonly PostgreSqlOptions _options; | |||||
private readonly DbContext _dbContext; | |||||
public CapPublisher(IServiceProvider provider, | |||||
ILogger<CapPublisher> logger, | |||||
PostgreSqlOptions options) | |||||
{ | |||||
_options = options; | |||||
_logger = logger; | |||||
if (_options.DbContextType != null) | |||||
{ | |||||
IsUsingEF = true; | |||||
_dbContext = (DbContext)ServiceProvider.GetService(_options.DbContextType); | |||||
} | |||||
} | |||||
protected override void PrepareConnectionForEF() | |||||
{ | |||||
_dbConnection = _dbContext.Database.GetDbConnection(); | |||||
var transaction = _dbContext.Database.CurrentTransaction; | |||||
if (transaction == null) | |||||
{ | |||||
IsCapOpenedTrans = true; | |||||
transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted); | |||||
} | |||||
_dbTranasaction = transaction.GetDbTransaction(); | |||||
} | |||||
protected override void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) | |||||
{ | |||||
dbConnection.Execute(PrepareSql(), message, dbTransaction); | |||||
_logger.LogDebug("Message has been persisted in the database. name:" + message.ToString()); | |||||
if (IsCapOpenedTrans) | |||||
{ | |||||
dbTransaction.Commit(); | |||||
dbTransaction.Dispose(); | |||||
dbConnection.Dispose(); | |||||
} | |||||
} | |||||
protected override async Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message) | |||||
{ | |||||
await dbConnection.ExecuteAsync(PrepareSql(), message, dbTransaction); | |||||
_logger.LogDebug("Message has been persisted in the database. name:" + message.ToString()); | |||||
if (IsCapOpenedTrans) | |||||
{ | |||||
dbTransaction.Commit(); | |||||
dbTransaction.Dispose(); | |||||
dbConnection.Dispose(); | |||||
} | |||||
} | |||||
private string PrepareSql() | |||||
{ | |||||
return $"INSERT INTO \"{_options.Schema}\".\"published\" (\"Name\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Content,@Retries,@Added,@ExpiresAt,@StatusName)"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,26 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<Import Project="..\..\build\common.props" /> | |||||
<PropertyGroup> | |||||
<TargetFrameworks>netstandard1.6;netstandard2.0;</TargetFrameworks> | |||||
<AssemblyName>DotNetCore.CAP.PostgreSql</AssemblyName> | |||||
<PackageTags>$(PackageTags);PostgreSQL</PackageTags> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard1.6|AnyCPU'"> | |||||
<DefineConstants>TRACE;DEBUG</DefineConstants> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Dapper" Version="1.50.2" /> | |||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" /> | |||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.2" /> | |||||
<PackageReference Include="Npgsql" Version="3.2.5" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\DotNetCore.CAP\DotNetCore.CAP.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,11 @@ | |||||
using DotNetCore.CAP.Models; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
internal class FetchedMessage | |||||
{ | |||||
public int MessageId { get; set; } | |||||
public MessageType MessageType { get; set; } | |||||
} | |||||
} |
@@ -0,0 +1,61 @@ | |||||
using System; | |||||
using System.Threading.Tasks; | |||||
using Dapper; | |||||
using DotNetCore.CAP.Processor; | |||||
using Microsoft.Extensions.Logging; | |||||
using Npgsql; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class DefaultAdditionalProcessor : IAdditionalProcessor | |||||
{ | |||||
private readonly IServiceProvider _provider; | |||||
private readonly ILogger _logger; | |||||
private readonly PostgreSqlOptions _options; | |||||
private const int MaxBatch = 1000; | |||||
private readonly TimeSpan _delay = TimeSpan.FromSeconds(1); | |||||
private readonly TimeSpan _waitingInterval = TimeSpan.FromHours(2); | |||||
private static readonly string[] Tables = | |||||
{ | |||||
"published","received" | |||||
}; | |||||
public DefaultAdditionalProcessor( | |||||
IServiceProvider provider, | |||||
ILogger<DefaultAdditionalProcessor> logger, | |||||
PostgreSqlOptions sqlServerOptions) | |||||
{ | |||||
_logger = logger; | |||||
_provider = provider; | |||||
_options = sqlServerOptions; | |||||
} | |||||
public async Task ProcessAsync(ProcessingContext context) | |||||
{ | |||||
_logger.LogDebug("Collecting expired entities."); | |||||
foreach (var table in Tables) | |||||
{ | |||||
var removedCount = 0; | |||||
do | |||||
{ | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
removedCount = await connection.ExecuteAsync($"DELETE FROM \"{_options.Schema}\".\"{table}\" WHERE ExpiresAt < @now LIMIT @count;", | |||||
new { now = DateTime.Now, count = MaxBatch }); | |||||
} | |||||
if (removedCount != 0) | |||||
{ | |||||
await context.WaitAsync(_delay); | |||||
context.ThrowIfStopping(); | |||||
} | |||||
} while (removedCount != 0); | |||||
} | |||||
await context.WaitAsync(_waitingInterval); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,74 @@ | |||||
using System; | |||||
using System.Data; | |||||
using System.Threading; | |||||
using Dapper; | |||||
using DotNetCore.CAP.Models; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class PostgreSqlFetchedMessage : IFetchedMessage | |||||
{ | |||||
private readonly IDbConnection _connection; | |||||
private readonly IDbTransaction _transaction; | |||||
private readonly Timer _timer; | |||||
private static readonly TimeSpan KeepAliveInterval = TimeSpan.FromMinutes(1); | |||||
private readonly object _lockObject = new object(); | |||||
public PostgreSqlFetchedMessage(int messageId, | |||||
MessageType type, | |||||
IDbConnection connection, | |||||
IDbTransaction transaction) | |||||
{ | |||||
MessageId = messageId; | |||||
MessageType = type; | |||||
_connection = connection; | |||||
_transaction = transaction; | |||||
_timer = new Timer(ExecuteKeepAliveQuery, null, KeepAliveInterval, KeepAliveInterval); | |||||
} | |||||
public int MessageId { get; } | |||||
public MessageType MessageType { get; } | |||||
public void RemoveFromQueue() | |||||
{ | |||||
lock (_lockObject) | |||||
{ | |||||
_transaction.Commit(); | |||||
} | |||||
} | |||||
public void Requeue() | |||||
{ | |||||
lock (_lockObject) | |||||
{ | |||||
_transaction.Rollback(); | |||||
} | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
lock (_lockObject) | |||||
{ | |||||
_timer?.Dispose(); | |||||
_transaction.Dispose(); | |||||
_connection.Dispose(); | |||||
} | |||||
} | |||||
private void ExecuteKeepAliveQuery(object obj) | |||||
{ | |||||
lock (_lockObject) | |||||
{ | |||||
try | |||||
{ | |||||
_connection?.Execute("SELECT 1", _transaction); | |||||
} | |||||
catch | |||||
{ | |||||
// ignored | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,67 @@ | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using Dapper; | |||||
using Microsoft.EntityFrameworkCore; | |||||
using Microsoft.Extensions.Logging; | |||||
using Npgsql; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class PostgreSqlStorage : IStorage | |||||
{ | |||||
private readonly PostgreSqlOptions _options; | |||||
private readonly ILogger _logger; | |||||
public PostgreSqlStorage(ILogger<PostgreSqlStorage> logger, PostgreSqlOptions options) | |||||
{ | |||||
_options = options; | |||||
_logger = logger; | |||||
} | |||||
public async Task InitializeAsync(CancellationToken cancellationToken) | |||||
{ | |||||
if (cancellationToken.IsCancellationRequested) return; | |||||
var sql = CreateDbTablesScript(_options.Schema); | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
await connection.ExecuteAsync(sql); | |||||
} | |||||
_logger.LogDebug("Ensuring all create database tables script are applied."); | |||||
} | |||||
protected virtual string CreateDbTablesScript(string schema) | |||||
{ | |||||
var batchSql = $@" | |||||
CREATE SCHEMA IF NOT EXISTS ""{schema}""; | |||||
CREATE TABLE IF NOT EXISTS ""{schema}"".""queue""( | |||||
""MessageId"" int NOT NULL , | |||||
""MessageType"" int NOT NULL | |||||
); | |||||
CREATE TABLE IF NOT EXISTS ""{schema}"".""received""( | |||||
""Id"" SERIAL PRIMARY KEY NOT NULL, | |||||
""Name"" VARCHAR(200) NOT NULL, | |||||
""Group"" VARCHAR(200) NULL, | |||||
""Content"" TEXT NULL, | |||||
""Retries"" INT NOT NULL, | |||||
""Added"" TIMESTAMP NOT NULL, | |||||
""ExpiresAt"" TIMESTAMP NULL, | |||||
""StatusName"" VARCHAR(50) NOT NULL | |||||
); | |||||
CREATE TABLE IF NOT EXISTS ""{schema}"".""published""( | |||||
""Id"" SERIAL PRIMARY KEY NOT NULL, | |||||
""Name"" VARCHAR(200) NOT NULL, | |||||
""Content"" TEXT NULL, | |||||
""Retries"" INT NOT NULL, | |||||
""Added"" TIMESTAMP NOT NULL, | |||||
""ExpiresAt"" TIMESTAMP NULL, | |||||
""StatusName"" VARCHAR(50) NOT NULL | |||||
);"; | |||||
return batchSql; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,140 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Data; | |||||
using System.Threading.Tasks; | |||||
using Dapper; | |||||
using DotNetCore.CAP.Infrastructure; | |||||
using DotNetCore.CAP.Models; | |||||
using Npgsql; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class PostgreSqlStorageConnection : IStorageConnection | |||||
{ | |||||
private readonly PostgreSqlOptions _options; | |||||
public PostgreSqlStorageConnection(PostgreSqlOptions options) | |||||
{ | |||||
_options = options; | |||||
} | |||||
public PostgreSqlOptions Options => _options; | |||||
public IStorageTransaction CreateTransaction() | |||||
{ | |||||
return new PostgreSqlStorageTransaction(this); | |||||
} | |||||
public async Task<CapPublishedMessage> GetPublishedMessageAsync(int id) | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"Id\"={id}"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql); | |||||
} | |||||
} | |||||
public Task<IFetchedMessage> FetchNextMessageAsync() | |||||
{ | |||||
var sql = $@" | |||||
SELECT ""MessageId"",""MessageType"" FROM ""{_options.Schema}"".""queue"" LIMIT 1 FOR UPDATE; | |||||
DELETE FROM ""{_options.Schema}"".""queue"" LIMIT 1;"; | |||||
return FetchNextMessageCoreAsync(sql); | |||||
} | |||||
public async Task<CapPublishedMessage> GetNextPublishedMessageToBeEnqueuedAsync() | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\" = '{StatusName.Scheduled}' LIMIT 1;"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryFirstOrDefaultAsync<CapPublishedMessage>(sql); | |||||
} | |||||
} | |||||
public async Task<IEnumerable<CapPublishedMessage>> GetFailedPublishedMessages() | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"published\" WHERE \"StatusName\"='{StatusName.Failed}'"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryAsync<CapPublishedMessage>(sql); | |||||
} | |||||
} | |||||
// CapReceviedMessage | |||||
public async Task StoreReceivedMessageAsync(CapReceivedMessage message) | |||||
{ | |||||
if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
var sql = $"INSERT INTO \"{_options.Schema}\".\"received\"(\"Name\",\"Group\",\"Content\",\"Retries\",\"Added\",\"ExpiresAt\",\"StatusName\")VALUES(@Name,@Group,@Content,@Retries,@Added,@ExpiresAt,@StatusName);"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
await connection.ExecuteAsync(sql, message); | |||||
} | |||||
} | |||||
public async Task<CapReceivedMessage> GetReceivedMessageAsync(int id) | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"Id\"={id}"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql); | |||||
} | |||||
} | |||||
public async Task<CapReceivedMessage> GetNextReceviedMessageToBeEnqueuedAsync() | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\" = '{StatusName.Scheduled}' LIMIT 1;"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryFirstOrDefaultAsync<CapReceivedMessage>(sql); | |||||
} | |||||
} | |||||
public async Task<IEnumerable<CapReceivedMessage>> GetFailedReceviedMessages() | |||||
{ | |||||
var sql = $"SELECT * FROM \"{_options.Schema}\".\"received\" WHERE \"StatusName\"='{StatusName.Failed}'"; | |||||
using (var connection = new NpgsqlConnection(_options.ConnectionString)) | |||||
{ | |||||
return await connection.QueryAsync<CapReceivedMessage>(sql); | |||||
} | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
} | |||||
private async Task<IFetchedMessage> FetchNextMessageCoreAsync(string sql, object args = null) | |||||
{ | |||||
//here don't use `using` to dispose | |||||
var connection = new NpgsqlConnection(_options.ConnectionString); | |||||
await connection.OpenAsync(); | |||||
var transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted); | |||||
FetchedMessage fetchedMessage = null; | |||||
try | |||||
{ | |||||
fetchedMessage = await connection.QueryFirstOrDefaultAsync<FetchedMessage>(sql, args, transaction); | |||||
} | |||||
catch (NpgsqlException) | |||||
{ | |||||
transaction.Dispose(); | |||||
throw; | |||||
} | |||||
if (fetchedMessage == null) | |||||
{ | |||||
transaction.Rollback(); | |||||
transaction.Dispose(); | |||||
connection.Dispose(); | |||||
return null; | |||||
} | |||||
return new PostgreSqlFetchedMessage(fetchedMessage.MessageId, fetchedMessage.MessageType, connection, transaction); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,71 @@ | |||||
using System; | |||||
using System.Data; | |||||
using System.Threading.Tasks; | |||||
using Dapper; | |||||
using DotNetCore.CAP.Models; | |||||
using Npgsql; | |||||
namespace DotNetCore.CAP.PostgreSql | |||||
{ | |||||
public class PostgreSqlStorageTransaction : IStorageTransaction, IDisposable | |||||
{ | |||||
private readonly string _schema; | |||||
private readonly IDbTransaction _dbTransaction; | |||||
private readonly IDbConnection _dbConnection; | |||||
public PostgreSqlStorageTransaction(PostgreSqlStorageConnection connection) | |||||
{ | |||||
var options = connection.Options; | |||||
_schema = options.Schema; | |||||
_dbConnection = new NpgsqlConnection(options.ConnectionString); | |||||
_dbConnection.Open(); | |||||
_dbTransaction = _dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); | |||||
} | |||||
public void UpdateMessage(CapPublishedMessage message) | |||||
{ | |||||
if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
var sql = $"UPDATE [{_schema}].[Published] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;"; | |||||
_dbConnection.Execute(sql, message, _dbTransaction); | |||||
} | |||||
public void UpdateMessage(CapReceivedMessage message) | |||||
{ | |||||
if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
var sql = $"UPDATE [{_schema}].[Received] SET [Retries] = @Retries,[ExpiresAt] = @ExpiresAt,[StatusName]=@StatusName WHERE Id=@Id;"; | |||||
_dbConnection.Execute(sql, message, _dbTransaction); | |||||
} | |||||
public void EnqueueMessage(CapPublishedMessage message) | |||||
{ | |||||
if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);"; | |||||
_dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Publish }, _dbTransaction); | |||||
} | |||||
public void EnqueueMessage(CapReceivedMessage message) | |||||
{ | |||||
if (message == null) throw new ArgumentNullException(nameof(message)); | |||||
var sql = $"INSERT INTO [{_schema}].[Queue] values(@MessageId,@MessageType);"; | |||||
_dbConnection.Execute(sql, new CapQueue { MessageId = message.Id, MessageType = MessageType.Subscribe }, _dbTransaction); | |||||
} | |||||
public Task CommitAsync() | |||||
{ | |||||
_dbTransaction.Commit(); | |||||
return Task.CompletedTask; | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
_dbTransaction.Dispose(); | |||||
_dbConnection.Dispose(); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,143 @@ | |||||
using System; | |||||
using System.Data; | |||||
using System.Threading.Tasks; | |||||
using DotNetCore.CAP.Infrastructure; | |||||
using DotNetCore.CAP.Models; | |||||
using DotNetCore.CAP.Processor; | |||||
namespace DotNetCore.CAP.Abstractions | |||||
{ | |||||
public abstract class CapPublisherBase : ICapPublisher | |||||
{ | |||||
protected IDbConnection _dbConnection; | |||||
protected IDbTransaction _dbTranasaction; | |||||
protected bool IsCapOpenedTrans { get; set; } | |||||
protected bool IsUsingEF { get; set; } | |||||
protected IServiceProvider ServiceProvider { get; } | |||||
public void Publish<T>(string name, T contentObj) | |||||
{ | |||||
CheckIsUsingEF(name); | |||||
PrepareConnectionForEF(); | |||||
var content = Serialize(contentObj); | |||||
PublishWithTrans(name, content, _dbConnection, _dbTranasaction); | |||||
} | |||||
public Task PublishAsync<T>(string name, T contentObj) | |||||
{ | |||||
CheckIsUsingEF(name); | |||||
PrepareConnectionForEF(); | |||||
var content = Serialize(contentObj); | |||||
return PublishWithTransAsync(name, content, _dbConnection, _dbTranasaction); | |||||
} | |||||
public void Publish<T>(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) | |||||
{ | |||||
CheckIsAdoNet(name); | |||||
PrepareConnectionForAdo(dbConnection, ref dbTransaction); | |||||
var content = Serialize(contentObj); | |||||
PublishWithTrans(name, content, dbConnection, dbTransaction); | |||||
} | |||||
public Task PublishAsync<T>(string name, T contentObj, IDbConnection dbConnection, IDbTransaction dbTransaction = null) | |||||
{ | |||||
CheckIsAdoNet(name); | |||||
PrepareConnectionForAdo(dbConnection, ref dbTransaction); | |||||
var content = Serialize(contentObj); | |||||
return PublishWithTransAsync(name, content, dbConnection, dbTransaction); | |||||
} | |||||
protected abstract void PrepareConnectionForEF(); | |||||
protected abstract void Execute(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message); | |||||
protected abstract Task ExecuteAsync(IDbConnection dbConnection, IDbTransaction dbTransaction, CapPublishedMessage message); | |||||
#region private methods | |||||
private string Serialize<T>(T obj) | |||||
{ | |||||
string content = string.Empty; | |||||
if (Helper.IsComplexType(typeof(T))) | |||||
{ | |||||
content = Helper.ToJson(obj); | |||||
} | |||||
else | |||||
{ | |||||
content = obj.ToString(); | |||||
} | |||||
return content; | |||||
} | |||||
private void PrepareConnectionForAdo(IDbConnection dbConnection, ref IDbTransaction dbTransaction) | |||||
{ | |||||
if (dbConnection == null) | |||||
throw new ArgumentNullException(nameof(dbConnection)); | |||||
if (dbConnection.State != ConnectionState.Open) | |||||
dbConnection.Open(); | |||||
if (dbTransaction == null) | |||||
{ | |||||
IsCapOpenedTrans = true; | |||||
dbTransaction = dbConnection.BeginTransaction(IsolationLevel.ReadCommitted); | |||||
} | |||||
} | |||||
private void CheckIsUsingEF(string name) | |||||
{ | |||||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
if (!IsUsingEF) | |||||
throw new InvalidOperationException("If you are using the EntityFramework, you need to configure the DbContextType first." + | |||||
" otherwise you need to use overloaded method with IDbConnection and IDbTransaction."); | |||||
} | |||||
private void CheckIsAdoNet(string name) | |||||
{ | |||||
if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
if (IsUsingEF) | |||||
throw new InvalidOperationException("If you are using the EntityFramework, you do not need to use this overloaded."); | |||||
} | |||||
private async Task PublishWithTransAsync(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) | |||||
{ | |||||
var message = new CapPublishedMessage | |||||
{ | |||||
Name = name, | |||||
Content = content, | |||||
StatusName = StatusName.Scheduled | |||||
}; | |||||
await ExecuteAsync(dbConnection, dbTransaction, message); | |||||
PublishQueuer.PulseEvent.Set(); | |||||
} | |||||
private void PublishWithTrans(string name, string content, IDbConnection dbConnection, IDbTransaction dbTransaction) | |||||
{ | |||||
var message = new CapPublishedMessage | |||||
{ | |||||
Name = name, | |||||
Content = content, | |||||
StatusName = StatusName.Scheduled | |||||
}; | |||||
Execute(dbConnection, dbTransaction, message); | |||||
PublishQueuer.PulseEvent.Set(); | |||||
} | |||||
#endregion private methods | |||||
} | |||||
} |
@@ -34,5 +34,10 @@ namespace DotNetCore.CAP.Models | |||||
public int Retries { get; set; } | public int Retries { get; set; } | ||||
public string StatusName { get; set; } | public string StatusName { get; set; } | ||||
public override string ToString() | |||||
{ | |||||
return "name:" + Name + ", content:" + Content; | |||||
} | |||||
} | } | ||||
} | } |
@@ -47,5 +47,10 @@ namespace DotNetCore.CAP.Models | |||||
Content = Content | Content = Content | ||||
}; | }; | ||||
} | } | ||||
public override string ToString() | |||||
{ | |||||
return "name:" + Name + ", content:" + Content; | |||||
} | |||||
} | } | ||||
} | } |