Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Comment on lines 16 to 20
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions replacing <WarningsAsErrors> with <TreatWarningsAsErrors>, but this repo doesn’t appear to contain <WarningsAsErrors> anywhere, and TreatWarningsAsErrors is already present here. Please update the PR description (or include the missing change) so it accurately reflects what’s actually being modified (e.g., enabling <Nullable>enable</Nullable> in this file).

Copilot uses AI. Check for mistakes.
<PackageOutputPath>$(SolutionDir)artifacts</PackageOutputPath>
<PackageIcon>foundatio-icon.png</PackageIcon>
Expand All @@ -37,7 +38,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.201" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.200" PrivateAssets="All" />
Comment thread
niemyjski marked this conversation as resolved.
Outdated
Comment thread
niemyjski marked this conversation as resolved.
Outdated
<PackageReference Include="AsyncFixer" Version="2.1.0" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="All" />
</ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions samples/Foundatio.AzureStorage.Dequeue/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
Console.WriteLine("Press Ctrl+C to stop...");
Console.WriteLine();

await DequeueMessages(connectionString, queueName, mode, count);
await DequeueMessages(connectionString, queueName!, mode, count);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the console error out if these are not specified and not get this far?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Sample now uses null coalescing with default values instead of null-forgiving operators, eliminating the ! hack.

return 0;
Comment thread
niemyjski marked this conversation as resolved.
Outdated
});

Expand Down Expand Up @@ -120,14 +120,14 @@ static async Task DequeueMessages(string connectionString, string queueName, Azu
processed++;

logger.LogInformation("Dequeued message {MessageId}: '{Message}' from '{Source}' at {Timestamp}",
entry.Id, entry.Value.Message, entry.Value.Source, entry.Value.Timestamp);
entry.Id, entry.Value?.Message, entry.Value?.Source, entry.Value?.Timestamp);

logger.LogInformation(" CorrelationId: '{CorrelationId}'", entry.CorrelationId ?? "<none>");

if (entry.Properties != null && entry.Properties.Count > 0)
{
logger.LogInformation(" Properties: [{Properties}]",
string.Join(", ", entry.Properties.Select(p => $"{p.Key}={p.Value}")));
String.Join(", ", entry.Properties.Select(p => $"{p.Key}={p.Value}")));
}
else
{
Expand Down
12 changes: 7 additions & 5 deletions samples/Foundatio.AzureStorage.Enqueue/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@
Console.WriteLine($"Mode: {mode}");
Console.WriteLine();

await EnqueueMessages(connectionString, queueName, message, correlationId, properties, mode, count);
await EnqueueMessages(connectionString, queueName!, message!, correlationId, properties!, mode, count);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the console error out if these are not specified and not get this far?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Sample now uses null coalescing with default values instead of null-forgiving operators.

return 0;
Comment thread
niemyjski marked this conversation as resolved.
Outdated
});

// Parse and invoke
return await rootCommand.Parse(args).InvokeAsync();

static async Task EnqueueMessages(string connectionString, string queueName, string message, string correlationId, string[] properties, AzureStorageQueueCompatibilityMode mode, int count)
static async Task EnqueueMessages(string connectionString, string queueName, string message, string? correlationId, string[] properties, AzureStorageQueueCompatibilityMode mode, int count)
{
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
var logger = loggerFactory.CreateLogger("Enqueue");
Expand Down Expand Up @@ -122,15 +122,17 @@ static async Task EnqueueMessages(string connectionString, string queueName, str

var entryOptions = new QueueEntryOptions
{
CorrelationId = correlationId,
Properties = queueProperties.Count > 0 ? queueProperties : null
CorrelationId = correlationId
};

if (queueProperties.Count > 0)
entryOptions.Properties = queueProperties;

var messageId = await queue.EnqueueAsync(sampleMessage, entryOptions);

logger.LogInformation("Enqueued message {MessageId}: '{Message}' with CorrelationId: '{CorrelationId}' Properties: [{Properties}]",
messageId, sampleMessage.Message, correlationId ?? "<none>",
string.Join(", ", queueProperties.Select(p => $"{p.Key}={p.Value}")));
String.Join(", ", queueProperties.Select(p => $"{p.Key}={p.Value}")));
}

logger.LogInformation("Successfully enqueued {Count} message(s)", count);
Expand Down
4 changes: 2 additions & 2 deletions samples/Foundatio.AzureStorage.Enqueue/SampleMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Foundatio.AzureStorage.Samples;

