Skip to content

Commit 7f4544d

Browse files
sebastienrosGitHub Copilot CLI
andauthored
Fix sync io usage in asp.net (#931)
Co-authored-by: GitHub Copilot CLI <copilot@github.com>
1 parent 29757a4 commit 7f4544d

File tree

10 files changed

+226
-98
lines changed

10 files changed

+226
-98
lines changed

Fluid.MvcViewEngine/FluidRendering.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public async Task RenderAsync(TextWriter writer, string path, ViewContext viewCo
5252
bufferSize = 16 * 1024;
5353
}
5454

55-
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true);
55+
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true, allowSynchronousIO: false);
5656
await _fluidViewRenderer.RenderViewAsync(output, path, context);
5757
await output.FlushAsync();
5858
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using Fluid.MvcViewEngine;
2+
using Fluid.Tests.Mocks;
3+
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.AspNetCore.Hosting;
5+
using Microsoft.AspNetCore.Hosting.Server;
6+
using Microsoft.AspNetCore.Hosting.Server.Features;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using System;
10+
using System.Net.Http;
11+
using System.Threading.Tasks;
12+
using Xunit;
13+
14+
namespace Fluid.Tests.MvcViewEngine
15+
{
16+
public class FunctionalServerTests
17+
{
18+
[Fact]
19+
public async Task KestrelWithSynchronousIoDisabled_ShouldRenderView()
20+
{
21+
var expected = new string('b', 4096);
22+
var mockFileProvider = new MockFileProvider();
23+
mockFileProvider.Add("Home/Index.liquid", "{{ Model }}");
24+
25+
var builder = WebApplication.CreateBuilder();
26+
builder.WebHost.UseKestrel(options => options.AllowSynchronousIO = false);
27+
builder.WebHost.UseUrls("http://127.0.0.1:0");
28+
29+
builder.Services.PostConfigure<FluidMvcViewOptions>(options =>
30+
{
31+
options.TemplateOptions.OutputBufferSize = 16;
32+
options.ViewsFileProvider = mockFileProvider;
33+
options.PartialsFileProvider = mockFileProvider;
34+
});
35+
36+
builder.Services
37+
.AddControllersWithViews()
38+
.AddApplicationPart(typeof(FunctionalServerTests).Assembly)
39+
.AddFluid();
40+
41+
var app = builder.Build();
42+
app.UseRouting();
43+
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
44+
45+
await app.StartAsync();
46+
47+
try
48+
{
49+
var addresses = app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()?.Addresses;
50+
var serverAddress = Assert.Single(addresses);
51+
52+
using var client = new HttpClient { BaseAddress = new Uri(serverAddress) };
53+
var response = await client.GetAsync("/Home/Index");
54+
var responseBody = await response.Content.ReadAsStringAsync();
55+
56+
Assert.True(response.IsSuccessStatusCode, $"Response status code was {(int) response.StatusCode}. Body: {responseBody}");
57+
Assert.Equal(expected, responseBody);
58+
}
59+
finally
60+
{
61+
await app.StopAsync();
62+
await app.DisposeAsync();
63+
}
64+
}
65+
66+
}
67+
68+
public class HomeController : Controller
69+
{
70+
[HttpGet]
71+
public IActionResult Index()
72+
{
73+
return View("Index", new string('b', 4096));
74+
}
75+
}
76+
}

Fluid.Tests/MvcViewEngine/ViewEngineTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,26 @@ public async Task RenderViewOnlyAsyncStream_LargePropertyValue_Nested()
311311
#endif
312312
}
313313

