SubscriptionClient carried over.
All checks were successful
Pack and Push NuGet Package / publish (push) Successful in 43s
All checks were successful
Pack and Push NuGet Package / publish (push) Successful in 43s
This commit is contained in:
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.SubscriptionClient\MessengerApi.SubscriptionClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
48
code/MessengerApi.SubscriptionClient.Example/Program.cs
Normal file
48
code/MessengerApi.SubscriptionClient.Example/Program.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using MessengerApi.Model;
|
||||
using MessengerApi.Model.Messages;
|
||||
using portaloggy;
|
||||
|
||||
namespace MessengerApi.Example
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var logger = new ConsoleLogger();
|
||||
var httpClient = new HttpClient();
|
||||
|
||||
var client1 = new Client(
|
||||
new Credentials(
|
||||
"aab8f7e9-ad13-4bf8-bb2e-0cd93d81adc0",
|
||||
"http://localhost:5259"),
|
||||
httpClient,
|
||||
logger);
|
||||
|
||||
var client2 = new SubscriptionClient(
|
||||
new Credentials(
|
||||
"8f73f683-7cb3-40df-998e-6e604aef0e53",
|
||||
"http://localhost:5259"),
|
||||
httpClient,
|
||||
logger);
|
||||
|
||||
var user1 = Guid.Parse("f696442b-e8dc-4074-b34f-94bcece8e74b");
|
||||
var user2 = Guid.Parse("15d97720-f5b7-47aa-9c1a-71f98b0b9248");
|
||||
|
||||
var client2Subscription = client2.Subscribe("TEST");
|
||||
|
||||
client2Subscription.OnMessage += (s, m) =>
|
||||
{
|
||||
logger.Info($"Received subscribed message - {m.Payload}");
|
||||
};
|
||||
|
||||
var messageId = client1.SendMessage(new OutboxMessage
|
||||
{
|
||||
ToUserId = user2,
|
||||
PayloadType = "TEST",
|
||||
Payload = "Testing payload."
|
||||
});
|
||||
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
49
code/MessengerApi.SubscriptionClient.sln
Normal file
49
code/MessengerApi.SubscriptionClient.sln
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35312.102
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\Directory.Packages.props = ..\Directory.Packages.props
|
||||
..\NuGet.config = ..\NuGet.config
|
||||
..\README.md = ..\README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\.gitea\workflows\publish-nuget.yml = ..\.gitea\workflows\publish-nuget.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{C3305381-7A52-4E26-9527-1697692DDD5A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.SubscriptionClient", "MessengerApi.SubscriptionClient\MessengerApi.SubscriptionClient.csproj", "{38F678EF-B8DB-5BF5-CFE7-69BA61F502EF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.SubscriptionClient.Example", "MessengerApi.SubscriptionClient.Example\MessengerApi.SubscriptionClient.Example.csproj", "{0DA95596-3ECF-4AC1-B9F0-E51AF344F64C}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{38F678EF-B8DB-5BF5-CFE7-69BA61F502EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{38F678EF-B8DB-5BF5-CFE7-69BA61F502EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{38F678EF-B8DB-5BF5-CFE7-69BA61F502EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{38F678EF-B8DB-5BF5-CFE7-69BA61F502EF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0DA95596-3ECF-4AC1-B9F0-E51AF344F64C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0DA95596-3ECF-4AC1-B9F0-E51AF344F64C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0DA95596-3ECF-4AC1-B9F0-E51AF344F64C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0DA95596-3ECF-4AC1-B9F0-E51AF344F64C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {C3305381-7A52-4E26-9527-1697692DDD5A}
|
||||
{C3305381-7A52-4E26-9527-1697692DDD5A} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {61948E36-4C2B-4BC9-80B6-9E155CE9F7DE}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Title>$(AssemblyName)</Title>
|
||||
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
|
||||
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
|
||||
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<Title>$(AssemblyName)</Title>
|
||||
<PackageProjectUrl>https://gitea.masita.net/mc/messengerapi.SubscriptionClient</PackageProjectUrl>
|
||||
<RepositoryUrl>https://gitea.masita.net/mc/messengerapi.SubscriptionClient</RepositoryUrl>
|
||||
<PackageTags>logging;log;logger</PackageTags>
|
||||
<PackageLicenseExpression>mit-0</PackageLicenseExpression>
|
||||
<Description>Allows subscription-based consumption of MessengerApi. Simply subscribe to pattern in PayloadType property of the message and SubscriptionClient will let you know when a message arrives.</Description>
|
||||
<Copyright>mc @ 2024</Copyright>
|
||||
<Authors>mc</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessengerApi.Client" />
|
||||
<PackageReference Include="portaloggy" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
52
code/MessengerApi.SubscriptionClient/Model/Subscription.cs
Normal file
52
code/MessengerApi.SubscriptionClient/Model/Subscription.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using MessengerApi.Model.Messages;
|
||||
|
||||
namespace MessengerApi.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// See <see cref="SubscriptionClient"/> if you need to subscribe. It will give you one of these upon subscribing.
|
||||
/// Also see <see cref="OnMessage"/> for what happens where you actually receive message for this sub.
|
||||
/// </summary>
|
||||
public class Subscription : IDisposable
|
||||
{
|
||||
public string MessageTypeMask { get; private set; }
|
||||
|
||||
public event EventHandler<InboxMessage> OnMessage;
|
||||
|
||||
internal SubscriptionClient client;
|
||||
|
||||
private bool isDisposed;
|
||||
|
||||
internal Subscription(string messageTypeMask, SubscriptionClient client)
|
||||
{
|
||||
MessageTypeMask = messageTypeMask;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
internal void Message(InboxMessage message)
|
||||
{
|
||||
OnMessage?.Invoke(this, message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AssertNotDisposedOrThrow();
|
||||
client.Unsubscribe(this);
|
||||
Dispose();
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
private void AssertNotDisposedOrThrow()
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(MessageTypeMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
code/MessengerApi.SubscriptionClient/SubscriptionClient.cs
Normal file
61
code/MessengerApi.SubscriptionClient/SubscriptionClient.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using MessengerApi.Model;
|
||||
using portaloggy;
|
||||
|
||||
namespace MessengerApi
|
||||
{
|
||||
/// <summary>
|
||||
/// This exists so you can mock it.
|
||||
/// </summary>
|
||||
public interface ISubscriptionClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribes to given message type mask. Thread-safe.
|
||||
/// </summary>
|
||||
/// <remarks>Expected format of mask: "MY-MESSAGE-TYPE". No wildcards or placeholders, we only compare MessageType.StartsWith using this value.</remarks>
|
||||
Subscription Subscribe(string payloadTypeMask);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from given subscriptions. Thread-safe.
|
||||
/// </summary>
|
||||
void Unsubscribe(Subscription subscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is where you begin. Instantiate one of these and start subscribing with <see cref="Subscribe(string)"/>.
|
||||
/// </summary>
|
||||
public class SubscriptionClient : Client, ISubscriptionClient
|
||||
{
|
||||
private readonly SubscriptionPollingEngine _engine;
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
public SubscriptionClient(Credentials credentials, HttpClient client = null, ILogger logger = null) : base(credentials, client, logger)
|
||||
{
|
||||
this._engine = new SubscriptionPollingEngine(this._logger, this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this._isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._engine.Dispose();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public Subscription Subscribe(string messageTypeMask)
|
||||
{
|
||||
var sub = new Subscription(messageTypeMask, this);
|
||||
this._engine.AddSubscription(sub);
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
public void Unsubscribe(Subscription subscription)
|
||||
{
|
||||
this._engine.RemoveSubscription(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
using portaloggy;
|
||||
using MessengerApi.Model.Messages;
|
||||
using MessengerApi.Model;
|
||||
|
||||
namespace MessengerApi
|
||||
{
|
||||
internal class SubscriptionPollingEngine : IDisposable
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly IClient client;
|
||||
private readonly List<Subscription> subscriptions = new List<Subscription>();
|
||||
private readonly object subscriptionsLocker = new object();
|
||||
|
||||
private Task executionTask;
|
||||
private CancellationTokenSource executionCts;
|
||||
private bool isDisposed;
|
||||
|
||||
internal SubscriptionPollingEngine(
|
||||
ILogger logger,
|
||||
IClient client)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public Task AddSubscription(Subscription subscription)
|
||||
{
|
||||
this.AssertNotDisposedOrThrow();
|
||||
|
||||
lock (subscriptionsLocker)
|
||||
{
|
||||
subscriptions.Add(subscription);
|
||||
}
|
||||
|
||||
this.logger.Log($"Subscription added for message {subscription.MessageTypeMask}.");
|
||||
|
||||
if(this.executionTask == null && this.executionCts == null)
|
||||
{
|
||||
this.BeginPolling();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.executionCts.Cancel();
|
||||
this.executionCts.Dispose();
|
||||
|
||||
this.isDisposed = true;
|
||||
}
|
||||
|
||||
internal void BeginPolling()
|
||||
{
|
||||
this.executionCts = new CancellationTokenSource();
|
||||
this.executionTask = this.PollEndlessly(this.executionCts.Token);
|
||||
|
||||
this.logger.Log("Polling endlessly now.");
|
||||
}
|
||||
|
||||
internal void RemoveSubscription(Subscription subscription)
|
||||
{
|
||||
lock (subscriptionsLocker)
|
||||
{
|
||||
this.subscriptions.Remove(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PollEndlessly(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var messages = Enumerable.Empty<InboxMessage>();
|
||||
|
||||
try
|
||||
{
|
||||
messages = this.client.GetMessages();
|
||||
this.logger.Info($"Received {messages.Count()} messages.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.Error("Can't obtain messages.", ex);
|
||||
}
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sub = (Subscription)null;
|
||||
|
||||
lock (this.subscriptionsLocker)
|
||||
{
|
||||
sub = this.subscriptions.FirstOrDefault(x => message.PayloadType.StartsWith(x.MessageTypeMask));
|
||||
}
|
||||
|
||||
if (sub == null)
|
||||
{
|
||||
this.logger.Log($"This message has no subscription and will be ignored: {message.PayloadType}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
sub.Message(message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.logger.Error("Can't process received message.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
this.logger.Info("Polling ended.");
|
||||
}
|
||||
|
||||
private void AssertNotDisposedOrThrow()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user