diff --git a/Fluid.Tests/Domain/WithInterfaces/PetValue.cs b/Fluid.Tests/Domain/WithInterfaces/PetValue.cs index 0df45163..ee0ae2ed 100644 --- a/Fluid.Tests/Domain/WithInterfaces/PetValue.cs +++ b/Fluid.Tests/Domain/WithInterfaces/PetValue.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Text.Encodings.Web; +using System.Threading.Tasks; namespace Fluid.Tests.Domain.WithInterfaces { @@ -42,11 +43,17 @@ public override string ToStringValue() throw new NotImplementedException(); } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { throw new NotImplementedException(); } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + throw new NotImplementedException(); + } + protected override FluidValue GetValue(string name, TemplateContext context) { if (name == "Name") diff --git a/Fluid.Tests/MvcViewEngine/NoSyncStream.cs b/Fluid.Tests/MvcViewEngine/NoSyncStream.cs new file mode 100644 index 00000000..a826714e --- /dev/null +++ b/Fluid.Tests/MvcViewEngine/NoSyncStream.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Fluid.Tests.MvcViewEngine +{ + /// + /// Stream implementation that prevents non-async usages. + /// + public sealed class NoSyncStream : Stream + { + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => Task.CompletedTask; + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; + + + public override void Flush() => throw new InvalidOperationException(); + public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); + public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException(); + public override void SetLength(long value) => throw new InvalidOperationException(); + public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); + + public override bool CanRead { get; } = false; + public override bool CanSeek { get; } = false; + public override bool CanWrite { get; } = true; + public override long Length { get; } + public override long Position { get; set; } + } +} diff --git a/Fluid.Tests/MvcViewEngine/ViewEngineTests.cs b/Fluid.Tests/MvcViewEngine/ViewEngineTests.cs index d9b999bd..94494012 100644 --- a/Fluid.Tests/MvcViewEngine/ViewEngineTests.cs +++ b/Fluid.Tests/MvcViewEngine/ViewEngineTests.cs @@ -1,6 +1,7 @@ using Fluid.Tests.Mocks; using Fluid.ViewEngine; using System.IO; +using System.Linq; using System.Threading.Tasks; using Xunit; @@ -278,5 +279,29 @@ public async Task LayoutShouldBeAbleToIncludeVarsFromViewStart() Assert.Equal("[TITLE][SUBTITLE][ViewStart][View]", sw.ToString()); } + + [Fact] + public async Task RenderViewOnlyAsyncStream_LargePropertyValue_Nested_SmallBuffer_BiggerThan128LengthString() + { + _mockFileProvider.Add("Views/Index.liquid", "{% layout '_Layout' %}{% section bigboy %}{{BigString}}{% endsection %} "); + _mockFileProvider.Add("Views/_Layout.liquid", "{% rendersection bigboy %}"); + + await using var sw = new StreamWriter(new NoSyncStream(), bufferSize: 10); + var template = new TemplateContext(new { BigString = new string(Enumerable.Range(0, 129).Select(x => 'b').ToArray()) }); + await _renderer.RenderViewAsync(sw, "Index.liquid", template); + await sw.FlushAsync(); + } + + [Fact] + public async Task RenderViewOnlyAsyncStream_LargePropertyValue_Nested() + { + _mockFileProvider.Add("Views/Index.liquid", "{% layout '_Layout' %}{% section bigboy %}{{BigString}}{% endsection %} "); + _mockFileProvider.Add("Views/_Layout.liquid", "{% rendersection bigboy %}"); + + await using var sw = new StreamWriter(new NoSyncStream()); + var template = new TemplateContext(new { BigString = new string(Enumerable.Range(0, 1500).Select(_ => 'b').ToArray()) }); + await _renderer.RenderViewAsync(sw, "Index.liquid", template); + await sw.FlushAsync(); + } } } diff --git a/Fluid/Ast/CycleStatement.cs b/Fluid/Ast/CycleStatement.cs index 3be3d7e9..1d34c755 100644 --- a/Fluid/Ast/CycleStatement.cs +++ b/Fluid/Ast/CycleStatement.cs @@ -32,7 +32,7 @@ public override async ValueTask WriteToAsync(TextWriter writer, Text var value = await Values[(int)index].EvaluateAsync(context); context.SetValue(groupValue, NumberValue.Create(index + 1)); - value.WriteTo(writer, encoder, context.CultureInfo); + await value.WriteToAsync(writer, encoder, context.CultureInfo); return Completion.Normal; } diff --git a/Fluid/Ast/DecrementStatement.cs b/Fluid/Ast/DecrementStatement.cs index 9479014c..03150933 100644 --- a/Fluid/Ast/DecrementStatement.cs +++ b/Fluid/Ast/DecrementStatement.cs @@ -12,7 +12,7 @@ public DecrementStatement(string identifier) public string Identifier { get; } - public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) + public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, TemplateContext context) { context.IncrementSteps(); @@ -35,9 +35,9 @@ public override ValueTask WriteToAsync(TextWriter writer, TextEncode context.SetValue(prefixedIdentifier, value); - value.WriteTo(writer, encoder, context.CultureInfo); + await value.WriteToAsync(writer, encoder, context.CultureInfo); - return Normal(); + return Completion.Normal; } protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitDecrementStatement(this); diff --git a/Fluid/Ast/IncrementStatement.cs b/Fluid/Ast/IncrementStatement.cs index 295e52b2..e8551302 100644 --- a/Fluid/Ast/IncrementStatement.cs +++ b/Fluid/Ast/IncrementStatement.cs @@ -36,9 +36,20 @@ public override ValueTask WriteToAsync(TextWriter writer, TextEncode context.SetValue(prefixedIdentifier, value); - value.WriteTo(writer, encoder, context.CultureInfo); + var task = value.WriteToAsync(writer, encoder, context.CultureInfo); - return Normal(); + if (task.IsCompletedSuccessfully) + { + return new ValueTask(Completion.Normal); + } + + return Awaited(task); + + static async ValueTask Awaited(ValueTask t) + { + await t; + return Completion.Normal; + } } protected internal override Statement Accept(AstVisitor visitor) => visitor.VisitIncrementStatement(this); diff --git a/Fluid/Ast/OutputStatement.cs b/Fluid/Ast/OutputStatement.cs index 98be5a96..86e402da 100644 --- a/Fluid/Ast/OutputStatement.cs +++ b/Fluid/Ast/OutputStatement.cs @@ -23,7 +23,7 @@ static async ValueTask Awaited( TemplateContext ctx) { var value = await t; - value.WriteTo(w, enc, ctx.CultureInfo); + await value.WriteToAsync(w, enc, ctx.CultureInfo); return Completion.Normal; } @@ -32,8 +32,20 @@ static async ValueTask Awaited( var task = Expression.EvaluateAsync(context); if (task.IsCompletedSuccessfully) { - task.Result.WriteTo(writer, encoder, context.CultureInfo); - return new ValueTask(Completion.Normal); + var valueTask = task.Result.WriteToAsync(writer, encoder, context.CultureInfo); + + if (valueTask.IsCompletedSuccessfully) + { + return new ValueTask(Completion.Normal); + } + + return AwaitedWriteTo(valueTask); + + static async ValueTask AwaitedWriteTo(ValueTask t) + { + await t; + return Completion.Normal; + } } return Awaited(task, writer, encoder, context); diff --git a/Fluid/Values/ArrayValue.cs b/Fluid/Values/ArrayValue.cs index 3ef598bf..030f695d 100644 --- a/Fluid/Values/ArrayValue.cs +++ b/Fluid/Values/ArrayValue.cs @@ -99,6 +99,7 @@ public override decimal ToNumberValue() public IReadOnlyList Values { get; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); @@ -109,6 +110,16 @@ public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo } } + public override async ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + + foreach (var v in Values) + { + await writer.WriteAsync(v.ToStringValue()); + } + } + public override string ToStringValue() { return String.Join("", Values.Select(x => x.ToStringValue())); diff --git a/Fluid/Values/BlankValue.cs b/Fluid/Values/BlankValue.cs index a0963cd9..3710e275 100644 --- a/Fluid/Values/BlankValue.cs +++ b/Fluid/Values/BlankValue.cs @@ -50,10 +50,16 @@ public override bool IsNil() return true; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; + } + public override bool Equals(object obj) { // The is operator will return false if null diff --git a/Fluid/Values/BooleanValue.cs b/Fluid/Values/BooleanValue.cs index ad40cf47..4f2711d5 100644 --- a/Fluid/Values/BooleanValue.cs +++ b/Fluid/Values/BooleanValue.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using Fluid.Utils; +using System.Globalization; using System.Text.Encodings.Web; namespace Fluid.Values @@ -28,7 +29,7 @@ public static BooleanValue Create(bool value) public override bool Equals(FluidValue other) { // blank == false -> true - if (other.Type == FluidValues.Blank) return _value == false; + if (other.Type == FluidValues.Blank) return !_value; return _value == other.ToBooleanValue(); } @@ -48,12 +49,32 @@ public override string ToStringValue() return _value ? "true" : "false"; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); writer.Write(encoder.Encode(ToStringValue())); } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + var task = writer.WriteAsync(encoder.Encode(ToStringValue())); + + if (task.IsCompletedSuccessfully()) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(Task t) + { + await t; + return; + } + } + public override object ToObjectValue() { return _value ? BoxedTrue : BoxedFalse; diff --git a/Fluid/Values/DateTimeValue.cs b/Fluid/Values/DateTimeValue.cs index 57420eaa..2e1ea48d 100644 --- a/Fluid/Values/DateTimeValue.cs +++ b/Fluid/Values/DateTimeValue.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using Fluid.Utils; +using System.Globalization; using System.Text.Encodings.Web; namespace Fluid.Values @@ -44,12 +45,32 @@ public override string ToStringValue() return _value.ToString("u", CultureInfo.InvariantCulture); } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); writer.Write(_value.ToString("u", cultureInfo)); } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + var task = writer.WriteAsync(_value.ToString("u", cultureInfo)); + + if (task.IsCompletedSuccessfully()) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(Task t) + { + await t; + return; + } + } + public override object ToObjectValue() { return _value; diff --git a/Fluid/Values/DictionaryValue.cs b/Fluid/Values/DictionaryValue.cs index a8962fcf..a4659967 100644 --- a/Fluid/Values/DictionaryValue.cs +++ b/Fluid/Values/DictionaryValue.cs @@ -90,10 +90,16 @@ public override decimal ToNumberValue() return 0; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; + } + public override string ToStringValue() { return ""; diff --git a/Fluid/Values/EmptyValue.cs b/Fluid/Values/EmptyValue.cs index 233d31f6..7042a7b7 100644 --- a/Fluid/Values/EmptyValue.cs +++ b/Fluid/Values/EmptyValue.cs @@ -50,10 +50,16 @@ public override bool IsNil() return true; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; + } + public override bool Equals(object obj) { // The is operator will return false if null diff --git a/Fluid/Values/FactoryValue.cs b/Fluid/Values/FactoryValue.cs index 9367b547..d198b4ac 100644 --- a/Fluid/Values/FactoryValue.cs +++ b/Fluid/Values/FactoryValue.cs @@ -79,10 +79,30 @@ public override string ToStringValue() return _factory.Value.ToStringValue(); } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); _factory.Value.WriteTo(writer, encoder, cultureInfo); } + + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + var task = _factory.Value.WriteToAsync(writer, encoder, cultureInfo); + + if (task.IsCompletedSuccessfully) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(ValueTask t) + { + await t; + return; + } + } } } diff --git a/Fluid/Values/FluidValue.cs b/Fluid/Values/FluidValue.cs index c49e0e1c..5c463a8c 100644 --- a/Fluid/Values/FluidValue.cs +++ b/Fluid/Values/FluidValue.cs @@ -9,8 +9,17 @@ namespace Fluid.Values public abstract class FluidValue : IEquatable #pragma warning restore CA1067 { + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public abstract void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo); + public virtual ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { +#pragma warning disable CS0618 // Type or member is obsolete + WriteTo(writer, encoder, cultureInfo); +#pragma warning restore CS0618 // Type or member is obsolete + return default; + } + private static Dictionary _genericDictionaryTypeCache = new(); [Conditional("DEBUG")] diff --git a/Fluid/Values/ForLoopValue.cs b/Fluid/Values/ForLoopValue.cs index 097fcbf0..fb3b58c4 100644 --- a/Fluid/Values/ForLoopValue.cs +++ b/Fluid/Values/ForLoopValue.cs @@ -57,8 +57,14 @@ public override ValueTask GetValueAsync(string name, TemplateContext }; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { } + + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; + } } } diff --git a/Fluid/Values/FunctionValue.cs b/Fluid/Values/FunctionValue.cs index 0e4a6933..d43dd207 100644 --- a/Fluid/Values/FunctionValue.cs +++ b/Fluid/Values/FunctionValue.cs @@ -55,10 +55,16 @@ public override bool IsNil() return false; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { // A function value should be invoked and its result used instead. - // Calling write to is equivalent to renderding {{ alert }} instead of {{ alert() }} + // Calling write to is equivalent to rendering {{ alert }} instead of {{ alert() }} + } + + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; } public override bool Equals(object obj) diff --git a/Fluid/Values/NilValue.cs b/Fluid/Values/NilValue.cs index e5dc7145..79cc9d47 100644 --- a/Fluid/Values/NilValue.cs +++ b/Fluid/Values/NilValue.cs @@ -52,10 +52,16 @@ public override bool IsNil() return true; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + return default; + } + public override bool Equals(object obj) { // The is operator will return false if null diff --git a/Fluid/Values/NumberValue.cs b/Fluid/Values/NumberValue.cs index 98bc5325..31075579 100644 --- a/Fluid/Values/NumberValue.cs +++ b/Fluid/Values/NumberValue.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using Fluid.Utils; +using System.Globalization; using System.Text.Encodings.Web; namespace Fluid.Values @@ -57,12 +58,32 @@ public override string ToStringValue() return _value.ToString(CultureInfo.InvariantCulture); } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); writer.Write(encoder.Encode(_value.ToString(cultureInfo))); } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + var task = writer.WriteAsync(encoder.Encode(_value.ToString(cultureInfo))); + + if (task.IsCompletedSuccessfully()) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(Task t) + { + await t; + return; + } + } + public override object ToObjectValue() { return _value; @@ -135,7 +156,7 @@ public static int GetScale(decimal value) return 0; } - int[] bits = decimal.GetBits(value); + var bits = decimal.GetBits(value); return (int)((bits[3] >> 16) & 0x7F); } diff --git a/Fluid/Values/ObjectValueBase.cs b/Fluid/Values/ObjectValueBase.cs index 793a3366..e7ebf524 100644 --- a/Fluid/Values/ObjectValueBase.cs +++ b/Fluid/Values/ObjectValueBase.cs @@ -1,4 +1,5 @@ -using System.Collections; +using Fluid.Utils; +using System.Collections; using System.Globalization; using System.Text.Encodings.Web; @@ -104,7 +105,7 @@ private async ValueTask GetNestedValueAsync(string name, TemplateCon { var members = name.Split(MemberSeparators); - object target = Value; + var target = Value; foreach (var prop in members) { @@ -148,12 +149,32 @@ public override decimal ToNumberValue() return Convert.ToDecimal(Value); } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); writer.Write(encoder.Encode(ToStringValue())); } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + var task = writer.WriteAsync(encoder.Encode(ToStringValue())); + + if (task.IsCompletedSuccessfully()) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(Task t) + { + await t; + return; + } + } + public override string ToStringValue() { return Convert.ToString(Value); diff --git a/Fluid/Values/StringValue.cs b/Fluid/Values/StringValue.cs index 1c01fd6b..a5d92cf5 100644 --- a/Fluid/Values/StringValue.cs +++ b/Fluid/Values/StringValue.cs @@ -1,4 +1,5 @@ -using Parlot; +using Fluid.Utils; +using Parlot; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; @@ -110,7 +111,7 @@ protected override FluidValue GetValue(string name, TemplateContext context) { "size" => NumberValue.Create(_value.Length), "first" => _value.Length > 0 ? Create(_value[0]) : NilValue.Instance, - "last" => _value.Length > 0 ? Create(_value[_value.Length - 1]) : NilValue.Instance, + "last" => _value.Length > 0 ? Create(_value[^1]) : NilValue.Instance, _ => NilValue.Instance, }; } @@ -140,6 +141,7 @@ public override string ToStringValue() return _value; } + [Obsolete("WriteTo is obsolete, prefer the WriteToAsync method.")] public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) { AssertWriteToParameters(writer, encoder, cultureInfo); @@ -165,6 +167,46 @@ public override void WriteTo(TextWriter writer, TextEncoder encoder, CultureInfo } } + public override ValueTask WriteToAsync(TextWriter writer, TextEncoder encoder, CultureInfo cultureInfo) + { + AssertWriteToParameters(writer, encoder, cultureInfo); + if (string.IsNullOrEmpty(_value)) + { + return default; + } + + Task task; + + if (Encode) + { + // perf: Don't use this overload + // encoder.Encode(writer, _value); + + // Use a transient string instead of calling + // encoder.Encode(TextWriter) since it would + // call writer.Write on each char if the string + // has even a single char to encode + task = writer.WriteAsync(encoder.Encode(_value)); + } + else + { + task = writer.WriteAsync(_value); + } + + if (task.IsCompletedSuccessfully()) + { + return default; + } + + return Awaited(task); + + static async ValueTask Awaited(Task t) + { + await t; + return; + } + } + public override object ToObjectValue() { return _value;