Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,49 @@ public void QuickStart_OtherInputAndOutputShapes_Work()
}
}

// quick-start.md "Web Applications (Async I/O)", FAQ.md "Is there an async API?" and
// Examples.md "Uploaded Templates (Async I/O)"
[Fact]
public async Task QuickStart_UploadBufferedAsynchronously_IsProcessed()
{
CancellationToken cancellationToken = TestContext.Current.CancellationToken;
var processor = new DocumentTemplateProcessor(new PlaceholderReplacementOptions { Culture = _invariant });
var data = new Dictionary<string, object> { ["CustomerName"] = "Jane" };
using AsyncOnlyStream uploadedFile = new AsyncOnlyStream(CreateTemplate("Hi {{CustomerName}}").ToArray());

using var templateStream = new MemoryStream();
await uploadedFile.CopyToAsync(templateStream, cancellationToken);
templateStream.Position = 0;

using var outputStream = new MemoryStream();
ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data);

Assert.True(result.IsSuccess);
Assert.Equal(new[] { "Hi Jane" }, ReadParagraphs(outputStream.ToArray()));
}

// Examples.md "Uploaded Templates (Async I/O)": the format of the buffered upload selects the content type
[Fact]
public async Task Examples_UploadedTemplate_FormatIsDetectedAndProcessed()
{
CancellationToken cancellationToken = TestContext.Current.CancellationToken;
using AsyncOnlyStream template = new AsyncOnlyStream(CreateTemplate("Hi {{CustomerName}}").ToArray());

using var templateStream = new MemoryStream();
await template.CopyToAsync(templateStream, cancellationToken);
templateStream.Position = 0;

TemplateFormat format = TemplateProcessor.DetectFormat(templateStream);

using var outputStream = new MemoryStream();
ProcessingResult result = new TemplateProcessor().ProcessTemplate(
templateStream, outputStream, """{"CustomerName": "Jane"}""");

Assert.Equal(TemplateFormat.Docx, format);
Assert.True(result.IsSuccess);
Assert.Equal(new[] { "Hi Jane" }, ReadParagraphs(outputStream.ToArray()));
}

[Fact]
public void QuickStart_WriteOnlyOutputStream_IsAFailedResult()
{
Expand Down Expand Up @@ -360,4 +403,55 @@ private sealed class WriteOnlyStream : MemoryStream
{
public override bool CanRead => false;
}

/// <summary>
/// A request/upload stream as in ASP.NET Core with <c>AllowSynchronousIO = false</c>: not seekable, and synchronous
/// reads throw.
/// </summary>
private sealed class AsyncOnlyStream : Stream
{
private readonly MemoryStream _content;

public AsyncOnlyStream(byte[] content) => _content = new MemoryStream(content, writable: false);

public override bool CanRead => true;

public override bool CanSeek => false;

public override bool CanWrite => false;

public override long Length => throw new NotSupportedException();

public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

public override int Read(byte[] buffer, int offset, int count) =>
throw new InvalidOperationException("Synchronous operations are disallowed.");

public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) =>
_content.ReadAsync(buffer, cancellationToken);

public override void Flush()
{
}

public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

public override void SetLength(long value) => throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

protected override void Dispose(bool disposing)
{
if (disposing)
{
_content.Dispose();
}

base.Dispose(disposing);
}
}
}
37 changes: 37 additions & 0 deletions TriasDev.Templify/Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,43 @@ public class ContractData
}
```

### Uploaded Templates (Async I/O)

The processing API is synchronous: processing runs in memory and typically takes milliseconds. Request streams in
ASP.NET Core do not allow synchronous reads (Kestrel's `AllowSynchronousIO` is `false`), so passing an upload stream
directly as the template fails. Buffer the upload asynchronously first, then process the buffer:

```csharp
[HttpPost("render")]
public async Task<IActionResult> Render(IFormFile template, [FromForm] string data, CancellationToken cancellationToken)
{
// Read the upload asynchronously; the processor then only works on memory.
using var templateStream = new MemoryStream();
await template.CopyToAsync(templateStream, cancellationToken);
templateStream.Position = 0;

// TemplateProcessor accepts Word (.docx) and OpenDocument (.odt/.ott) templates.
TemplateFormat format = TemplateProcessor.DetectFormat(templateStream);

using var outputStream = new MemoryStream();
ProcessingResult result = new TemplateProcessor().ProcessTemplate(templateStream, outputStream, data);

if (!result.IsSuccess)
{
return BadRequest(new { error = result.ErrorMessage });
}

// File(...) writes the response body asynchronously.
return format == TemplateFormat.Odt
? File(outputStream.ToArray(), "application/vnd.oasis.opendocument.text", "document.odt")
: File(outputStream.ToArray(),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "document.docx");
}
```

The output stream cannot be `Response.Body` either: a Word output must be readable, writable and seekable. Write into a
`MemoryStream` and return it as shown.

### Dependency Injection Setup

```csharp
Expand Down
24 changes: 24 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ syntax is the same, and LibreOffice does not need to be installed where the temp
- Desktop applications
- Console applications

