Skip to content

Commit 37fd26b

Browse files
committed
fixes
1 parent ec2eb57 commit 37fd26b

File tree

18 files changed

+449
-287
lines changed

18 files changed

+449
-287
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ env:
1010
DOTNET_VERSION: '10.0.x'
1111

1212
jobs:
13-
build:
14-
name: Build and Test
13+
build-and-test:
14+
name: build-and-test
1515
runs-on: ubuntu-latest
1616

1717
steps:

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ If no new rule is detected → do not update the file.
142142
- Nullability is enabled: annotate optional members; avoid `!` unless justified
143143
- Suffix async APIs with `Async`; keep test names aligned with existing patterns (e.g., `DownloadFile_WhenFileExists_ReturnsSuccess`)
144144
- Remove unused usings and let analyzers guide layout
145+
- When a `foreach` loop’s first step is just transforming the iteration variable (e.g., `var y = Map(x)`), prefer mapping the sequence explicitly with `.Select(...)` so intent is clearer and analyzers stay quiet
145146
- No magic literals — extract to constants, enums, or config when it improves clarity
146147

147148
### Git & PRs
@@ -150,6 +151,7 @@ If no new rule is detected → do not update the file.
150151
- Group related edits in one commit and avoid WIP spam
151152
- PRs should summarize impact, list touched projects, reference issues, and note new configuration or secrets
152153
- Include the `dotnet` commands you ran and add logs when CI needs context
154+
- Keep a required CI check named `build-and-test` running on every PR and push to `main` so branch protection always receives a status (it’s worse for merges if the check is missing/never reported than if it runs and fails)
153155

154156
### Critical (NEVER violate)
155157

Integraions/ManagedCode.Storage.Client/StorageClient.cs

