Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ services.AddAzureStorageQueueClient(x =>
});
```

### Add typed clients (similar to IHttpClientFactory)

The library now supports strongly-typed clients similar to the IHttpClientFactory pattern in ASP.NET Core:

```csharp
// Register a typed client for a specific message type using the default queue client
services.AddAzureStorageQueueClient(x =>
x.AddDefaultClient(y => Configuration.Bind(nameof(QueueClientSettings), y)));
services.AddTypedQueueClient<MyMessage>();

// Register a typed client for a specific message type using a named queue client
services.AddAzureStorageQueueClient(x =>
x.AddClient("orders", y => Configuration.Bind(nameof(OrderQueueSettings), y)));
services.AddTypedQueueClient<OrderMessage>("orders");
```

This allows you to inject `ITypedQueueClient<TMessage>` directly instead of using the factory pattern:

```csharp
public class OrderService
{
private readonly ITypedQueueClient<OrderMessage> _queueClient;

public OrderService(ITypedQueueClient<OrderMessage> queueClient)
{
_queueClient = queueClient;
}

public async Task ProcessOrderAsync(OrderMessage order)
{
await _queueClient.SendMessageAsync(order);
}
}
```

## Using the IQueueClientFactory

### Example 1: Get a default queue client
Expand Down Expand Up @@ -150,6 +185,8 @@ public class MyClass

The following example shows the .NET Worker Service template where the class uses the `IHostedService` interface to send a message every five seconds.

### Using the IQueueClientFactory

1. Inject the `IQueueClientFactory` interface and use as follows:

```csharp
Expand All @@ -171,10 +208,35 @@ The following example shows the .NET Worker Service template where the class use
}
```

### Using Typed Clients

Alternatively, you can use typed clients for a cleaner dependency injection experience:

```csharp
public class Sender : IHostedService
{
private readonly ITypedQueueClient<MyMessage> _queueClient;

public Sender(ITypedQueueClient<MyMessage> queueClient) => _queueClient = queueClient;

public async Task StartAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var myMessage = new MyMessage("Test");
await _queueClient.SendMessageAsync(myMessage, cancellationToken);
await Task.Delay(5000);
}
}
}
```

## Receiving and handling messages from an Azure storage account queue

The following example shows the .NET Worker Service template where the class uses the `IHostedService` interface to run a particular code block repeatedly. The application will receive the payload from the queue repeatedly.

### Using the IQueueClientFactory

1. Inject the `IQueueClientFactory` interface and use as follows:

```csharp
Expand Down Expand Up @@ -204,6 +266,36 @@ The following example shows the .NET Worker Service template where the class use
}
```

### Using Typed Clients

Alternatively, you can use typed clients for a cleaner dependency injection experience:

```csharp
public class MySubscriber : IHostedService
{
private readonly ITypedQueueClient<MyMessage> _queueClient;
private readonly IMyMessageHandler _myMessageHandler;

public MySubscriber(ITypedQueueClient<MyMessage> queueClient, IMyMessageHandler myMessageHandler)
{
_queueClient = queueClient;
_myMessageHandler = myMessageHandler;
}

public async Task StartAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
await _queueClient.ReceiveMessagesAsync(
message => _myMessageHandler.HandleAsync(message),
exception => _myMessageHandler.HandleExceptionAsync(exception),
cancellationToken);
await Task.Delay(1000);
}
}
}
```

### Handling multiple messages

The library allows you to pull multiple messages by specifying the `maxMessage` count as an integer in the `ReceiveMessagesAsync<T>()` method. These are sent to the handler as individual messages but pulled from the queue as a batch the consuming application would hold a lock on for the default duration used in the Azure Storage Queue library.
Expand Down
10 changes: 0 additions & 10 deletions src/AzureStorage.QueueService/IQueueClientBuilder.cs

This file was deleted.

64 changes: 64 additions & 0 deletions src/AzureStorage.QueueService/ITypedQueueClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Azure.Storage.Queues.Models;

namespace AzureStorage.QueueService;

/// <summary>
/// Represents a typed queue client for a specific message type.
/// This interface provides strongly-typed operations for queue interactions.
/// </summary>
/// <typeparam name="TMessage">The type of message this client handles</typeparam>
public interface ITypedQueueClient<TMessage> where TMessage : class
{
/// <summary>
/// Creates the queue if it doesn't exist.
/// </summary>
/// <param name="metadata">Optional metadata for the queue</param>
/// <param name="cancellationToken">Cancellation token</param>
ValueTask CreateQueueIfNotExistsAsync(IDictionary<string, string>? metadata = null, CancellationToken cancellationToken = default);

/// <summary>
/// Clears all messages from the queue.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
ValueTask ClearMessagesAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Peeks at messages without removing them from the queue.
/// </summary>
/// <param name="numMessages">Number of messages to peek at</param>
/// <param name="cancellationToken">Cancellation token</param>
ValueTask<IEnumerable<TMessage>> PeekMessagesAsync(int numMessages, CancellationToken cancellationToken = default);

/// <summary>
/// Peeks at messages without removing them from the queue (synchronous).
/// </summary>
/// <param name="numMessages">Number of messages to peek at</param>
/// <param name="cancellationToken">Cancellation token</param>
IEnumerable<TMessage> PeekMessages(int numMessages, CancellationToken cancellationToken = default);

/// <summary>
/// Receives and processes messages from the queue.
/// </summary>
/// <param name="handleMessage">Delegate to handle each received message</param>
/// <param name="handleException">Delegate to handle exceptions during processing</param>
/// <param name="numMessages">Number of messages to receive</param>
/// <param name="cancellationToken">Cancellation token</param>
ValueTask ReceiveMessagesAsync(
Func<TMessage?, IDictionary<string, string>?, ValueTask> handleMessage,
Func<Exception, IDictionary<string, string>?, ValueTask> handleException,
int numMessages = 1,
CancellationToken cancellationToken = default);

/// <summary>
/// Sends a message to the queue.
/// </summary>
/// <param name="message">The message to send</param>
/// <param name="visibilityTimeout">Optional visibility timeout</param>
/// <param name="timeToLive">Optional time to live</param>
/// <param name="cancellationToken">Cancellation token</param>
ValueTask<SendReceipt> SendMessageAsync(
TMessage message,
TimeSpan? visibilityTimeout = null,
TimeSpan? timeToLive = null,
CancellationToken cancellationToken = default);
}
41 changes: 0 additions & 41 deletions src/AzureStorage.QueueService/QueueClientBuilder.cs

This file was deleted.

27 changes: 22 additions & 5 deletions src/AzureStorage.QueueService/QueueClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AzureStorage.QueueService.Telemetry;
using Azure.Storage.Queues;
using AzureStorage.QueueService.Telemetry;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand All @@ -9,20 +10,17 @@ internal sealed class QueueClientFactory : IQueueClientFactory
private readonly Dictionary<string, AzureStorageQueueClient> _namedClients = new();
private AzureStorageQueueClient? _defaultClient;
private readonly QueueClientSettingsRegistry _registry;
private readonly IQueueClientBuilder _queueClientBuilder;
private readonly ILoggerFactory _loggerFactory;
private readonly IMessageConverter _messageConverter;
private readonly IOptions<QueueServiceTelemetrySettings> _telemetrySettings;

public QueueClientFactory(
QueueClientSettingsRegistry registry,
IQueueClientBuilder queueClientBuilder,
ILoggerFactory loggerFactory,
IMessageConverter messageConverter,
IOptions<QueueServiceTelemetrySettings> telemetrySettingsOptions)
{
_registry = registry;
_queueClientBuilder = queueClientBuilder;
_loggerFactory = loggerFactory;
_messageConverter = messageConverter;
_telemetrySettings = telemetrySettingsOptions;
Expand Down Expand Up @@ -60,8 +58,27 @@ public AzureStorageQueueClient GetQueueClient()

private AzureStorageQueueClient Create(QueueClientSettings settings)
{
var queueClient = _queueClientBuilder.CreateClient(settings);
var queueClient = CreateQueueClient(settings);
var azureStorageQueueClient = new AzureStorageQueueClient(_messageConverter, queueClient, _loggerFactory, _telemetrySettings);
return azureStorageQueueClient;
}

private QueueClient CreateQueueClient(QueueClientSettings settings)
{
QueueClient? client = default;
if (settings.TokenCredential is not null && settings.EndpointUri is not null)
client = new QueueClient(settings.EndpointUri, settings.TokenCredential);

if (settings.ConnectionString is not null)
client = new QueueClient(settings.ConnectionString, settings.QueueName);

if (client is null)
throw new ApplicationException(
"There was a problem creating the client. Unable to determine if the endpoint URI or connection string should be used. " +
"Please be sure to set the connection string or the token credential and endpoint URI together.");

if (settings.CreateIfNotExists) client.CreateIfNotExists();

return client;
}
}
57 changes: 56 additions & 1 deletion src/AzureStorage.QueueService/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,67 @@ public static IServiceCollection AddAzureStorageQueueClient(this IServiceCollect

services.AddSingleton(Registry);
services.AddSingleton<IQueueClientFactory, QueueClientFactory>();
services.AddSingleton<IQueueClientBuilder, QueueClientBuilder>();
services.AddSingleton<IMessageConverter>(new JsonQueueMessageConverter(serializerOptions));

return services;
}

/// <summary>
/// Adds a strongly-typed queue client using the default queue client configuration.
/// Similar to IHttpClientFactory's typed client pattern.
/// </summary>
/// <typeparam name="TClient">The typed client interface</typeparam>
/// <typeparam name="TImplementation">The typed client implementation</typeparam>
/// <typeparam name="TMessage">The message type the client handles</typeparam>
/// <param name="services">The service collection</param>
/// <param name="clientName">Optional name of the queue client to use. If null, uses the default client.</param>
/// <returns>The service collection</returns>
public static IServiceCollection AddTypedQueueClient<TClient, TImplementation, TMessage>(
this IServiceCollection services,
string? clientName = null)
where TClient : class, ITypedQueueClient<TMessage>
where TImplementation : class, TClient
where TMessage : class
{
services.AddTransient<TClient>(provider =>
{
var factory = provider.GetRequiredService<IQueueClientFactory>();
var queueClient = string.IsNullOrEmpty(clientName)
? factory.GetQueueClient()
: factory.GetQueueClient(clientName);

var typedClient = new TypedQueueClient<TMessage>(queueClient);
return (TClient)(object)typedClient;
});

return services;
}

/// <summary>
/// Adds a strongly-typed queue client interface with default implementation.
/// </summary>
/// <typeparam name="TMessage">The message type the client handles</typeparam>
/// <param name="services">The service collection</param>
/// <param name="clientName">Optional name of the queue client to use. If null, uses the default client.</param>
/// <returns>The service collection</returns>
public static IServiceCollection AddTypedQueueClient<TMessage>(
this IServiceCollection services,
string? clientName = null)
where TMessage : class
{
services.AddTransient<ITypedQueueClient<TMessage>>(provider =>
{
var factory = provider.GetRequiredService<IQueueClientFactory>();
var queueClient = string.IsNullOrEmpty(clientName)
? factory.GetQueueClient()
: factory.GetQueueClient(clientName);

return new TypedQueueClient<TMessage>(queueClient);
});

return services;
}

public static IServiceCollection ConfigureQueueServiceTelemetry(this IServiceCollection services, Action<QueueServiceTelemetrySettings> options)
{
services.Configure(options);
Expand Down
Loading
Loading