public record SampleMessage
{
public string Message { get; init; } = string.Empty;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like these should be made required and we should not be using String.Empty ever for defaults.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: SampleMessage now uses
equired\ properties. No more \String.Empty\ defaults.

public string Message { get; init; } = String.Empty;
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
public string Source { get; init; } = string.Empty;
public string Source { get; init; } = String.Empty;
}
2 changes: 1 addition & 1 deletion src/Foundatio.AzureStorage/Foundatio.AzureStorage.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="Azure.Storage.Queues" Version="12.25.0" />

<PackageReference Include="Foundatio" Version="13.0.0-beta3.32" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<PackageReference Include="Foundatio" Version="13.0.0-beta3.36" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio\Foundatio.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
</ItemGroup>
</Project>
41 changes: 24 additions & 17 deletions src/Foundatio.AzureStorage/Queues/AzureStorageQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ await Task.WhenAll(
}
}

protected override async Task<string> EnqueueImplAsync(T data, QueueEntryOptions options)
protected override async Task<string?> EnqueueImplAsync(T data, QueueEntryOptions options)
{
if (!await OnEnqueuingAsync(data, options).AnyContext())
return null;
Expand Down Expand Up @@ -110,7 +110,7 @@ protected override async Task<string> EnqueueImplAsync(T data, QueueEntryOptions
return response.Value.MessageId;
}

protected override async Task<IQueueEntry<T>> DequeueImplAsync(CancellationToken linkedCancellationToken)
protected override async Task<IQueueEntry<T>?> DequeueImplAsync(CancellationToken linkedCancellationToken)
{
_logger.LogTrace("Checking for message: IsCancellationRequested={IsCancellationRequested} VisibilityTimeout={VisibilityTimeout}", linkedCancellationToken.IsCancellationRequested, _options.WorkItemTimeout);

Expand Down Expand Up @@ -177,41 +177,48 @@ protected override async Task<IQueueEntry<T>> DequeueImplAsync(CancellationToken
message.MessageId, insertedOn, nowUtc, queueTime.TotalMilliseconds, linkedCancellationToken.IsCancellationRequested);
Interlocked.Increment(ref _dequeuedCount);

T data;
string correlationId = null;
IDictionary<string, string> properties = null;
T? data;
string? correlationId = null;
IDictionary<string, string>? properties = null;

try
{
if (_options.CompatibilityMode == AzureStorageQueueCompatibilityMode.Default)
{
try
{
// Unwrap envelope to extract metadata
var envelope = _serializer.Deserialize<QueueMessageEnvelope<T>>(message.Body.ToArray());
data = envelope.Data;
correlationId = envelope.CorrelationId;
properties = envelope.Properties;
if (envelope is not null)
{
Comment thread
niemyjski marked this conversation as resolved.
data = envelope.Data;
correlationId = envelope.CorrelationId;
properties = envelope.Properties;
}
else
{
data = _serializer.Deserialize<T>(message.Body.ToArray());
}
}
catch (Exception ex)
{
// Fallback: try legacy format (raw T) for messages written before the envelope format.
// If this also fails, let the exception propagate to the outer catch which will dead-letter it.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are valid, we should not be removing them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Restored the removed comments (envelope unwrap, legacy fallback, legacy mode).

_logger.LogWarning(ex, "Failed to deserialize message {MessageId} as envelope format, attempting legacy format fallback", message.MessageId);
data = _serializer.Deserialize<T>(message.Body.ToArray());
}
}
else
{
// Legacy mode: deserialize data directly (no envelope)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are valid, we should not be removing them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Restored the removed comments.

data = _serializer.Deserialize<T>(message.Body.ToArray());
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error deserializing message {MessageId} (attempt {DequeueCount}), abandoning for retry", message.MessageId, message.DequeueCount);
Comment thread
niemyjski marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not following our deserializeException pattern!!!!! FIX ALL THE PROVIDERS COME ON.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Now follows the \deserializeException\ pattern matching SQS and Azure Service Bus providers — exception is captured outside the try block and passed to the log message when \data is null.

data = null;
}

var poisonEntry = new AzureStorageQueueEntry<T>(message, null, null, null, this);
if (data is null)
{
var poisonEntry = new AzureStorageQueueEntry<T>(message, null, null, default!, this);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHY did this need to change, just pass null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: Reverted to pass
ull\ directly. Updated \AzureStorageQueueEntry\ constructor to accept \T?\ so null is valid without \default!.

await AbandonAsync(poisonEntry).AnyContext();
Comment thread
niemyjski marked this conversation as resolved.
Outdated
return null;
}
Expand Down Expand Up @@ -419,7 +426,7 @@ protected override void StartWorkingImpl(Func<IQueueEntry<T>, CancellationToken,
{
_logger.LogTrace("WorkerLoop Signaled {QueueName}", _options.Name);

IQueueEntry<T> queueEntry = null;
IQueueEntry<T>? queueEntry = null;
try
{
queueEntry = await DequeueImplAsync(linkedCancellationToken.Token).AnyContext();
Expand Down Expand Up @@ -498,15 +505,15 @@ internal record QueueMessageEnvelope<T> where T : class
/// <summary>
/// Correlation ID for distributed tracing
/// </summary>
public string CorrelationId { get; init; }
public string? CorrelationId { get; init; }

/// <summary>
/// Custom properties/metadata
/// </summary>
public IDictionary<string, string> Properties { get; init; }
public IDictionary<string, string>? Properties { get; init; }

/// <summary>
/// The actual message payload
/// </summary>
public T Data { get; init; }
public required T Data { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class AzureStorageQueueEntry<T> : QueueEntry<T> where T : class
/// </summary>
public string PopReceipt { get; internal set; }

public AzureStorageQueueEntry(QueueMessage message, string correlationId, IDictionary<string, string> properties, T data, IQueue<T> queue)
public AzureStorageQueueEntry(QueueMessage message, string? correlationId, IDictionary<string, string>? properties, T data, IQueue<T> queue)
: base(message.MessageId, correlationId, data, queue, message.InsertedOn?.UtcDateTime ?? DateTime.MinValue, (int)message.DequeueCount)
Comment thread
niemyjski marked this conversation as resolved.
Outdated
{
UnderlyingMessage = message;
Expand Down
6 changes: 3 additions & 3 deletions src/Foundatio.AzureStorage/Queues/AzureStorageQueueOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public enum AzureStorageQueueCompatibilityMode

public class AzureStorageQueueOptions<T> : SharedQueueOptions<T> where T : class
{
public string ConnectionString { get; set; }
public string? ConnectionString { get; set; }

/// <summary>
/// The interval to wait between polling for new messages when the queue is empty.
Expand Down Expand Up @@ -71,14 +71,14 @@ public class AzureStorageQueueOptions<T> : SharedQueueOptions<T> where T : class
/// };
/// </code>
/// </example>
public Action<RetryOptions> ConfigureRetry { get; set; }
public Action<RetryOptions>? ConfigureRetry { get; set; }
}

public class AzureStorageQueueOptionsBuilder<T> : SharedQueueOptionsBuilder<T, AzureStorageQueueOptions<T>, AzureStorageQueueOptionsBuilder<T>> where T : class
{
public AzureStorageQueueOptionsBuilder<T> ConnectionString(string connectionString)
{
ArgumentException.ThrowIfNullOrEmpty(connectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);

Target.ConnectionString = connectionString;
return this;
Expand Down
24 changes: 12 additions & 12 deletions src/Foundatio.AzureStorage/Storage/AzureFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ public AzureFileStorage(Builder<AzureFileStorageOptionsBuilder, AzureFileStorage
public BlobContainerClient Container => _container;

[Obsolete($"Use {nameof(GetFileStreamAsync)} with {nameof(StreamMode)} instead to define read or write behavior of stream")]
public Task<Stream> GetFileStreamAsync(string path, CancellationToken cancellationToken = default)
public Task<Stream?> GetFileStreamAsync(string path, CancellationToken cancellationToken = default)
=> GetFileStreamAsync(path, StreamMode.Read, cancellationToken);

public async Task<Stream> GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default)
public async Task<Stream?> GetFileStreamAsync(string path, StreamMode streamMode, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(path);

Expand Down Expand Up @@ -91,7 +91,7 @@ public async Task<Stream> GetFileStreamAsync(string path, StreamMode streamMode,
}
}

public async Task<FileSpec> GetFileInfoAsync(string path)
public async Task<FileSpec?> GetFileInfoAsync(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);

Comment thread
niemyjski marked this conversation as resolved.
Expand Down Expand Up @@ -255,7 +255,7 @@ public async Task<bool> DeleteFileAsync(string path, CancellationToken cancellat
}
}

public async Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken cancellationToken = default)
public async Task<int> DeleteFilesAsync(string? searchPattern = null, CancellationToken cancellationToken = default)
{
var files = await GetFileListAsync(searchPattern, cancellationToken: cancellationToken).AnyContext();
int count = 0;
Expand All @@ -273,7 +273,7 @@ public async Task<int> DeleteFilesAsync(string searchPattern = null, Cancellatio
return count;
}

public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default)
public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100, string? searchPattern = null, CancellationToken cancellationToken = default)
{
if (pageSize <= 0)
return PagedFileListResult.Empty;
Expand All @@ -283,7 +283,7 @@ public async Task<PagedFileListResult> GetPagedFileListAsync(int pageSize = 100,
return result;
}

private async Task<NextPageResult> GetFiles(string searchPattern, int page, int pageSize, CancellationToken cancellationToken)
private async Task<NextPageResult> GetFiles(string? searchPattern, int page, int pageSize, CancellationToken cancellationToken)
{
int pagingLimit = pageSize;
int skip = (page - 1) * pagingLimit;
Expand All @@ -307,7 +307,7 @@ private async Task<NextPageResult> GetFiles(string searchPattern, int page, int
};
}

private async Task<List<FileSpec>> GetFileListAsync(string searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default)
private async Task<List<FileSpec>> GetFileListAsync(string? searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default)
{
if (limit is <= 0)
return new List<FileSpec>();
Expand Down Expand Up @@ -368,16 +368,16 @@ private static FileSpec ToFileInfo(string path, BlobProperties properties)

private static string NormalizePath(string path)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should return and take a string?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: \NormalizePath\ now accepts \string?\ and returns \string?.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong here this might just be string and not string? as long as we are sure every caller of this has argument validation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping \NormalizePath\ as \string? -> string?\ since it accurately reflects the null-in/null-out behavior. All callers that pass validated (non-null) paths already guard with \ArgumentException.ThrowIfNullOrEmpty\ before calling, so the return is non-null in those paths.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e795f95. Changed \NormalizePath\ to take and return non-nullable \string. All callers already validate null via \ArgumentException.ThrowIfNullOrEmpty\ before calling, and the one \string?\ caller in \GetRequestCriteria\ is guarded by the \String.IsNullOrEmpty\ early return. Removed all !\ null-forgiving operators from call sites.

{
return path?.Replace('\\', '/');
return path.Replace('\\', '/');
}

private record SearchCriteria
{
public string Prefix { get; set; }
public Regex Pattern { get; set; }
public string Prefix { get; set; } = String.Empty;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required no string.empty hacks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: \SearchCriteria.Prefix\ now uses
equired\ property instead of \String.Empty\ default.

public Regex? Pattern { get; set; }
}

private SearchCriteria GetRequestCriteria(string searchPattern)
private SearchCriteria GetRequestCriteria(string? searchPattern)
{
if (String.IsNullOrEmpty(searchPattern))
return new SearchCriteria { Prefix = String.Empty };
Expand All @@ -387,7 +387,7 @@ private SearchCriteria GetRequestCriteria(string searchPattern)
bool hasWildcard = wildcardPos >= 0;

string prefix = normalizedSearchPattern;
Regex patternRegex = null;
Regex? patternRegex = null;

if (hasWildcard)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Foundatio.AzureStorage/Storage/AzureFileStorageOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Foundatio.Storage;

public class AzureFileStorageOptions : SharedOptions
{
public string ConnectionString { get; set; }
public string? ConnectionString { get; set; }
Comment thread
niemyjski marked this conversation as resolved.
public string ContainerName { get; set; } = "storage";

/// <summary>
Expand All @@ -22,14 +22,14 @@ public class AzureFileStorageOptions : SharedOptions
/// };
/// </code>
/// </example>
public Action<RetryOptions> ConfigureRetry { get; set; }
public Action<RetryOptions>? ConfigureRetry { get; set; }
}

public class AzureFileStorageOptionsBuilder : SharedOptionsBuilder<AzureFileStorageOptions, AzureFileStorageOptionsBuilder>
{
public AzureFileStorageOptionsBuilder ConnectionString(string connectionString)
{
ArgumentException.ThrowIfNullOrEmpty(connectionString);
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);

Target.ConnectionString = connectionString;
return this;
Expand Down
2 changes: 1 addition & 1 deletion tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="GitHubActionsTestLogger" Version="3.0.3" PrivateAssets="All" />

<PackageReference Include="Foundatio.TestHarness" Version="13.0.0-beta3.32" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<PackageReference Include="Foundatio.TestHarness" Version="13.0.0-beta3.36" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio.TestHarness\Foundatio.TestHarness.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
</ItemGroup>
</Project>
Loading
Loading