diff --git a/Directory.Build.props b/Directory.Build.props index 5403640d8..630b778da 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -70,7 +70,7 @@ net10.0 net8.0;net10.0 $(SamplesFrameworks);net472 - $(DefineConstants);FEATURE_ASYNC_SAX_XML + $(DefineConstants);TASKS_SUPPORTED diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReader.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReader.cs index b5f6abbba..2fd94e502 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReader.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReader.cs @@ -11,6 +11,9 @@ using System.IO; using System.Linq; using System.Xml; +#if TASKS_SUPPORTED +using System.Threading.Tasks; +#endif namespace DocumentFormat.OpenXml { @@ -100,7 +103,7 @@ public OpenXmlPartReader(Stream partStream, IFeatureCollection features, OpenXml _resolver = features.GetRequired(); _rootElements = features.GetRequired(); - _xmlReader = CreateReader(partStream, options.CloseStream, options.MaxCharactersInPart, ignoreWhitespace: options.IgnoreWhitespace, out _standalone, out _encoding); + _xmlReader = CreateReader(partStream, options, out _standalone, out _encoding); } /// @@ -667,17 +670,20 @@ public override void Close() _xmlReader.Close(); } - private XmlReader CreateReader(Stream partStream, bool closeInput, long maxCharactersInPart, bool ignoreWhitespace, out bool? standalone, out string? encoding) + private XmlReader CreateReader(Stream partStream, OpenXmlPartReaderOptions options, out bool? standalone, out string? encoding) { var settings = new XmlReaderSettings { - MaxCharactersInDocument = maxCharactersInPart, - CloseInput = closeInput, - IgnoreWhitespace = ignoreWhitespace, + MaxCharactersInDocument = options.MaxCharactersInPart, + CloseInput = options.CloseStream, + IgnoreWhitespace = options.IgnoreWhitespace, #if NET35 ProhibitDtd = true, #else DtdProcessing = DtdProcessing.Prohibit, +#endif +#if TASKS_SUPPORTED + Async = options.Async, #endif }; @@ -896,5 +902,292 @@ private void ThrowIfEof() throw new InvalidOperationException(ExceptionMessages.ReaderInEofState); } } + +#if TASKS_SUPPORTED + // Async Methods + + /// + public override async Task ReadAsync() + { + ThrowIfObjectDisposed(); + bool result = await MoveToNextElementAsync().ConfigureAwait(false); + + if (result && !ReadMiscNodes) + { + // skip miscellaneous node + while (result && IsMiscNode) + { + result = await MoveToNextElementAsync().ConfigureAwait(false); + } + } + + return result; + } + + /// + public override async Task ReadFirstChildAsync() + { + ThrowIfObjectDisposed(); + bool result = await MoveToFirstChildAsync().ConfigureAwait(false); + + if (result && !ReadMiscNodes) + { + // skip miscellaneous node + while (result && IsMiscNode) + { + result = await MoveToNextSiblingInternalAsync().ConfigureAwait(false); + } + } + + return result; + } + + /// + public override async Task ReadNextSiblingAsync() + { + ThrowIfObjectDisposed(); + bool result = await MoveToNextSiblingInternalAsync().ConfigureAwait(false); + + if (result && !ReadMiscNodes) + { + // skip miscellaneous node + while (result && IsMiscNode) + { + result = await MoveToNextSiblingInternalAsync().ConfigureAwait(false); + } + } + + return result; + } + + /// + public override async Task SkipAsync() + { + ThrowIfObjectDisposed(); + await InnerSkipAsync().ConfigureAwait(false); + + if (!EOF && !ReadMiscNodes) + { + // skip miscellaneous node + while (!EOF && IsMiscNode) + { + await InnerSkipAsync().ConfigureAwait(false); + } + } + } + + private async Task ReadRootAsync() + { + Debug.Assert(_elementState == ElementState.Null); + Debug.Assert(_elementStack.Count == 0); + + await _xmlReader.MoveToContentAsync().ConfigureAwait(false); + + while (!_xmlReader.EOF && _xmlReader.NodeType != XmlNodeType.Element) + { + await _xmlReader.SkipAsync().ConfigureAwait(false); + } + + if (_xmlReader.EOF || !_xmlReader.IsStartElement()) + { + throw new InvalidDataException(ExceptionMessages.PartIsEmpty); + } + + // create the root element object + var rootElement = CreateElement(new(_xmlReader.NamespaceURI, _xmlReader.LocalName)); + + if (rootElement is null) + { + throw new InvalidDataException(ExceptionMessages.PartUnknown); + } + + _elementStack.Push(rootElement); + + LoadAttributes(); + + if (_xmlReader.IsEmptyElement) + { + _elementState = ElementState.LeafStart; + rootElement.Load(_xmlReader, OpenXmlLoadMode.Full); + } + else + { + _elementState = ElementState.Start; + } + + return true; + } + + private async Task MoveToNextElementAsync() + { + switch (_elementState) + { + case ElementState.Null: + return await ReadRootAsync().ConfigureAwait(false); + + case ElementState.EOF: + return false; + + case ElementState.Start: + break; + + case ElementState.End: + case ElementState.MiscNode: + // cursor is end element, pop stack + _elementStack.Pop(); + if (_elementStack.Count == 0) + { + _elementState = ElementState.EOF; + return false; + } + + break; + + case ElementState.LeafStart: + // cursor is leaf element start + // just change the state to element end + // do not move the cursor + _elementState = ElementState.LeafEnd; + return true; + + case ElementState.LeafEnd: + case ElementState.LoadEnd: + // cursor is end element, pop stack + _elementStack.Pop(); + if (_elementStack.Count == 0) + { + _elementState = ElementState.EOF; + return false; + } + else + { + GetElementInformation(); + return true; + } + + default: + break; + } + + _elementState = ElementState.Null; + + if (_xmlReader.EOF || !await _xmlReader.ReadAsync().ConfigureAwait(false)) + { + _elementState = ElementState.EOF; + return false; + } + else + { + GetElementInformation(); + return true; + } + } + + private async Task MoveToFirstChildAsync() + { + switch (_elementState) + { + case ElementState.EOF: + return false; + + case ElementState.Start: + if (!await _xmlReader.ReadAsync().ConfigureAwait(false)) + { + // should be able to read. + Debug.Assert(false); + return false; + } + + GetElementInformation(); + if (_elementState == ElementState.End) + { + return false; + } + + return true; + + case ElementState.LeafStart: + _elementState = ElementState.LeafEnd; + return false; + + case ElementState.End: + case ElementState.LeafEnd: + case ElementState.LoadEnd: + case ElementState.MiscNode: + return false; + + case ElementState.Null: + ThrowIfNull(); + break; + + default: + break; + } + + return false; + } + + private async Task MoveToNextSiblingInternalAsync() + { + Debug.Assert(_xmlReader is not null); + + if (_elementState == ElementState.EOF) + { + return false; + } + + await InnerSkipAsync().ConfigureAwait(false); + + if (_elementState == ElementState.EOF) + { + return false; + } + else if (_elementState == ElementState.End) + { + return false; + } + else + { + return true; + } + } + + private async Task InnerSkipAsync() + { + switch (_elementState) + { + case ElementState.Null: + ThrowIfNull(); + break; + + case ElementState.EOF: + return; + + case ElementState.Start: + case ElementState.End: + case ElementState.MiscNode: + await _xmlReader.SkipAsync().ConfigureAwait(false); + _elementStack.Pop(); + GetElementInformation(); + return; + + case ElementState.LeafStart: + // no move, just process cursor + _elementStack.Pop(); + GetElementInformation(); + return; + + case ElementState.LeafEnd: + case ElementState.LoadEnd: + // cursor is leaf element, pop stack, no move + _elementStack.Pop(); + GetElementInformation(); + return; + + default: + break; + } + } +#endif } } diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReaderOptions.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReaderOptions.cs index 53c3c64f0..576b27955 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReaderOptions.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartReaderOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using DocumentFormat.OpenXml.Packaging; @@ -31,11 +31,21 @@ public struct OpenXmlPartReaderOptions /// public bool CloseStream { get; set; } +#if TASKS_SUPPORTED + /// + /// Gets or sets a value indicating whether asynchronous methods can be used. + /// + public bool Async { get; set; } +#endif + internal OpenXmlPartReaderOptions UpdateForPart(OpenXmlPart part) => new() { ReadMiscellaneousNodes = ReadMiscellaneousNodes, MaxCharactersInPart = MaxCharactersInPart != 0 ? MaxCharactersInPart : part.MaxCharactersInPart, IgnoreWhitespace = IgnoreWhitespace, CloseStream = true, +#if TASKS_SUPPORTED + Async = Async, +#endif }; } diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriter.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriter.cs index 7dbb47209..141e41106 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriter.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriter.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED using DocumentFormat.OpenXml.Framework; using System.Threading.Tasks; #endif @@ -82,7 +82,7 @@ public OpenXmlPartWriter(OpenXmlPart openXmlPart, OpenXmlPartWriterSettings sett { CloseOutput = true, Encoding = settings.Encoding, -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED Async = settings.Async, #endif }; @@ -146,7 +146,7 @@ public OpenXmlPartWriter(Stream partStream, OpenXmlPartWriterSettings settings) { CloseOutput = settings.CloseOutput, Encoding = settings.Encoding, -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED Async = settings.Async, #endif }; @@ -430,7 +430,7 @@ public override void Close() #endregion // Async Methods -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED /// /// Asynchronously writes the XML declaration with the version "1.0". /// diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriterSettings.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriterSettings.cs index 88e076308..c955f69c8 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriterSettings.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlPartWriterSettings.cs @@ -10,7 +10,7 @@ namespace DocumentFormat.OpenXml; /// public class OpenXmlPartWriterSettings { -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED /// /// Gets or sets a value indicating whether asynchronous methods can be used. /// diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlReader.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlReader.cs index 5cf655c24..23c9ec83d 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlReader.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlReader.cs @@ -7,6 +7,9 @@ using System.Collections.ObjectModel; using System.IO; using System.Xml; +#if TASKS_SUPPORTED +using System.Threading.Tasks; +#endif namespace DocumentFormat.OpenXml { @@ -235,6 +238,46 @@ public virtual bool HasAttributes /// public abstract void Close(); +#if TASKS_SUPPORTED + /// + /// Asynchronously moves to read the next element. + /// + /// Returns true if the next element was read successfully; false if there are no more elements to read. + public virtual Task ReadAsync() + { + return Task.FromResult(Read()); + } + + /// + /// Asynchronously moves to read the first child element. + /// + /// Returns true if the first child element was read successfully; false if there are no child elements to read. + /// This method can only be called on element start. At the current node, the reader will move to the end tag if there is no child element. + public virtual Task ReadFirstChildAsync() + { + return Task.FromResult(ReadFirstChild()); + } + + /// + /// Asynchronously moves to read the next sibling element. + /// + /// Returns true if the next sibling element was read successfully; false if there are no more sibling elements to read. + /// At the current node, the reader will move to the end tag of the parent node if there are no more sibling elements. + public virtual Task ReadNextSiblingAsync() + { + return Task.FromResult(ReadNextSibling()); + } + + /// + /// Asynchronously skips the child elements of the current node. + /// + public virtual Task SkipAsync() + { + Skip(); + return Task.CompletedTask; + } +#endif + /// /// Thrown if the object is disposed. /// diff --git a/src/DocumentFormat.OpenXml.Framework/OpenXmlWriter.cs b/src/DocumentFormat.OpenXml.Framework/OpenXmlWriter.cs index 24fa6af83..00c58299c 100644 --- a/src/DocumentFormat.OpenXml.Framework/OpenXmlWriter.cs +++ b/src/DocumentFormat.OpenXml.Framework/OpenXmlWriter.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED using System.Threading.Tasks; #endif @@ -131,7 +131,7 @@ protected OpenXmlWriter() /// public abstract void Close(); -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED /// /// Asynchronously writes the XML declaration with the version "1.0". /// diff --git a/src/DocumentFormat.OpenXml.Framework/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net10.0/PublicAPI.Unshipped.txt index e2eebeccc..362fd2601 100644 --- a/src/DocumentFormat.OpenXml.Framework/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,2 +1,12 @@ #nullable enable +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.SkipAsync() -> System.Threading.Tasks.Task! +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.get -> bool +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.set -> void +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.SkipAsync() -> System.Threading.Tasks.Task! diff --git a/src/DocumentFormat.OpenXml.Framework/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net6.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..362fd2601 --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable + +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.SkipAsync() -> System.Threading.Tasks.Task! +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.get -> bool +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.set -> void +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.SkipAsync() -> System.Threading.Tasks.Task! diff --git a/src/DocumentFormat.OpenXml.Framework/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net8.0/PublicAPI.Unshipped.txt index e2eebeccc..362fd2601 100644 --- a/src/DocumentFormat.OpenXml.Framework/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/DocumentFormat.OpenXml.Framework/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,12 @@ #nullable enable +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.SkipAsync() -> System.Threading.Tasks.Task! +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.get -> bool +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.set -> void +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.SkipAsync() -> System.Threading.Tasks.Task! diff --git a/src/DocumentFormat.OpenXml.Framework/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/DocumentFormat.OpenXml.Framework/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..362fd2601 --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,12 @@ +#nullable enable + +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +virtual DocumentFormat.OpenXml.OpenXmlReader.SkipAsync() -> System.Threading.Tasks.Task! +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.get -> bool +DocumentFormat.OpenXml.OpenXmlPartReaderOptions.Async.set -> void +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadFirstChildAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.ReadNextSiblingAsync() -> System.Threading.Tasks.Task! +override DocumentFormat.OpenXml.OpenXmlPartReader.SkipAsync() -> System.Threading.Tasks.Task! diff --git a/src/DocumentFormat.OpenXml.Framework/XmlConvertingReader.cs b/src/DocumentFormat.OpenXml.Framework/XmlConvertingReader.cs index 76b3bd350..d71bd6ac9 100644 --- a/src/DocumentFormat.OpenXml.Framework/XmlConvertingReader.cs +++ b/src/DocumentFormat.OpenXml.Framework/XmlConvertingReader.cs @@ -5,6 +5,9 @@ using DocumentFormat.OpenXml.Framework; using System; using System.Xml; +#if TASKS_SUPPORTED +using System.Threading.Tasks; +#endif namespace DocumentFormat.OpenXml { @@ -188,5 +191,16 @@ private string ApplyStrictTranslation(OpenXmlNamespace ns) return ns.Uri; } + +#if TASKS_SUPPORTED + /// + public override Task ReadAsync() => BaseReader.ReadAsync(); + + /// + public override Task SkipAsync() => BaseReader.SkipAsync(); + + /// + public override Task MoveToContentAsync() => BaseReader.MoveToContentAsync(); +#endif } } diff --git a/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlReaderAsyncTest.cs b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlReaderAsyncTest.cs new file mode 100644 index 000000000..3cb338bca --- /dev/null +++ b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlReaderAsyncTest.cs @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace DocumentFormat.OpenXml.Tests +{ + /// + /// Tests for async methods on and . + /// + public class OpenXmlReaderAsyncTest + { +#if TASKS_SUPPORTED + [Fact] + public async Task ReadAsync_ShouldNavigateToRootElement() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph(new Run(new Text("Hello"))))); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + bool result = await reader.ReadAsync(); + + // Assert - should be at the Document element + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Document), reader.ElementType); + } + } + } + + [Fact] + public async Task ReadAsync_ShouldReturnFalse_WhenNoMoreElements() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body()); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Read through all elements + while (await reader.ReadAsync()) + { + } + + // Assert + Assert.True(reader.EOF); + } + } + } + + [Fact] + public async Task ReadFirstChildAsync_ShouldMoveToFirstChild() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph())); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Move to Document element + await reader.ReadAsync(); + + // Move to first child (Body) + bool result = await reader.ReadFirstChildAsync(); + + // Assert + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Body), reader.ElementType); + } + } + } + + [Fact] + public async Task ReadFirstChildAsync_ShouldReturnFalse_WhenNoChildren() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body()); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Move to Document element + await reader.ReadAsync(); + + // Move to Body + await reader.ReadFirstChildAsync(); + + // Try to move to first child of Body (should fail - empty Body) + bool result = await reader.ReadFirstChildAsync(); + + // Assert + Assert.False(result); + Assert.True(reader.IsEndElement); + } + } + } + + [Fact] + public async Task ReadNextSiblingAsync_ShouldMoveToNextSibling() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph(), new Paragraph())); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Move to Document -> Body -> first Paragraph + await reader.ReadAsync(); + await reader.ReadFirstChildAsync(); + await reader.ReadFirstChildAsync(); + + Assert.Equal(typeof(Paragraph), reader.ElementType); + + // Move to second Paragraph (next sibling) + bool result = await reader.ReadNextSiblingAsync(); + + // Assert + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + } + + [Fact] + public async Task ReadNextSiblingAsync_ShouldReturnFalse_WhenNoMoreSiblings() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph())); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Move to Document -> Body -> Paragraph + await reader.ReadAsync(); + await reader.ReadFirstChildAsync(); + await reader.ReadFirstChildAsync(); + + Assert.Equal(typeof(Paragraph), reader.ElementType); + + // Try to move to next sibling (should be the Body end element) + bool result = await reader.ReadNextSiblingAsync(); + + // Assert - no more siblings, moved to end element of parent + Assert.False(result); + Assert.True(reader.IsEndElement); + } + } + } + + [Fact] + public async Task SkipAsync_ShouldSkipCurrentElementChildren() + { + // Arrange + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph(new Run(new Text("Hello"))), new Paragraph())); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + // Move to Document -> Body + await reader.ReadAsync(); + await reader.ReadFirstChildAsync(); + + Assert.Equal(typeof(Body), reader.ElementType); + + // Move to first Paragraph + await reader.ReadFirstChildAsync(); + Assert.Equal(typeof(Paragraph), reader.ElementType); + + // Skip the first Paragraph (and its children) + await reader.SkipAsync(); + + // Should now be at the second Paragraph + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + } + + [Fact] + public async Task ReadAsync_DomReader_ShouldUseBaseVirtualImplementation() + { + // Arrange - OpenXmlDomReader uses the base virtual implementation + string paragraphOuterXml = "Run Text."; + Paragraph para = new Paragraph(paragraphOuterXml); + + // Act + using (OpenXmlReader reader = OpenXmlReader.Create(para)) + { + bool result = await reader.ReadAsync(); + + // Assert + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + + [Fact] + public async Task ReadFirstChildAsync_DomReader_ShouldUseBaseVirtualImplementation() + { + // Arrange - OpenXmlDomReader uses the base virtual implementation + string bodyOuterXml = ""; + Body body = new Body(bodyOuterXml); + + // Act + using (OpenXmlReader reader = OpenXmlReader.Create(body)) + { + await reader.ReadAsync(); // move to Body + + bool result = await reader.ReadFirstChildAsync(); // move to Paragraph + + // Assert + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + + [Fact] + public async Task ReadNextSiblingAsync_DomReader_ShouldUseBaseVirtualImplementation() + { + // Arrange - OpenXmlDomReader uses the base virtual implementation + Body body = new Body(new Paragraph(), new Paragraph()); + + // Act + using (OpenXmlReader reader = OpenXmlReader.Create(body)) + { + await reader.ReadAsync(); // move to Body + await reader.ReadFirstChildAsync(); // move to first Paragraph + + bool result = await reader.ReadNextSiblingAsync(); // move to second Paragraph + + // Assert + Assert.True(result); + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + + [Fact] + public async Task SkipAsync_DomReader_ShouldUseBaseVirtualImplementation() + { + // Arrange - OpenXmlDomReader uses the base virtual implementation + Body body = new Body(new Paragraph(new Run(new Text("Hello"))), new Paragraph()); + + // Act + using (OpenXmlReader reader = OpenXmlReader.Create(body)) + { + await reader.ReadAsync(); // move to Body + await reader.ReadFirstChildAsync(); // move to first Paragraph + + await reader.SkipAsync(); // skip first Paragraph + + // Should now be at second Paragraph + Assert.True(reader.IsStartElement); + Assert.Equal(typeof(Paragraph), reader.ElementType); + } + } + + [Fact] + public async Task ReadAsync_PartReader_ShouldReadAllElements() + { + // Arrange - full traversal of a document + using (MemoryStream memoryStream = new MemoryStream()) + using (WordprocessingDocument wpd = WordprocessingDocument.Create(memoryStream, WordprocessingDocumentType.Document)) + { + MainDocumentPart mdp = wpd.AddMainDocumentPart(); + mdp.Document = new Document(new Body(new Paragraph(new Run(new Text("Hello World"))))); + mdp.Document.Save(); + wpd.Save(); + + // Act + using (OpenXmlPartReader reader = new OpenXmlPartReader(mdp, new OpenXmlPartReaderOptions { Async = true })) + { + int elementCount = 0; + while (await reader.ReadAsync()) + { + if (reader.IsStartElement) + { + elementCount++; + } + } + + // Assert: Document, Body, Paragraph, Run, Text = 5 start elements + Assert.Equal(5, elementCount); + } + } + } + + [Fact] + public async Task OpenXmlPartReaderOptions_Async_DefaultsFalse() + { + // Arrange + OpenXmlPartReaderOptions options = default; + + // Assert + Assert.False(options.Async); + } +#endif + } +} diff --git a/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlWriterTest.cs b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlWriterTest.cs index 60d4f83e1..4a3b929a7 100644 --- a/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlWriterTest.cs +++ b/test/DocumentFormat.OpenXml.Tests/ofapiTest/OpenXmlWriterTest.cs @@ -239,7 +239,7 @@ public void WriteStringExceptionTest7() } } -#if FEATURE_ASYNC_SAX_XML +#if TASKS_SUPPORTED [Fact] public async Task WriteStartDocumentAsync_ShouldWriteStartDocument() {