Lines changed: 113 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public async Task<Result<BlobMetadata>> UploadFile(Stream stream, string apiUrl,
4545
using var formData = new MultipartFormDataContent();
4646
formData.Add(streamContent, contentName, contentName);
4747

48-
var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
48+
using var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
4949

5050
if (response.IsSuccessStatusCode)
5151
return await response.Content.ReadFromJsonAsync<Result<BlobMetadata>>(cancellationToken: cancellationToken);
@@ -63,7 +63,7 @@ public async Task<Result<BlobMetadata>> UploadFile(FileInfo fileInfo, string api
6363
{
6464
formData.Add(streamContent, contentName, contentName);
6565

66-
var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
66+
using var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
6767

6868
if (response.IsSuccessStatusCode)
6969
{
@@ -77,27 +77,21 @@ public async Task<Result<BlobMetadata>> UploadFile(FileInfo fileInfo, string api
7777

7878
public async Task<Result<BlobMetadata>> UploadFile(byte[] bytes, string apiUrl, string contentName, CancellationToken cancellationToken = default)
7979
{
80-
using (var stream = new MemoryStream())
81-
{
82-
stream.Write(bytes, 0, bytes.Length);
83-
84-
using var streamContent = new StreamContent(stream);
85-
86-
using (var formData = new MultipartFormDataContent())
87-
{
88-
formData.Add(streamContent, contentName, contentName);
80+
using var stream = new MemoryStream(bytes, writable: false);
81+
using var streamContent = new StreamContent(stream);
82+
using var formData = new MultipartFormDataContent();
8983

90-
var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
84+
formData.Add(streamContent, contentName, contentName);
9185

92-
if (response.IsSuccessStatusCode)
93-
{
94-
var result = await response.Content.ReadFromJsonAsync<Result<BlobMetadata>>(cancellationToken: cancellationToken);
95-
return result;
96-
}
86+
using var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
9787

98-
return Result<BlobMetadata>.Fail(response.StatusCode);
99-
}
88+
if (response.IsSuccessStatusCode)
89+
{
90+
var result = await response.Content.ReadFromJsonAsync<Result<BlobMetadata>>(cancellationToken: cancellationToken);
91+
return result;
10092
}
93+
94+
return Result<BlobMetadata>.Fail(response.StatusCode);
10195
}
10296

10397
public async Task<Result<BlobMetadata>> UploadFile(string base64, string apiUrl, string contentName,
@@ -110,7 +104,7 @@ public async Task<Result<BlobMetadata>> UploadFile(string base64, string apiUrl,
110104

111105
formData.Add(fileContent, contentName, contentName);
112106

113-
var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
107+
using var response = await httpClient.PostAsync(apiUrl, formData, cancellationToken);
114108

115109
if (response.IsSuccessStatusCode)
116110
return await response.Content.ReadFromJsonAsync<Result<BlobMetadata>>(cancellationToken: cancellationToken);
@@ -188,11 +182,13 @@ public async Task<Result<uint>> UploadLargeFile(Stream file, string uploadApiUrl
188182
formData.Add(new StringContent(bytesRead.ToString()), "Payload.ChunkSize");
189183
formData.Add(new StringContent(totalChunks.ToString()), "Payload.TotalChunks");
190184

191-
var response = await httpClient.PostAsync(uploadApiUrl, formData, cancellationToken);
192-
if (!response.IsSuccessStatusCode)
185+
using (var response = await httpClient.PostAsync(uploadApiUrl, formData, cancellationToken))
193186
{
194-
var message = await response.Content.ReadAsStringAsync(cancellationToken);
195-
return Result<uint>.Fail(response.StatusCode, message);
187+
if (!response.IsSuccessStatusCode)
188+
{
189+
var message = await response.Content.ReadAsStringAsync(cancellationToken);
190+
return Result<uint>.Fail(response.StatusCode, message);
191+
}
196192
}
197193

198194
transmitted += bytesRead;
@@ -230,7 +226,7 @@ public async Task<Result<uint>> UploadLargeFile(Stream file, string uploadApiUrl
230226
KeepMergedFile = false
231227
};
232228

233-
var mergeResult = await httpClient.PostAsJsonAsync(completeApiUrl, completePayload, cancellationToken);
229+
using var mergeResult = await httpClient.PostAsJsonAsync(completeApiUrl, completePayload, cancellationToken);
234230
if (!mergeResult.IsSuccessStatusCode)
235231
{
236232
var message = await mergeResult.Content.ReadAsStringAsync(cancellationToken);
@@ -308,14 +304,25 @@ public async Task<Result<Stream>> GetFileStream(string fileName, string apiUrl,
308304
{
309305
try
310306
{
311-
var response = await httpClient.GetAsync($"{apiUrl}/{fileName}");
312-
if (response.IsSuccessStatusCode)
307+
var response = await httpClient.GetAsync($"{apiUrl}/{fileName}", HttpCompletionOption.ResponseHeadersRead, cancellationToken);
308+
if (!response.IsSuccessStatusCode)
309+
{
310+
response.Dispose();
311+
return Result<Stream>.Fail(response.StatusCode);
312+
}
313+
314+
Stream contentStream;
315+
try
316+
{
317+
contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
318+
}
319+
catch
313320
{
314-
var stream = await response.Content.ReadAsStreamAsync();
315-
return Result<Stream>.Succeed(stream);
321+
response.Dispose();
322+
throw;
316323
}
317324

318-
return Result<Stream>.Fail(response.StatusCode);
325+
return Result<Stream>.Succeed(new HttpResponseMessageStream(contentStream, response));
319326
}
320327
catch (HttpRequestException e) when (e.StatusCode != null)
321328
{
@@ -328,6 +335,82 @@ public async Task<Result<Stream>> GetFileStream(string fileName, string apiUrl,
328335
}
329336
}
330337

338+
file sealed class HttpResponseMessageStream(Stream innerStream, HttpResponseMessage response) : Stream
339+
{
340+
private readonly Stream _innerStream = innerStream ?? throw new ArgumentNullException(nameof(innerStream));
341+
private readonly HttpResponseMessage _response = response ?? throw new ArgumentNullException(nameof(response));
342+
343+
public override bool CanRead => _innerStream.CanRead;
344+
public override bool CanSeek => _innerStream.CanSeek;
345+
public override bool CanWrite => _innerStream.CanWrite;
346+
public override long Length => _innerStream.Length;
347+
348+
public override long Position
349+
{
350+
get => _innerStream.Position;
351+
set => _innerStream.Position = value;
352+
}
353+
354+
public override void Flush() => _innerStream.Flush();
355+
356+
public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken);
357+
358+
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
359+
360+
public override int Read(Span<byte> buffer) => _innerStream.Read(buffer);
361+
362+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
363+
_innerStream.ReadAsync(buffer, offset, count, cancellationToken);
364+
365+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) =>
366+
_innerStream.ReadAsync(buffer, cancellationToken);
367+
368+
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
369+
370+
public override void SetLength(long value) => _innerStream.SetLength(value);
371+
372+
public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
373+
374+
public override void Write(ReadOnlySpan<byte> buffer) => _innerStream.Write(buffer);
375+
376+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
377+
_innerStream.WriteAsync(buffer, offset, count, cancellationToken);
378+
379+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) =>
380+
_innerStream.WriteAsync(buffer, cancellationToken);
381+
382+
protected override void Dispose(bool disposing)
383+
{
384+
if (disposing)
385+
{
386+
try
387+
{
388+
_innerStream.Dispose();
389+
}
390+
finally
391+
{
392+
_response.Dispose();
393+
}
394+
}
395+
396+
base.Dispose(disposing);
397+
}
398+
399+
public override async ValueTask DisposeAsync()
400+
{
401+
try
402+
{
403+
await _innerStream.DisposeAsync();
404+
}
405+
finally
406+
{
407+
_response.Dispose();
408+
}
409+
410+
await base.DisposeAsync();
411+
}
412+
}
413+
331414
file class ChunkUploadCompleteRequestDto
332415
{
333416
public string UploadId { get; set; } = string.Empty;

ManagedCode.Storage.Core/BaseStorage.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,23 +110,25 @@ public Task<Result<BlobMetadata>> UploadAsync(Stream stream, UploadOptions optio
110110
return UploadInternalAsync(stream, SetUploadOptions(options), cancellationToken);
111111
}
112112

113-
public Task<Result<BlobMetadata>> UploadAsync(byte[] data, UploadOptions options, CancellationToken cancellationToken = default)
113+
public async Task<Result<BlobMetadata>> UploadAsync(byte[] data, UploadOptions options, CancellationToken cancellationToken = default)
114114
{
115115
if (string.IsNullOrWhiteSpace(options.MimeType))
116116
options.MimeType = MimeHelper.GetMimeType(options.FileName);
117117

118-
return UploadInternalAsync(new MemoryStream(data), SetUploadOptions(options), cancellationToken);
118+
using var stream = new MemoryStream(data, writable: false);
119+
return await UploadInternalAsync(stream, SetUploadOptions(options), cancellationToken);
119120
}
120121

121-
public Task<Result<BlobMetadata>> UploadAsync(string content, UploadOptions options, CancellationToken cancellationToken = default)
122+
public async Task<Result<BlobMetadata>> UploadAsync(string content, UploadOptions options, CancellationToken cancellationToken = default)
122123
{
123124
if (string.IsNullOrWhiteSpace(options.MimeType))
124125
options.MimeType = MimeHelper.TEXT;
125126

126-
return UploadInternalAsync(new Utf8StringStream(content), SetUploadOptions(options), cancellationToken);
127+
using var stream = new Utf8StringStream(content);
128+
return await UploadInternalAsync(stream, SetUploadOptions(options), cancellationToken);
127129
}
128130

129-
public Task<Result<BlobMetadata>> UploadAsync(FileInfo fileInfo, UploadOptions options, CancellationToken cancellationToken = default)
131+
public async Task<Result<BlobMetadata>> UploadAsync(FileInfo fileInfo, UploadOptions options, CancellationToken cancellationToken = default)
130132
{
131133
if (string.IsNullOrWhiteSpace(options.MimeType))
132134
options.MimeType = MimeHelper.GetMimeType(fileInfo.Extension);
@@ -136,7 +138,8 @@ public Task<Result<BlobMetadata>> UploadAsync(FileInfo fileInfo, UploadOptions o
136138
options.FileName = fileInfo.Name;
137139
}
138140

139-
return UploadInternalAsync(fileInfo.OpenRead(), SetUploadOptions(options), cancellationToken);
141+
using var stream = fileInfo.OpenRead();
142+
return await UploadInternalAsync(stream, SetUploadOptions(options), cancellationToken);
140143
}
141144

142145
public Task<Result<LocalFile>> DownloadAsync(string fileName, CancellationToken cancellationToken = default)
@@ -336,4 +339,4 @@ public void Dispose()
336339
if (StorageClient is IDisposable disposable)
337340
disposable.Dispose();
338341
}
339-
}
342+
}

Storages/ManagedCode.Storage.Azure/AzureStorage.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,8 @@ protected override async Task<Result> DeleteDirectoryInternalAsync(string direct
204204
{
205205
var blobs = StorageClient.GetBlobs(prefix: directory, cancellationToken: cancellationToken);
206206

207-
foreach (var blob in blobs)
207+
foreach (var blobClient in blobs.Select(blob => StorageClient.GetBlobClient(blob.Name)))
208208
{
209-
var blobClient = StorageClient.GetBlobClient(blob.Name);
210209
await blobClient.DeleteIfExistsAsync(DeleteSnapshotsOption.None, null, cancellationToken);
211210
}
212211

Tests/ManagedCode.Storage.Tests/AspNetTests/CrossProvider/CrossProviderSyncTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ public async Task SyncBlobAcrossProviders_PreservesPayloadAndMetadata(string sou
4242
await EnsureContainerAsync(sourceStorage);
4343
await EnsureContainerAsync(targetStorage);
4444

45-
var payload = new byte[256 * 1024];
46-
RandomNumberGenerator.Fill(payload);
45+
var payload = new byte[256 * 1024];
46+
RandomNumberGenerator.Fill(payload);
4747

48-
var expectedCrc = Crc32Helper.CalculateStreamCrc(new MemoryStream(payload, writable: false));
48+
using var crcStream = new MemoryStream(payload, writable: false);
49+
var expectedCrc = Crc32Helper.CalculateStreamCrc(crcStream);
4950

5051
var directory = $"sync-tests/{Guid.NewGuid():N}";
5152
var fileName = $"payload-{Guid.NewGuid():N}.bin";

Tests/ManagedCode.Storage.Tests/Common/FileHelper.cs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.IO;
33
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
46
using ManagedCode.MimeTypes;
57
using ManagedCode.Storage.Core.Models;
68
using Microsoft.AspNetCore.Http;
@@ -65,18 +67,17 @@ public static LocalFile GenerateLocalFileWithData(LocalFile file, int sizeInByte
6567
public static IFormFile GenerateFormFile(string fileName, int byteSize)
6668
{
6769
var localFile = GenerateLocalFile(fileName, byteSize);
70+
var contentType = MimeHelper.GetMimeType(localFile.FileInfo.Extension);
6871

69-
var ms = new MemoryStream();
70-
localFile.FileStream.CopyTo(ms);
71-
var formFile = new FormFile(ms, 0, ms.Length, fileName, fileName)
72+
byte[] bytes;
73+
using (localFile)
7274
{
73-
Headers = new HeaderDictionary(),
74-
ContentType = MimeHelper.GetMimeType(localFile.FileInfo.Extension)
75-
};
76-
77-
localFile.Dispose();
75+
using var ms = new MemoryStream();
76+
localFile.FileStream.CopyTo(ms);
77+
bytes = ms.ToArray();
78+
}
7879

79-
return formFile;
80+
return new InMemoryFormFile(bytes, fileName, fileName, contentType);
8081
}
8182

8283
public static string GenerateRandomFileName()
@@ -94,4 +95,47 @@ public static string GenerateRandomFileContent(int charCount = 250_000)
9495
.Select(s => s[Random.Next(s.Length)])
9596
.ToArray());
9697
}
98+
99+
private sealed class InMemoryFormFile : IFormFile
100+
{
101+
private readonly byte[] _content;
102+
103+
public InMemoryFormFile(byte[] content, string name, string fileName, string contentType)
104+
{
105+
_content = content;
106+
Name = name;
107+
FileName = fileName;
108+
ContentType = contentType;
109+
Headers = new HeaderDictionary
110+
{
111+
{ "Content-Type", contentType }
112+
};
113+
ContentDisposition = $"form-data; name=\"{name}\"; filename=\"{fileName}\"";
114+
}
115+
116+
public string ContentType { get; }
117+
public string ContentDisposition { get; }
118+
public IHeaderDictionary Headers { get; }
119+
public long Length => _content.Length;
120+
public string Name { get; }
121+
public string FileName { get; }
122+
123+
public Stream OpenReadStream() => new MemoryStream(_content, writable: false);
124+
125+
public void CopyTo(Stream target)
126+
{
127+
ArgumentNullException.ThrowIfNull(target);
128+
129+
using var stream = OpenReadStream();
130+
stream.CopyTo(target);
131+
}
132+
133+
public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default)
134+
{
135+
ArgumentNullException.ThrowIfNull(target);
136+
137+
await using var stream = OpenReadStream();
138+
await stream.CopyToAsync(target, cancellationToken);
139+
}
140+
}
97141
}

0 commit comments

Comments
 (0)