In ASP.NET Core, buffer uploaded templates asynchronously before processing (see
[Is there an async API?](#q-is-there-an-async-api)).

---

## Features & Capabilities
Expand Down Expand Up @@ -564,6 +567,27 @@ size. See the [performance notes](https://github.com/TriasDev/templify/blob/main
- Keep the template bytes in memory and use `ProcessTemplate(byte[] template, data, out byte[] output)`
- The API is synchronous; process independent documents in parallel for throughput

### Q: Is there an async API?

**A:** No, and none is planned for now. Processing is CPU-bound and runs in memory (typically milliseconds), so an
async method would only wrap synchronous work. The I/O around it is in your code, where it can be async:

- **Input:** request and upload streams in ASP.NET Core do not allow synchronous reads. Copy them into a
`MemoryStream` with `await CopyToAsync(...)` and pass that stream as the template.
- **Output:** a Word output stream must be readable, writable and seekable, so write into a `MemoryStream` and return
it (`File(...)` in ASP.NET Core sends it asynchronously).

```csharp
using var templateStream = new MemoryStream();
await uploadedFile.CopyToAsync(templateStream, cancellationToken);
templateStream.Position = 0;

using var outputStream = new MemoryStream();
ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data);
```

For many documents, process them in parallel (see the next question).

### Q: Can I process templates in parallel?

**A:** **Yes!** A `DocumentTemplateProcessor` and its options can be shared by concurrent calls (register custom boolean formatters before sharing the options). Best practice:
Expand Down
17 changes: 17 additions & 0 deletions docs/for-developers/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ Every overload (stream, `byte[]` and file) also accepts a JSON string instead of
[Using JSON Data](#using-json-data)). `TextTemplateProcessor.ProcessTemplate` and
`DocumentTemplateProcessor.ValidateTemplate` accept `IReadOnlyDictionary<string, object?>` data as well.

### Web Applications (Async I/O)

The API is synchronous: processing is CPU-bound, runs in memory and typically takes milliseconds. Do the I/O around it
asynchronously. In ASP.NET Core, request and upload streams do not allow synchronous reads, so buffer an uploaded
template first:

```csharp
using var templateStream = new MemoryStream();
await uploadedFile.CopyToAsync(templateStream, cancellationToken);
templateStream.Position = 0;

using var outputStream = new MemoryStream(); // a Word output must be readable, writable and seekable
ProcessingResult result = processor.ProcessTemplate(templateStream, outputStream, data);
```

See the [ASP.NET Core examples](https://github.com/TriasDev/templify/blob/main/TriasDev.Templify/Examples.md#web-application-integration).

## Data

### Nested Data and Objects
Expand Down
Loading