314+
[Fact]
315+
public async Task RenderViewOnlyAsyncStream_LargePropertyValue_SmallOutputBuffer()
316+
{
317+
_mockFileProvider.Add("Views/Index.liquid", "{{ BigString }}");
318+
319+
var options = new TemplateOptions
320+
{
321+
OutputBufferSize = 16
322+
};
323+
324+
await using var sw = new StreamWriter(new NoSyncStream(), bufferSize: 10);
325+
var template = new TemplateContext(new { BigString = new string('b', 4096) }, options);
326+
await _renderer.RenderViewAsync(sw, "Index.liquid", template);
327+
#if NET8_0_OR_GREATER
328+
await sw.FlushAsync(TestContext.Current.CancellationToken);
329+
#else
330+
await sw.FlushAsync();
331+
#endif
332+
}
333+
314334
[Fact]
315335
public async Task ShouldApplyTemplateParsedCallback()
316336
{

Fluid.Tests/TextWriterFluidOutputTests.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public async Task Write_SingleChar_WritesContent()
164164
using var writer = new StringWriter();
165165
await using (var output = new TextWriterFluidOutput(writer, bufferSize: 1024, leaveOpen: true))
166166
{
167-
output.Write('A');
167+
output.Write("A");
168168
await output.FlushAsync();
169169
}
170170

@@ -177,11 +177,11 @@ public async Task Write_MultipleChars_WritesAllContent()
177177
using var writer = new StringWriter();
178178
await using (var output = new TextWriterFluidOutput(writer, bufferSize: 1024, leaveOpen: true))
179179
{
180-
output.Write('H');
181-
output.Write('e');
182-
output.Write('l');
183-
output.Write('l');
184-
output.Write('o');
180+
output.Write("H");
181+
output.Write("e");
182+
output.Write("l");
183+
output.Write("l");
184+
output.Write("o");
185185
await output.FlushAsync();
186186
}
187187

@@ -197,7 +197,7 @@ public async Task Write_CharsFillBuffer_FlushesAutomatically()
197197
{
198198
for (int i = 0; i < 10; i++)
199199
{
200-
output.Write('x');
200+
output.Write("x");
201201
}
202202
await output.FlushAsync();
203203
}
@@ -556,6 +556,30 @@ public async Task DisposeAsync_WithAsyncOnlyWriter_CompletesSuccessfully()
556556
Assert.True(asyncOnlyWriter.AsyncWriteCount > 0);
557557
}
558558

559+
[Fact]
560+
public void Write_LargeString_DefaultAllowSynchronousIO_UsesSyncWrite()
561+
{
562+
var trackingWriter = new TrackingTextWriter();
563+
var output = new TextWriterFluidOutput(trackingWriter, bufferSize: 16, leaveOpen: true);
564+
565+
output.Write(new string('x', 128));
566+
output.Dispose();
567+
568+
Assert.True(trackingWriter.SyncWriteCount > 0);
569+
}
570+
571+
[Fact]
572+
public async Task Write_LargeString_WithAllowSynchronousIODisabled_UsesAsyncWrite()
573+
{
574+
var strictWriter = new StrictAsyncOnlyTextWriter();
575+
await using var output = new TextWriterFluidOutput(strictWriter, bufferSize: 16, leaveOpen: true, allowSynchronousIO: false);
576+
577+
output.Write(new string('x', 128));
578+
await output.FlushAsync();
579+
580+
Assert.Equal(new string('x', 128), strictWriter.ToString());
581+
}
582+
559583
/// <summary>
560584
/// Tests that when content is buffered and sync Dispose is called,
561585
/// it performs a synchronous flush.
@@ -720,7 +744,7 @@ public async Task Write_MixedContentSizes_FlushesCorrectly()
720744
output.Write("Small");
721745
output.Write(new string('m', 100)); // Large, direct write
722746
output.Write("Tiny");
723-
output.Write('!');
747+
output.Write("!");
724748
await output.FlushAsync();
725749
}
726750

Fluid.ViewEngine/FluidViewRenderer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public virtual async Task RenderViewAsync(TextWriter writer, string relativePath
5050
bufferSize = 16 * 1024;
5151
}
5252

53-
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true);
53+
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true, allowSynchronousIO: false);
5454
await RenderViewAsync(output, relativePath, context);
5555
await output.FlushAsync();
5656
}
@@ -108,7 +108,7 @@ public virtual async Task RenderPartialAsync(TextWriter writer, string relativeP
108108
bufferSize = 16 * 1024;
109109
}
110110

111-
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true);
111+
await using var output = new TextWriterFluidOutput(writer, bufferSize, leaveOpen: true, allowSynchronousIO: false);
112112
await RenderPartialAsync(output, relativePath, context);
113113
await output.FlushAsync();
114114
}

Fluid/IFluidOutput.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@ public interface IFluidOutput : IBufferWriter<char>
1616
/// </summary>
1717
void Write(string value);
1818

19-
/// <summary>
20-
/// Writes a single character to the output.
21-
/// </summary>
22-
void Write(char value);
23-
2419
/// <summary>
2520
/// Writes a range of characters to the output.
2621
/// </summary>

Fluid/Utils/BufferFluidOutput.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,6 @@ public void Write(string value)
5959
_index += value.Length;
6060
}
6161

62-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
63-
public void Write(char value)
64-
{
65-
EnsureCapacity(1);
66-
_buffer[_index++] = value;
67-
}
68-
6962
public void Write(char[] buffer, int index, int count)
7063
{
7164
ArgumentNullException.ThrowIfNull(buffer);

Fluid/Utils/NullFluidOutput.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ public void Advance(int count) { }
1616

1717
public void Write(string value) { }
1818

19-
public void Write(char value) { }
20-
2119
public void Write(char[] buffer, int index, int count) { }
2220

2321
public ValueTask FlushAsync() => default;

0 commit comments

Comments
 (0)