diff --git a/src/Microsoft.OData.Core/Batch/ODataBatchUtils.cs b/src/Microsoft.OData.Core/Batch/ODataBatchUtils.cs
index b6061b568a..63e650c8d3 100644
--- a/src/Microsoft.OData.Core/Batch/ODataBatchUtils.cs
+++ b/src/Microsoft.OData.Core/Batch/ODataBatchUtils.cs
@@ -110,12 +110,13 @@ internal static ODataReadStream CreateBatchOperationReadStream(
/// A new instance.
internal static ODataWriteStream CreateBatchOperationWriteStream(
Stream outputStream,
- IODataStreamListener operationListener)
+ IODataStreamListener operationListener,
+ bool synchronous = true)
{
Debug.Assert(outputStream != null, "outputStream != null");
Debug.Assert(operationListener != null, "operationListener != null");
- return new ODataWriteStream(outputStream, operationListener);
+ return new ODataWriteStream(outputStream, operationListener, synchronous);
}
///
diff --git a/src/Microsoft.OData.Core/Batch/ODataBatchWriter.cs b/src/Microsoft.OData.Core/Batch/ODataBatchWriter.cs
index 711b5fc037..aa27b05c3d 100644
--- a/src/Microsoft.OData.Core/Batch/ODataBatchWriter.cs
+++ b/src/Microsoft.OData.Core/Batch/ODataBatchWriter.cs
@@ -56,7 +56,7 @@ public abstract class ODataBatchWriter : IODataStreamListener, IODataOutputInStr
///
/// Whether the writer is currently processing inside a changeset or atomic group.
///
- private bool isInChangset;
+ private bool isInChangeset;
/// The batch-specific URL converter that stores the content IDs found in a changeset and supports resolving cross-referencing URLs.
internal readonly ODataBatchPayloadUriConverter payloadUriConverter;
@@ -167,10 +167,11 @@ public void WriteStartBatch()
/// Asynchronously starts a new batch; can be only called once and as first call.
/// A task instance that represents the asynchronous write operation.
- public Task WriteStartBatchAsync()
+ public async Task WriteStartBatchAsync()
{
this.VerifyCanWriteStartBatch(false);
- return TaskUtils.GetTaskForSynchronousOperation(this.WriteStartBatchImplementation);
+ await this.WriteStartBatchImplementationAsync()
+ .ConfigureAwait(false);
}
/// Ends a batch; can only be called after WriteStartBatch has been called and if no other active changeset or operation exist.
@@ -185,13 +186,13 @@ public void WriteEndBatch()
/// Asynchronously ends a batch; can only be called after WriteStartBatch has been called and if no other active change set or operation exist.
/// A task instance that represents the asynchronous write operation.
- public Task WriteEndBatchAsync()
+ public async Task WriteEndBatchAsync()
{
this.VerifyCanWriteEndBatch(false);
- return TaskUtils.GetTaskForSynchronousOperation(this.WriteEndBatchImplementation)
-
+ await this.WriteEndBatchImplementationAsync()
// Note that we intentionally go through the public API so that if the Flush fails the writer moves to the Error state.
- .FollowOnSuccessWithTask(task => this.FlushAsync());
+ .FollowOnSuccessWithTask(task => this.FlushAsync())
+ .ConfigureAwait(false);
}
///
@@ -231,13 +232,14 @@ public Task WriteStartChangesetAsync()
/// The change set Id of the batch request. Cannot be null.
/// A task instance that represents the asynchronous write operation.
/// Thrown if the is null.
- public Task WriteStartChangesetAsync(string changesetId)
+ public async Task WriteStartChangesetAsync(string changesetId)
{
ExceptionUtils.CheckArgumentNotNull(changesetId, "changesetId");
this.VerifyCanWriteStartChangeset(false);
- return TaskUtils.GetTaskForSynchronousOperation(() => this.WriteStartChangesetImplementation(changesetId))
- .FollowOnSuccessWith(t => this.FinishWriteStartChangeset());
+ await this.WriteStartChangesetImplementationAsync(changesetId)
+ .FollowOnSuccessWith(t => this.FinishWriteStartChangeset())
+ .ConfigureAwait(false);
}
/// Ends an active changeset; this can only be called after WriteStartChangeset and only once for each changeset.
@@ -250,11 +252,12 @@ public void WriteEndChangeset()
/// Asynchronously ends an active change set; this can only be called after WriteStartChangeset and only once for each change set.
/// A task instance that represents the asynchronous write operation.
- public Task WriteEndChangesetAsync()
+ public async Task WriteEndChangesetAsync()
{
this.VerifyCanWriteEndChangeset(false);
- return TaskUtils.GetTaskForSynchronousOperation(this.WriteEndChangesetImplementation)
- .FollowOnSuccessWith(t => this.FinishWriteEndChangeset());
+ await this.WriteEndChangesetImplementationAsync()
+ .FollowOnSuccessWith(t => this.FinishWriteEndChangeset())
+ .ConfigureAwait(false);
}
/// Creates an for writing an operation of a batch request.
@@ -336,13 +339,12 @@ public Task CreateOperationRequestMessageAsyn
/// The format of operation Request-URI, which could be AbsoluteUri, AbsoluteResourcePathAndHost, or RelativeResourcePath.
/// The prerequisite request ids of this request.
/// A task that when completed returns the newly created operation request message.
- public Task CreateOperationRequestMessageAsync(string method, Uri uri, string contentId,
+ public async Task CreateOperationRequestMessageAsync(string method, Uri uri, string contentId,
BatchPayloadUriOption payloadUriOption, IList dependsOnIds)
{
this.VerifyCanCreateOperationRequestMessage(false, method, uri, contentId);
-
- return TaskUtils.GetTaskForSynchronousOperation(() =>
- CreateOperationRequestMessageInternal(method, uri, contentId, payloadUriOption, dependsOnIds));
+ return await this.CreateOperationRequestMessageInternalAsync(
+ method, uri, contentId, payloadUriOption, dependsOnIds).ConfigureAwait(false);
}
/// Creates a message for writing an operation of a batch response.
@@ -357,11 +359,11 @@ public ODataBatchOperationResponseMessage CreateOperationResponseMessage(string
/// Asynchronously creates an for writing an operation of a batch response.
/// The Content-ID value to write in ChangeSet head.
/// A task that when completed returns the newly created operation response message.
- public Task CreateOperationResponseMessageAsync(string contentId)
+ public async Task CreateOperationResponseMessageAsync(string contentId)
{
this.VerifyCanCreateOperationResponseMessage(false);
- return TaskUtils.GetTaskForSynchronousOperation(
- () => this.CreateOperationResponseMessageImplementation(contentId));
+ return await this.CreateOperationResponseMessageImplementationAsync(contentId)
+ .ConfigureAwait(false);
}
/// Flushes the write buffer to the underlying stream.
@@ -383,12 +385,14 @@ public void Flush()
/// Flushes the write buffer to the underlying stream asynchronously.
/// A task instance that represents the asynchronous operation.
- public Task FlushAsync()
+ public async Task FlushAsync()
{
this.VerifyCanFlush(false);
// Make sure we switch to writer state Error if an exception is thrown during flushing.
- return this.FlushAsynchronously().FollowOnFaultWith(t => this.SetState(BatchWriterState.Error));
+ await this.FlushAsynchronously()
+ .FollowOnFaultWith(t => this.SetState(BatchWriterState.Error))
+ .ConfigureAwait(false);
}
///
@@ -553,7 +557,8 @@ protected ODataBatchOperationRequestMessage BuildOperationRequestMessage(Stream
ODataBatchUtils.ValidateReferenceUri(uri, requestIdsForUrlReferenceValidation, this.outputContext.MessageWriterSettings.BaseUri);
- Func streamCreatorFunc = () => ODataBatchUtils.CreateBatchOperationWriteStream(outputStream, this);
+ Func streamCreatorFunc = () => ODataBatchUtils.CreateBatchOperationWriteStream(
+ outputStream, this, this.outputContext.Synchronous);
ODataBatchOperationRequestMessage requestMessage =
new ODataBatchOperationRequestMessage(streamCreatorFunc, method, uri, /*headers*/ null, this, contentId,
this.payloadUriConverter, /*writing*/ true, this.container, dependsOnIds, groupId);
@@ -573,11 +578,91 @@ protected ODataBatchOperationRequestMessage BuildOperationRequestMessage(Stream
protected ODataBatchOperationResponseMessage BuildOperationResponseMessage(Stream outputStream,
string contentId, string groupId)
{
- Func streamCreatorFunc = () => ODataBatchUtils.CreateBatchOperationWriteStream(outputStream, this);
+ Func streamCreatorFunc = () => ODataBatchUtils.CreateBatchOperationWriteStream(
+ outputStream, this, this.outputContext.Synchronous);
return new ODataBatchOperationResponseMessage(streamCreatorFunc, /*headers*/ null, this, contentId,
this.payloadUriConverter.BatchMessagePayloadUriConverter, /*writing*/ true, this.container, groupId);
}
+ ///
+ /// Asnchronously starts a new batch.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected virtual Task WriteStartBatchImplementationAsync()
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(this.WriteStartBatchImplementation);
+ }
+
+ ///
+ /// Asynchronously ends a batch.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected virtual Task WriteEndBatchImplementationAsync()
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(this.WriteEndBatchImplementation);
+ }
+
+ ///
+ /// Asynchronously starts a new changeset.
+ ///
+ /// The atomic group id, aka changeset GUID of the batch request.
+ /// A task that represents the asynchronous write operation.
+ protected virtual Task WriteStartChangesetImplementationAsync(string groupOrChangesetId)
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(
+ () => this.WriteStartChangesetImplementation(groupOrChangesetId));
+ }
+
+ ///
+ /// Asynchronously ends an active changeset.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected virtual Task WriteEndChangesetImplementationAsync()
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(this.WriteEndChangesetImplementation);
+ }
+
+ ///
+ /// Asynchronously creates an for writing an operation of a batch request.
+ ///
+ /// The Http method to be used for this request operation.
+ /// The Uri to be used for this request operation.
+ /// The Content-ID value to write in ChangeSet head.
+ ///
+ /// The format of operation Request-URI, which could be AbsoluteUri, AbsoluteResourcePathAndHost, or RelativeResourcePath.
+ /// The prerequisite request ids of this request.
+ /// A task that represents the asynchronous operation.
+ /// The value of the TResult parameter contains an
+ /// that can be used to write the request operation.
+ protected virtual Task CreateOperationRequestMessageImplementationAsync(
+ string method,
+ Uri uri,
+ string contentId,
+ BatchPayloadUriOption payloadUriOption,
+ IEnumerable dependsOnIds)
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(
+ () => this.CreateOperationRequestMessageImplementation(
+ method,
+ uri,
+ contentId,
+ payloadUriOption,
+ dependsOnIds));
+ }
+
+ ///
+ /// Asynchronously creates an for writing an operation of a batch response.
+ ///
+ /// The Content-ID value to write in ChangeSet head.
+ /// A task that represents the asynchronous operation.
+ /// The value of the TResult parameter contains an
+ /// that can be used to write the response operation.
+ protected virtual Task CreateOperationResponseMessageImplementationAsync(string contentId)
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(
+ () => this.CreateOperationResponseMessageImplementation(contentId));
+ }
+
///
/// Catch any exception thrown by the action passed in; in the exception case move the writer into
/// state ExceptionThrown and then re-throw the exception.
@@ -628,7 +713,7 @@ private void ThrowODataException(string errorMessage)
private ODataBatchOperationRequestMessage CreateOperationRequestMessageInternal(string method, Uri uri, string contentId,
BatchPayloadUriOption payloadUriOption, IEnumerable dependsOnIds)
{
- if (!this.isInChangset)
+ if (!this.isInChangeset)
{
this.InterceptException(this.IncreaseBatchSize);
}
@@ -653,7 +738,65 @@ private ODataBatchOperationRequestMessage CreateOperationRequestMessageInternal(
this.CurrentOperationRequestMessage = this.CreateOperationRequestMessageImplementation(
method, uri, contentId, payloadUriOption, dependsOnIds);
- if (this.isInChangset || this.outputContext.MessageWriterSettings.Version > ODataVersion.V4)
+ if (this.isInChangeset || this.outputContext.MessageWriterSettings.Version > ODataVersion.V4)
+ {
+ // The content Id can be generated if the value passed in is null, therefore here we get the real value of the content Id.
+ this.RememberContentIdHeader(this.CurrentOperationRequestMessage.ContentId);
+ }
+
+ return this.CurrentOperationRequestMessage;
+ }
+
+ ///
+ /// Internal method to create an for writing
+ /// an operation of a batch request.
+ ///
+ /// The Http method to be used for this request operation.
+ /// The Uri to be used for this request operation.
+ ///
+ /// For batch in multipart format, the Content-ID value to write in ChangeSet header, would be ignored if
+ /// is "GET".
+ /// For batch in Json format, if the value passed in is null, an GUID will be generated and used as the request id.
+ ///
+ ///
+ /// The format of operation Request-URI, which could be AbsoluteUri, AbsoluteResourcePathAndHost, or RelativeResourcePath.
+ /// The prerequisite request ids of this request.
+ /// A task that represents the asynchronous operation.
+ /// The value of the TResult parameter contains an
+ /// that can be used to write the request operation.
+ private async Task CreateOperationRequestMessageInternalAsync(
+ string method,
+ Uri uri,
+ string contentId,
+ BatchPayloadUriOption payloadUriOption,
+ IEnumerable dependsOnIds)
+ {
+ if (!this.isInChangeset)
+ {
+ this.InterceptException(this.IncreaseBatchSize);
+ }
+ else
+ {
+ this.InterceptException(this.IncreaseChangeSetSize);
+ }
+
+ // Add a potential Content-ID header to the URL resolver so that it will be available
+ // to subsequent operations.
+ // Note that what we add here is the Content-ID header of the previous operation (if any).
+ // This also means that the Content-ID of the last operation in a changeset will never get
+ // added to the cache which is fine since we cannot reference it anywhere.
+ if (this.currentOperationContentId != null)
+ {
+ this.payloadUriConverter.AddContentId(this.currentOperationContentId);
+ }
+
+ this.InterceptException(() =>
+ uri = ODataBatchUtils.CreateOperationRequestUri(uri, this.outputContext.MessageWriterSettings.BaseUri, this.payloadUriConverter));
+
+ this.CurrentOperationRequestMessage = await this.CreateOperationRequestMessageImplementationAsync(
+ method, uri, contentId, payloadUriOption, dependsOnIds).ConfigureAwait(false);
+
+ if (this.isInChangeset || this.outputContext.MessageWriterSettings.Version > ODataVersion.V4)
{
// The content Id can be generated if the value passed in is null, therefore here we get the real value of the content Id.
this.RememberContentIdHeader(this.CurrentOperationRequestMessage.ContentId);
@@ -680,7 +823,7 @@ private void FinishWriteStartChangeset()
// reset the size of the current changeset and increase the size of the batch.
this.ResetChangeSetSize();
this.InterceptException(this.IncreaseBatchSize);
- this.isInChangset = true;
+ this.isInChangeset = true;
}
///
@@ -695,7 +838,7 @@ private void FinishWriteEndChangeset()
this.currentOperationContentId = null;
}
- this.isInChangset = false;
+ this.isInChangeset = false;
}
///
@@ -799,7 +942,7 @@ private void VerifyCanCreateOperationRequestMessage(bool synchronousCall, string
ExceptionUtils.CheckArgumentNotNull(uri, "uri");
// For the case within a changeset, verify CreateOperationRequestMessage is valid.
- if (this.isInChangset)
+ if (this.isInChangeset)
{
if (HttpUtils.IsQueryMethod(method))
{
@@ -933,7 +1076,7 @@ private void ValidateTransition(BatchWriterState newState)
{
case BatchWriterState.ChangesetStarted:
// make sure that we are not starting a changeset when one is already active
- if (this.isInChangset)
+ if (this.isInChangeset)
{
throw new ODataException(Strings.ODataBatchWriter_CannotStartChangeSetWithActiveChangeSet);
}
@@ -941,7 +1084,7 @@ private void ValidateTransition(BatchWriterState newState)
break;
case BatchWriterState.ChangesetCompleted:
// make sure that we are not completing a changeset without an active changeset
- if (!this.isInChangset)
+ if (!this.isInChangeset)
{
throw new ODataException(Strings.ODataBatchWriter_CannotCompleteChangeSetWithoutActiveChangeSet);
}
@@ -949,7 +1092,7 @@ private void ValidateTransition(BatchWriterState newState)
break;
case BatchWriterState.BatchCompleted:
// make sure that we are not completing a batch while a changeset is still active
- if (this.isInChangset)
+ if (this.isInChangeset)
{
throw new ODataException(Strings.ODataBatchWriter_CannotCompleteBatchWithActiveChangeSet);
}
diff --git a/src/Microsoft.OData.Core/GlobalSuppressions.cs b/src/Microsoft.OData.Core/GlobalSuppressions.cs
index 94f4249174..0f925cff90 100644
--- a/src/Microsoft.OData.Core/GlobalSuppressions.cs
+++ b/src/Microsoft.OData.Core/GlobalSuppressions.cs
@@ -18,6 +18,10 @@
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.UriParser.FunctionCallBinder.#BindAsUriFunction(Microsoft.OData.UriParser.FunctionCallToken,System.Collections.Generic.List`1)")]
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.UriParser.SelectExpandOptionParser.#BuildStarExpandTermToken(Microsoft.OData.UriParser.PathSegmentToken)")]
[module: SuppressMessage("Microsoft.Naming", "CA1701:ResourceStringCompoundWordsShouldBeCasedCorrectly", MessageId = "NonEntity", Scope = "resource", Target = "Microsoft.OData.Core.resources")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.JsonLight.ODataJsonLightBatchWriter.#WritePendingRequestMessageData")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.JsonLight.ODataJsonLightBatchWriter.#WritePendingResponseMessageData")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.JsonLight.ODataJsonLightBatchWriter.#WritePendingRequestMessageDataAsync")]
+[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Scope = "member", Target = "Microsoft.OData.JsonLight.ODataJsonLightBatchWriter.#WritePendingResponseMessageDataAsync")]
// By design.
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Scope = "member", Target = "Microsoft.OData.Evaluation.ODataResourceMetadataContext+ODataResourceMetadataContextWithoutModel.#serializationInfo")]
diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchWriter.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchWriter.cs
index 37db15118d..51669f7479 100644
--- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchWriter.cs
+++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchWriter.cs
@@ -83,6 +83,11 @@ internal sealed class ODataJsonLightBatchWriter : ODataBatchWriter
///
private readonly IJsonWriter jsonWriter;
+ ///
+ /// The underlying asynchronous JSON writer.
+ ///
+ private readonly IJsonWriterAsync asynchronousJsonWriter;
+
///
/// The auto-generated GUID for AtomicityGroup of the Json item. Should be null for Json item
/// that doesn't belong to atomic group.
@@ -109,6 +114,7 @@ internal ODataJsonLightBatchWriter(ODataJsonLightOutputContext jsonLightOutputCo
: base(jsonLightOutputContext)
{
this.jsonWriter = this.JsonLightOutputContext.JsonWriter;
+ this.asynchronousJsonWriter = this.JsonLightOutputContext.AsynchronousJsonWriter;
}
///
@@ -167,15 +173,17 @@ public override void StreamRequested()
/// A task representing any action that is running as part of the status change of the operation;
/// null if no such action exists.
///
- public override Task StreamRequestedAsync()
+ public override async Task StreamRequestedAsync()
{
// Write any pending data and flush the batch writer to the async buffered stream
- this.StartBatchOperationContent();
+ await this.StartBatchOperationContentAsync()
+ .ConfigureAwait(false);
// Asynchronously flush the async buffered stream to the underlying message stream (if there's any);
// then dispose the batch writer (since we are now writing the operation content) and set the corresponding state.
- return this.JsonLightOutputContext.FlushBuffersAsync()
- .FollowOnSuccessWith(task => this.SetState(BatchWriterState.OperationStreamRequested));
+ await this.JsonLightOutputContext.FlushBuffersAsync()
+ .FollowOnSuccessWith(task => this.SetState(BatchWriterState.OperationStreamRequested))
+ .ConfigureAwait(false);
}
///
@@ -192,6 +200,21 @@ public override void StreamDisposed()
this.EnsurePrecedingMessageIsClosed();
}
+ ///
+ /// This method is called to notify that the content stream of a batch operation has been disposed.
+ ///
+ public override async Task StreamDisposedAsync()
+ {
+ Debug.Assert(this.CurrentOperationMessage != null, "Expected non-null operation message!");
+
+ this.SetState(BatchWriterState.OperationStreamDisposed);
+ this.CurrentOperationRequestMessage = null;
+ this.CurrentOperationResponseMessage = null;
+
+ await this.EnsurePrecedingMessageIsClosedAsync()
+ .ConfigureAwait(false);
+ }
+
///
/// This method notifies the listener, that an in-stream error is to be written.
///
@@ -203,9 +226,22 @@ public override void OnInStreamError()
{
this.JsonLightOutputContext.VerifyNotDisposed();
this.SetState(BatchWriterState.Error);
- this.JsonLightOutputContext.JsonWriter.Flush();
+ this.jsonWriter.Flush();
- // The OData protocol spec did not defined the behavior when an exception is encountered outside of a batch operation. The batch writer
+ // The OData protocol spec does not define the behavior when an exception is encountered outside of a batch operation. The batch writer
+ // should not allow WriteError in this case. Note that WCF DS Server does serialize the error in XML format when it encounters one outside of a
+ // batch operation.
+ throw new ODataException(Strings.ODataBatchWriter_CannotWriteInStreamErrorForBatch);
+ }
+
+ public override async Task OnInStreamErrorAsync()
+ {
+ this.JsonLightOutputContext.VerifyNotDisposed();
+ this.SetState(BatchWriterState.Error);
+ await this.asynchronousJsonWriter.FlushAsync()
+ .ConfigureAwait(false);
+
+ // The OData protocol spec does not define the behavior when an exception is encountered outside of a batch operation. The batch writer
// should not allow WriteError in this case. Note that WCF DS Server does serialize the error in XML format when it encounters one outside of a
// batch operation.
throw new ODataException(Strings.ODataBatchWriter_CannotWriteInStreamErrorForBatch);
@@ -268,7 +304,7 @@ protected override IEnumerable GetDependsOnRequestIds(IEnumerableThe dependsOn ids specifying current request's prerequisites.
protected override void ValidateDependsOnIds(string contentId, IEnumerable dependsOnIds)
{
- foreach (var id in dependsOnIds)
+ foreach (var id in dependsOnIds)
{
// Content-ID cannot be part of dependsOnIds. This is to avoid self referencing.
// The dependsOnId must be an existing request ID.
@@ -431,6 +467,189 @@ protected override void VerifyNotDisposed()
this.JsonLightOutputContext.VerifyNotDisposed();
}
+ ///
+ /// Asynchronously starts a new batch.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected override async Task WriteStartBatchImplementationAsync()
+ {
+ await WriteBatchEnvelopeAsync()
+ .ConfigureAwait(false);
+ this.SetState(BatchWriterState.BatchStarted);
+ }
+
+ ///
+ /// Asynchronously ends a batch.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected override async Task WriteEndBatchImplementationAsync()
+ {
+ // write pending message data (headers, response line) for a previously unclosed message/request
+ await this.WritePendingMessageDataAsync(true)
+ .ConfigureAwait(false);
+
+ this.SetState(BatchWriterState.BatchCompleted);
+
+ // Close the messages array
+ await asynchronousJsonWriter.EndArrayScopeAsync()
+ .ConfigureAwait(false);
+
+ // Close the top level scope
+ await asynchronousJsonWriter.EndObjectScopeAsync()
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously starts a new changeset.
+ ///
+ /// The atomic group id of the changeset to start.
+ /// If it is null for Json batch, an GUID will be generated and used as the atomic group id.
+ /// A task that represents the asynchronous write operation.
+ protected override async Task WriteStartChangesetImplementationAsync(string groupId)
+ {
+ Debug.Assert(groupId != null, "groupId != null");
+
+ // write pending message data (headers, response line) for a previously unclosed message/request
+ await this.WritePendingMessageDataAsync(true)
+ .ConfigureAwait(false);
+
+ // important to do this first since it will set up the change set boundary.
+ this.SetState(BatchWriterState.ChangesetStarted);
+
+ this.atomicityGroupId = groupId;
+ }
+
+ ///
+ /// Asynchronously ends an active changeset.
+ ///
+ /// A task that represents the asynchronous write operation.
+ protected override async Task WriteEndChangesetImplementationAsync()
+ {
+ // write pending message data (headers, response line) for a previously unclosed message/request
+ await this.WritePendingMessageDataAsync(true)
+ .ConfigureAwait(false);
+
+ // change the state first so we validate the change set boundary before attempting to write it.
+ this.SetState(BatchWriterState.ChangesetCompleted);
+ this.atomicityGroupId = null;
+ }
+
+ ///
+ /// Asynchronously creates an for writing an operation of a batch request.
+ ///
+ /// The Http method to be used for this request operation.
+ /// The Uri to be used for this request operation.
+ /// The Content-ID value to write in ChangeSet head.
+ ///
+ /// The format of operation Request-URI, which could be AbsoluteUri, AbsoluteResourcePathAndHost, or RelativeResourcePath.
+ /// The prerequisite request ids of this request.
+ /// A task that represents the asynchronous operation.
+ /// The value of the TResult parameter contains an
+ /// that can be used to write the request operation.
+ protected override async Task CreateOperationRequestMessageImplementationAsync(
+ string method,
+ Uri uri,
+ string contentId,
+ BatchPayloadUriOption payloadUriOption,
+ IEnumerable dependsOnIds)
+ {
+ // write pending message data (headers, request line) for a previously unclosed message/request
+ await this.WritePendingMessageDataAsync(true)
+ .ConfigureAwait(false);
+
+ // For json batch request, content Id is required for single request or request within atomicityGroup.
+ if (contentId == null)
+ {
+ contentId = Guid.NewGuid().ToString();
+ }
+
+ AddGroupIdLookup(contentId);
+
+ // Create the new request operation with a non-null dependsOnIds.
+ this.CurrentOperationRequestMessage = BuildOperationRequestMessage(
+ this.JsonLightOutputContext.GetOutputStream(), method, uri, contentId,
+ this.atomicityGroupId,
+ dependsOnIds ?? Enumerable.Empty());
+
+ this.SetState(BatchWriterState.OperationCreated);
+
+ // write the operation's start boundary string
+ await this.WriteStartBoundaryForOperationAsync()
+ .ConfigureAwait(false);
+
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyId)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(contentId)
+ .ConfigureAwait(false);
+
+ if (this.atomicityGroupId != null)
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyAtomicityGroup)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(this.atomicityGroupId)
+ .ConfigureAwait(false);
+ }
+
+ if (this.CurrentOperationRequestMessage.DependsOnIds != null
+ && this.CurrentOperationRequestMessage.DependsOnIds.Any())
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyDependsOn)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.StartArrayScopeAsync()
+ .ConfigureAwait(false);
+
+ foreach (string dependsOnId in this.CurrentOperationRequestMessage.DependsOnIds)
+ {
+ ValidateDependsOnId(contentId, dependsOnId);
+ await this.asynchronousJsonWriter.WriteValueAsync(dependsOnId)
+ .ConfigureAwait(false);
+ }
+
+ await this.asynchronousJsonWriter.EndArrayScopeAsync()
+ .ConfigureAwait(false);
+ }
+
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyMethod)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(method)
+ .ConfigureAwait(false);
+
+ await this.WriteRequestUriAsync(uri, payloadUriOption)
+ .ConfigureAwait(false);
+
+ return this.CurrentOperationRequestMessage;
+ }
+
+ ///
+ /// Asynchronously creates an for writing an operation of a batch response.
+ ///
+ /// The Content-ID value to write in ChangeSet head.
+ /// A task that represents the asynchronous operation.
+ /// The value of the TResult parameter contains an
+ /// that can be used to write the response operation.
+ protected override async Task CreateOperationResponseMessageImplementationAsync(string contentId)
+ {
+ await this.WritePendingMessageDataAsync(true)
+ .ConfigureAwait(false);
+
+ // Url resolver: In responses we don't need to use our batch URL resolver, since there are no cross referencing URLs
+ // so use the URL resolver from the batch message instead.
+ //
+ // ContentId: could be null from public API common for both formats, so we don't enforce non-null value for Json format
+ // for sake of backward compatibility. For Json Batch response message, normally caller should use the same value
+ // from the request.
+ this.CurrentOperationResponseMessage = BuildOperationResponseMessage(
+ this.JsonLightOutputContext.GetOutputStream(),
+ contentId, this.atomicityGroupId);
+ this.SetState(BatchWriterState.OperationCreated);
+
+ // Start the Json object for the response
+ await this.WriteStartBoundaryForOperationAsync()
+ .ConfigureAwait(false);
+
+ return this.CurrentOperationResponseMessage;
+ }
+
///
/// Validates the dependsOnId. It needs to be a valid id, and cannot be inside another atomic group.
///
@@ -521,7 +740,7 @@ private void WriteStartBoundaryForOperation()
private void StartBatchOperationContent()
{
Debug.Assert(this.CurrentOperationMessage != null, "Expected non-null operation message!");
- Debug.Assert(this.JsonLightOutputContext.JsonWriter != null, "Must have a Json writer!");
+ Debug.Assert(this.jsonWriter != null, "Must have a Json writer!");
// write the pending headers (if any)
this.WritePendingMessageData(false);
@@ -534,7 +753,7 @@ private void StartBatchOperationContent()
// flush the text writer to make sure all buffers of the text writer
// are flushed to the underlying async stream
- this.JsonLightOutputContext.JsonWriter.Flush();
+ this.jsonWriter.Flush();
}
///
@@ -547,7 +766,7 @@ private void WritePendingMessageData(bool reportMessageCompleted)
{
if (this.CurrentOperationMessage != null)
{
- Debug.Assert(this.JsonLightOutputContext.JsonWriter != null,
+ Debug.Assert(this.jsonWriter != null,
"Must have a batch writer if pending data exists.");
if (this.CurrentOperationRequestMessage != null)
@@ -590,14 +809,13 @@ private void WriteBatchEnvelope()
this.jsonWriter.StartObjectScope();
// Start the requests / responses property
- this.jsonWriter.WriteName(this.JsonLightOutputContext.WritingResponse ? PropertyResponses : PropertyRequests);
+ this.jsonWriter.WriteName(this.JsonLightOutputContext.WritingResponse ? PropertyResponses : PropertyRequests);
this.jsonWriter.StartArrayScope();
}
///
/// Writing pending data for the current request message.
///
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "need to use lower characters for header key")]
private void WritePendingRequestMessageData()
{
Debug.Assert(this.CurrentOperationRequestMessage != null, "this.CurrentOperationRequestMessage != null");
@@ -621,7 +839,6 @@ private void WritePendingRequestMessageData()
///
/// Writing pending data for the current response message.
///
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "need to use lower characters for header key")]
private void WritePendingResponseMessageData()
{
Debug.Assert(this.JsonLightOutputContext.WritingResponse, "If the response message is available we must be writing response.");
@@ -674,9 +891,9 @@ private void WriteRequestUri(Uri uri, BatchPayloadUriOption payloadUriOption)
break;
case BatchPayloadUriOption.AbsoluteUriUsingHostHeader:
- string absoluteResourcePath = absoluteUriString.Substring(absoluteUriString.IndexOf('/', absoluteUriString.IndexOf("//", StringComparison.Ordinal) + 2));
+ string absoluteResourcePath = ExtractAbsoluteResourcePath(absoluteUriString);
this.jsonWriter.WriteValue(absoluteResourcePath);
- this.CurrentOperationRequestMessage.SetHeader("host", string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}:{1}", uri.Host, uri.Port));
+ this.CurrentOperationRequestMessage.SetHeader("host", string.Format(CultureInfo.InvariantCulture, "{0}:{1}", uri.Host, uri.Port));
break;
case BatchPayloadUriOption.RelativeUri:
@@ -693,5 +910,246 @@ private void WriteRequestUri(Uri uri, BatchPayloadUriOption payloadUriOption)
this.jsonWriter.WriteValue(UriUtils.UriToString(uri));
}
}
+
+ ///
+ /// Asynchronously writes the start boundary for an operation. This is Json start object.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private Task WriteStartBoundaryForOperationAsync()
+ {
+ // Start the individual message object
+ return this.asynchronousJsonWriter.StartObjectScopeAsync();
+ }
+
+ ///
+ /// Asynchronously writes all the pending headers and prepares the writer to write a content of the operation.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task StartBatchOperationContentAsync()
+ {
+ Debug.Assert(this.CurrentOperationMessage != null, "Expected non-null operation message!");
+ Debug.Assert(this.asynchronousJsonWriter != null, "Must have an asynchronous Json writer!");
+
+ // write the pending headers (if any)
+ await this.WritePendingMessageDataAsync(false)
+ .ConfigureAwait(false);
+
+ await this.asynchronousJsonWriter.WriteRawValueAsync(string.Format(CultureInfo.InvariantCulture,
+ "{0} \"{1}\" {2}",
+ JsonConstants.ArrayElementSeparator,
+ PropertyBody,
+ JsonConstants.NameValueSeparator)).ConfigureAwait(false);
+
+ // flush the text writer to make sure all buffers of the text writer
+ // are flushed to the underlying async stream
+ await this.asynchronousJsonWriter.FlushAsync()
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously writes any pending data for the current operation message (if any).
+ ///
+ ///
+ /// A flag to control whether after writing the pending data we report writing the message to be completed or not.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task WritePendingMessageDataAsync(bool reportMessageCompleted)
+ {
+ if (this.CurrentOperationMessage != null)
+ {
+ Debug.Assert(this.asynchronousJsonWriter != null,
+ "Must have an asynchronous Json writer if pending data exists.");
+
+ if (this.CurrentOperationRequestMessage != null)
+ {
+ await WritePendingRequestMessageDataAsync()
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ await WritePendingResponseMessageDataAsync()
+ .ConfigureAwait(false);
+ }
+
+ if (reportMessageCompleted)
+ {
+ this.CurrentOperationMessage.PartHeaderProcessingCompleted();
+ this.CurrentOperationRequestMessage = null;
+ this.CurrentOperationResponseMessage = null;
+
+ await this.EnsurePrecedingMessageIsClosedAsync()
+ .ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ /// Asynchronously closes preceding message Json object if any.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private Task EnsurePrecedingMessageIsClosedAsync()
+ {
+ // There shouldn't be any pending message object.
+ Debug.Assert(this.CurrentOperationMessage == null, "this.CurrentOperationMessage == null");
+ return this.asynchronousJsonWriter.EndObjectScopeAsync();
+ }
+
+ ///
+ /// Asynchronously writes the json format batch envelope.
+ /// Always sets the isBatchEnvelopeWritten flag to true before return.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task WriteBatchEnvelopeAsync()
+ {
+ // Start the top level scope
+ await this.asynchronousJsonWriter.StartObjectScopeAsync()
+ .ConfigureAwait(false);
+
+ // Start the requests / responses property
+ await this.asynchronousJsonWriter.WriteNameAsync(this.JsonLightOutputContext.WritingResponse ? PropertyResponses : PropertyRequests)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.StartArrayScopeAsync()
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously writes pending data for the current request message.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task WritePendingRequestMessageDataAsync()
+ {
+ Debug.Assert(this.CurrentOperationRequestMessage != null, "this.CurrentOperationRequestMessage != null");
+
+ // headers property.
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyHeaders)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.StartObjectScopeAsync()
+ .ConfigureAwait(false);
+ IEnumerable> headers = this.CurrentOperationRequestMessage.Headers;
+ if (headers != null)
+ {
+ foreach (KeyValuePair headerPair in headers)
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(headerPair.Key.ToLowerInvariant())
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(headerPair.Value)
+ .ConfigureAwait(false);
+ }
+ }
+
+ await this.asynchronousJsonWriter.EndObjectScopeAsync()
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously writes pending data for the current response message.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task WritePendingResponseMessageDataAsync()
+ {
+ Debug.Assert(this.JsonLightOutputContext.WritingResponse, "If the response message is available we must be writing response.");
+ Debug.Assert(this.CurrentOperationResponseMessage != null, "this.CurrentOperationResponseMessage != null");
+
+ // id property.
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyId)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(this.CurrentOperationResponseMessage.ContentId)
+ .ConfigureAwait(false);
+
+ // atomicityGroup property.
+ if (this.atomicityGroupId != null)
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyAtomicityGroup)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(this.atomicityGroupId)
+ .ConfigureAwait(false);
+ }
+
+ // response status property.
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyStatus)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(this.CurrentOperationResponseMessage.StatusCode)
+ .ConfigureAwait(false);
+
+ // headers property.
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyHeaders)
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.StartObjectScopeAsync()
+ .ConfigureAwait(false);
+ IEnumerable> headers = this.CurrentOperationMessage.Headers;
+ if (headers != null)
+ {
+ foreach (KeyValuePair headerPair in headers)
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(headerPair.Key.ToLowerInvariant())
+ .ConfigureAwait(false);
+ await this.asynchronousJsonWriter.WriteValueAsync(headerPair.Value)
+ .ConfigureAwait(false);
+ }
+ }
+
+ await this.asynchronousJsonWriter.EndObjectScopeAsync()
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously writes the request uri.
+ ///
+ /// The uri for the request operation.
+ ///
+ /// The format of operation Request-URI, which could be AbsoluteUri, AbsoluteResourcePathAndHost, or RelativeResourcePath.
+ ///
+ /// A task that represents the asynchronous write operation.
+ private async Task WriteRequestUriAsync(Uri uri, BatchPayloadUriOption payloadUriOption)
+ {
+ await this.asynchronousJsonWriter.WriteNameAsync(PropertyUrl)
+ .ConfigureAwait(false);
+
+ if (uri.IsAbsoluteUri)
+ {
+ Uri baseUri = this.OutputContext.MessageWriterSettings.BaseUri;
+ string absoluteUriString = uri.AbsoluteUri;
+
+ switch (payloadUriOption)
+ {
+ case BatchPayloadUriOption.AbsoluteUri:
+ await this.asynchronousJsonWriter.WriteValueAsync(UriUtils.UriToString(uri))
+ .ConfigureAwait(false);
+ break;
+
+ case BatchPayloadUriOption.AbsoluteUriUsingHostHeader:
+ string absoluteResourcePath = ExtractAbsoluteResourcePath(absoluteUriString);
+ await this.asynchronousJsonWriter.WriteValueAsync(absoluteResourcePath)
+ .ConfigureAwait(false);
+ this.CurrentOperationRequestMessage.SetHeader("host",
+ string.Format(CultureInfo.InvariantCulture, "{0}:{1}", uri.Host, uri.Port));
+ break;
+
+ case BatchPayloadUriOption.RelativeUri:
+ Debug.Assert(baseUri != null, "baseUri != null");
+ string baseUriString = UriUtils.UriToString(baseUri);
+ Debug.Assert(uri.AbsoluteUri.StartsWith(baseUriString, StringComparison.Ordinal), "absoluteUriString.StartsWith(baseUriString)");
+ string relativeResourcePath = uri.AbsoluteUri.Substring(baseUriString.Length);
+ await this.asynchronousJsonWriter.WriteValueAsync(relativeResourcePath)
+ .ConfigureAwait(false);
+ break;
+ }
+ }
+ else
+ {
+ await this.asynchronousJsonWriter.WriteValueAsync(UriUtils.UriToString(uri))
+ .ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Extracts the absolute resource path from the absolute Uri string.
+ ///
+ /// The absolute Uri string.
+ ///
+ private static string ExtractAbsoluteResourcePath(string absoluteUriString)
+ {
+ return absoluteUriString.Substring(absoluteUriString.IndexOf('/', absoluteUriString.IndexOf("//", StringComparison.Ordinal) + 2));
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.OData.Core/MultipartMixed/ODataMultipartMixedBatchWriter.cs b/src/Microsoft.OData.Core/MultipartMixed/ODataMultipartMixedBatchWriter.cs
index 7e843900dd..4948f512b9 100644
--- a/src/Microsoft.OData.Core/MultipartMixed/ODataMultipartMixedBatchWriter.cs
+++ b/src/Microsoft.OData.Core/MultipartMixed/ODataMultipartMixedBatchWriter.cs
@@ -139,6 +139,12 @@ public override void StreamDisposed()
this.RawOutputContext.InitializeRawValueWriter();
}
+ public override Task StreamDisposedAsync()
+ {
+ return TaskUtils.GetTaskForSynchronousOperation(
+ () => this.StreamDisposed());
+ }
+
///
/// This method notifies the listener, that an in-stream error is to be written.
///
diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchWriterTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchWriterTests.cs
new file mode 100644
index 0000000000..b901b9b517
--- /dev/null
+++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchWriterTests.cs
@@ -0,0 +1,942 @@
+//---------------------------------------------------------------------
+//
+// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
+//
+//---------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.OData.Edm;
+using Microsoft.OData.JsonLight;
+using Xunit;
+
+namespace Microsoft.OData.Core.Tests.JsonLight
+{
+ public class ODataJsonLightBatchWriterTests
+ {
+ private const string ServiceUri = "http://tempuri.org";
+ private EdmModel model;
+ private MemoryStream stream;
+ private ODataMessageWriterSettings settings;
+ private ODataMediaType mediaType;
+ private Encoding encoding;
+
+ private EdmEnumType customerTypeEnumType;
+ private EdmEntityType customerEntityType;
+ private EdmEntityType orderEntityType;
+ private EdmEntitySet customerEntitySet;
+ private EdmEntitySet orderEntitySet;
+
+ public ODataJsonLightBatchWriterTests()
+ {
+ InitializeEdmModel();
+ this.stream = new MemoryStream();
+ this.settings = new ODataMessageWriterSettings { Version = ODataVersion.V4 };
+ this.settings.SetServiceDocumentUri(new Uri(ServiceUri));
+ this.mediaType = new ODataMediaType("application", "json",
+ new[]
+ {
+ new KeyValuePair("odata.metadata", "minimal"),
+ new KeyValuePair("odata.streaming", "true"),
+ new KeyValuePair("IEEE754Compatible", "false"),
+ new KeyValuePair("charset", "utf-8")
+ });
+ this.encoding = Encoding.UTF8;
+ }
+
+ private void InitializeEdmModel()
+ {
+ this.model = new EdmModel();
+
+ this.customerTypeEnumType = new EdmEnumType("NS", "CustomerType");
+ this.customerEntityType = new EdmEntityType("NS", "Customer");
+ this.orderEntityType = new EdmEntityType("NS", "Order");
+
+ this.customerTypeEnumType.AddMember(new EdmEnumMember(this.customerTypeEnumType, "Retail", new EdmEnumMemberValue(0)));
+ this.customerTypeEnumType.AddMember(new EdmEnumMember(this.customerTypeEnumType, "Wholesale", new EdmEnumMemberValue(1)));
+ this.model.AddElement(this.customerTypeEnumType);
+
+ var customerIdProperty = this.customerEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32);
+ this.customerEntityType.AddKeys(customerIdProperty);
+ this.customerEntityType.AddStructuralProperty("Name", EdmPrimitiveTypeKind.String);
+ this.customerEntityType.AddStructuralProperty("Type", new EdmEnumTypeReference(this.customerTypeEnumType, false));
+ this.model.AddElement(this.customerEntityType);
+
+ var orderIdProperty = this.orderEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32);
+ this.orderEntityType.AddKeys(orderIdProperty);
+ this.orderEntityType.AddStructuralProperty("CustomerId", EdmPrimitiveTypeKind.Int32);
+ this.orderEntityType.AddStructuralProperty("Amount", EdmPrimitiveTypeKind.Decimal);
+ this.model.AddElement(this.orderEntityType);
+
+ var entityContainer = new EdmEntityContainer("NS", "Container");
+ this.model.AddElement(entityContainer);
+
+ this.customerEntitySet = entityContainer.AddEntitySet("Customers", this.customerEntityType);
+ this.orderEntitySet = entityContainer.AddEntitySet("Orders", this.orderEntityType);
+ }
+
+ [Theory]
+ [InlineData(true, "{\"requests\":[]}")]
+ [InlineData(false, "{\"responses\":[]}")]
+ public async Task WriteBatchAsync(bool writingRequest, string expected)
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ },
+ writingRequest: writingRequest);
+
+ Assert.Equal(result, expected);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal("{\"requests\":[{" +
+ "\"id\":\"1\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithChangesetAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync("69028f2c-f57b-4850-89f0-b7e5e002d4bc");
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal("{\"requests\":[{" +
+ "\"id\":\"1\"," +
+ "\"atomicityGroup\":\"69028f2c-f57b-4850-89f0-b7e5e002d4bc\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithDependsOnIdsAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage1 = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter1 = new ODataMessageWriter(operationRequestMessage1))
+ {
+ var jsonLightWriter = await messageWriter1.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ // Operation request depends on the previous (Content ID: 1)
+ var dependsOnIds = new List { "1" };
+ var operationRequestMessage2 = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Orders"), "2", BatchPayloadUriOption.AbsoluteUri, dependsOnIds);
+
+ using (var messageWriter2 = new ODataMessageWriter(operationRequestMessage2))
+ {
+ var jsonLightWriter = await messageWriter2.CreateODataResourceWriterAsync(this.orderEntitySet, this.orderEntityType);
+
+ var orderResource = CreateOrderResource(1);
+ await jsonLightWriter.WriteStartAsync(orderResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal("{\"requests\":[" +
+ "{\"id\":\"1\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}," +
+ "{\"id\":\"2\"," +
+ "\"dependsOn\":[\"1\"]," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Orders\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"CustomerId\":1,\"Amount\":13}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithChangesetAndDependsOnIdsAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync("69028f2c-f57b-4850-89f0-b7e5e002d4bc");
+
+ var operationRequestMessage1 = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter1 = new ODataMessageWriter(operationRequestMessage1))
+ {
+ var jsonLightWriter = await messageWriter1.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ // Operation request depends on the previous (Content ID: 1)
+ var dependsOnIds = new List { "1" };
+ var operationRequestMessage2 = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Orders"), "2", BatchPayloadUriOption.AbsoluteUri, dependsOnIds);
+
+ using (var messageWriter2 = new ODataMessageWriter(operationRequestMessage2))
+ {
+ var jsonLightWriter = await messageWriter2.CreateODataResourceWriterAsync(this.orderEntitySet, this.orderEntityType);
+
+ var orderResource = CreateOrderResource(1);
+ await jsonLightWriter.WriteStartAsync(orderResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal(
+ "{\"requests\":[" +
+ "{\"id\":\"1\"," +
+ "\"atomicityGroup\":\"69028f2c-f57b-4850-89f0-b7e5e002d4bc\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}," +
+ "{\"id\":\"2\"," +
+ "\"atomicityGroup\":\"69028f2c-f57b-4850-89f0-b7e5e002d4bc\"," +
+ "\"dependsOn\":[\"1\"]," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Orders\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"CustomerId\":1,\"Amount\":13}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithGroupIdForChangesetNotSpecifiedAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.StartsWith("{\"requests\":[{" +
+ "\"id\":\"1\"," +
+ "\"atomicityGroup\":\"", // atomicityGroup is a random Guid
+ result);
+ Assert.EndsWith("\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithContentIdNullAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), /*contentId*/ null);
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.StartsWith("{\"requests\":[{\"id\":\"", // id is a random Guid
+ result);
+ Assert.EndsWith("\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"http://tempuri.org/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchResponseAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationResponseMessage = await jsonLightBatchWriter.CreateOperationResponseMessageAsync("1");
+
+ using (var messageWriter = new ODataMessageWriter(operationResponseMessage, this.settings, this.model))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ },
+ /*writingRequest*/ false);
+
+ Assert.Equal("{\"responses\":[{" +
+ "\"id\":\"1\"," +
+ "\"status\":0," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"@odata.context\":\"http://tempuri.org/$metadata#Customers/$entity\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchResponseWithChangesetAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync("69028f2c-f57b-4850-89f0-b7e5e002d4bc");
+
+ var operationResponseMessage = await jsonLightBatchWriter.CreateOperationResponseMessageAsync("1");
+
+ using (var messageWriter = new ODataMessageWriter(operationResponseMessage, this.settings, this.model))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ },
+ /*writingRequest*/ false);
+
+ Assert.Equal("{\"responses\":[{" +
+ "\"id\":\"1\"," +
+ "\"atomicityGroup\":\"69028f2c-f57b-4850-89f0-b7e5e002d4bc\"," +
+ "\"status\":0," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"@odata.context\":\"http://tempuri.org/$metadata#Customers/$entity\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithAbsoluteUriUsingHostHeaderAsync()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/odata/Customers"), "1", BatchPayloadUriOption.AbsoluteUriUsingHostHeader);
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal("{\"requests\":[{" +
+ "\"id\":\"1\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"/odata/Customers\"," +
+ "\"headers\":{\"host\":\"tempuri.org:80\",\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithRelativeUriAsync()
+ {
+ this.settings.BaseUri = new Uri(ServiceUri);
+
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri("/odata/Customers", UriKind.Relative), "1", BatchPayloadUriOption.RelativeUri);
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal(
+ "{\"requests\":[{" +
+ "\"id\":\"1\"," +
+ "\"method\":\"POST\"," +
+ "\"url\":\"odata/Customers\"," +
+ "\"headers\":{\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " +
+ "\"body\" :{\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}",
+ result);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ReportMessageCompleted()
+ {
+ var result = await SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+ // No writer created for the request message
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ });
+
+ Assert.Equal(
+ "{\"requests\":[{\"id\":\"1\",\"method\":\"POST\",\"url\":\"http://tempuri.org/Customers\",\"headers\":{}}]}",
+ result);
+ }
+
+ #region Exception Cases
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForChangesetStartedWithinChangeset()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ // Try to start writing a changeset when another is active
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ }));
+
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(Strings.ODataBatchWriter_CannotStartChangeSetWithActiveChangeSet, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForEndChangesetNotPrecededByStartChangeset()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ // Try to end changeset when there's none active
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ }));
+
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(Strings.ODataBatchWriter_CannotCompleteChangeSetWithoutActiveChangeSet, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForEndBatchBeforeEndChangeset()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ // Try to stop writing batch before changeset end
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ }));
+
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(Strings.ODataBatchWriter_CannotCompleteBatchWithActiveChangeSet, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForNoStartBatch()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ // Try to start writing changeset before batch start
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ }));
+
+ Assert.IsType(exception.InnerException);
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromStart, exception.InnerException.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForMultipleStartBatch()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ // Try to start writing batch again
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromBatchStarted, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForStateTransitionsAfterEndBatch()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ // Try to start writing batch after batch end
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromBatchCompleted, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForResponseOperationMessage()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.CreateOperationResponseMessageAsync("1");
+ },
+ /*writingRequest*/ true));
+
+ Assert.Equal(Strings.ODataBatchWriter_CannotCreateResponseOperationWhenWritingRequest, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchResponseAsync_ThrowsExceptionForRequestOperationMessage()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+ },
+ /*writingRequest*/ false));
+
+ Assert.Equal(Strings.ODataBatchWriter_CannotCreateRequestOperationWhenWritingResponse, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForInvalidTransitionFromEndChangeset()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ // // Try to start writing batch after changeset end
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromChangeSetCompleted, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForInvalidTransitionFromOperationStreamDisposed()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ var jsonLightWriter = await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ var customerResource = CreateCustomerResource(1);
+ await jsonLightWriter.WriteStartAsync(customerResource);
+ await jsonLightWriter.WriteEndAsync();
+ }
+
+ // Try to start writing batch after operation stream disposed
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromOperationContentStreamDisposed, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForInvalidTransitionFromOperationStreamRequested()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+
+ var operationRequestMessage = await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ using (var messageWriter = new ODataMessageWriter(operationRequestMessage))
+ {
+ await messageWriter.CreateODataResourceWriterAsync(this.customerEntitySet, this.customerEntityType);
+
+ // Try to end writing batch after operation stream requested
+ await jsonLightBatchWriter.WriteEndBatchAsync();
+ }
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromOperationContentStreamRequested, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForInvalidTransitionFromOperationCreated()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+ // Try to start writing batch after operation created
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromOperationCreated, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForInvalidTransitionFromChangesetStarted()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ // Try to start writing batch after operation created
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_InvalidTransitionFromChangeSetStarted, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForSynchronousOutputContext()
+ {
+ var jsonLightOutputContext = CreateJsonLightOutputContext(/*writingRequest*/ true, /*asynchronous*/ false);
+ var jsonLightBatchWriter = new ODataJsonLightBatchWriter(jsonLightOutputContext);
+ // Try to asynchronously start writing batch with an output context intended for synchronous writing
+ var exception = await Assert.ThrowsAsync(
+ () => jsonLightBatchWriter.WriteStartBatchAsync());
+
+ Assert.Equal(Strings.ODataBatchWriter_AsyncCallOnSyncWriter, exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForGetMethodInChangeset()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ // Try to create operation request message for a GET
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "GET", new Uri($"{ServiceUri}/Customers(1)"), "1");
+ }));
+
+ Assert.Equal(Strings.ODataBatch_InvalidHttpMethodForChangeSetRequest("GET"), exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestWithChangesetAsync_ThrowsExceptionForContentIdIsNull()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ // Try to create operation request message with null content id
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "PUT", new Uri($"{ServiceUri}/Customers(1)"), /*contentId*/ null);
+ }));
+
+ Assert.Equal(Strings.ODataBatchOperationHeaderDictionary_KeyNotFound(ODataConstants.ContentIdHeader), exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForMaxPartsPerBatchLimitExceeded()
+ {
+ this.settings.MessageQuotas.MaxPartsPerBatch = 1;
+
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_MaxBatchSizeExceeded(1), exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForMaxOperationsPerChangesetLimitExceeded()
+ {
+ this.settings.MessageQuotas.MaxOperationsPerChangeset = 1;
+
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync();
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "PUT", new Uri($"{ServiceUri}/Customers(1)"), "1");
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "PUT", new Uri($"{ServiceUri}/Customers(1)"), "2");
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_MaxChangeSetSizeExceeded(1), exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForDuplicateContentId()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "PUT", new Uri($"{ServiceUri}/Customers(1)"), "1");
+ // Try to create operation request message with similar content id
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "PUT", new Uri($"{ServiceUri}/Customers(1)"), "1");
+ }));
+
+ Assert.Equal(Strings.ODataBatchWriter_DuplicateContentIDsNotAllowed("1"), exception.Message);
+ }
+
+ [Fact]
+ public async Task WriteBatchRequestAsync_ThrowsExceptionForContentIdNotInTheSameAtomicityGroup()
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => SetupJsonLightBatchWriterAndRunTestAsync(
+ async (jsonLightBatchWriter) =>
+ {
+ await jsonLightBatchWriter.WriteStartBatchAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync("fd04fc24");
+
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Customers"), "1");
+
+ await jsonLightBatchWriter.WriteEndChangesetAsync();
+ await jsonLightBatchWriter.WriteStartChangesetAsync("b62a2456");
+
+ // Operation request depends on content id from different atomicity group
+ var dependsOnIds = new List { "1" };
+ await jsonLightBatchWriter.CreateOperationRequestMessageAsync(
+ "POST", new Uri($"{ServiceUri}/Orders"), "2", BatchPayloadUriOption.AbsoluteUri, dependsOnIds);
+ }));
+
+ Assert.Equal(Strings.ODataBatchReader_DependsOnRequestIdIsPartOfAtomicityGroupNotAllowed("2", "fd04fc24"), exception.Message);
+ }
+
+ #endregion Exception Cases
+
+ ///
+ /// Sets up an ODataJsonLightBatchWriter,
+ /// then runs the given test code asynchronously,
+ /// then flushes and reads the stream back as a string for customized verification.
+ ///
+ private async Task SetupJsonLightBatchWriterAndRunTestAsync(
+ Func func,
+ bool writingRequest = true)
+ {
+ var jsonLightOutputContext = CreateJsonLightOutputContext(writingRequest, /*asynchronous*/ true);
+ var jsonLightBatchWriter = new ODataJsonLightBatchWriter(jsonLightOutputContext);
+
+ await func(jsonLightBatchWriter);
+
+ this.stream.Position = 0;
+ return await new StreamReader(this.stream).ReadToEndAsync();
+ }
+
+ private ODataJsonLightOutputContext CreateJsonLightOutputContext(bool writingRequest = true, bool asynchronous = false)
+ {
+ var messageInfo = new ODataMessageInfo
+ {
+ MessageStream = this.stream,
+ MediaType = this.mediaType,
+ Encoding = this.encoding,
+ IsResponse = !writingRequest,
+ IsAsync = asynchronous,
+ Model = this.model
+ };
+
+ return new ODataJsonLightOutputContext(messageInfo, this.settings);
+ }
+
+ #region Helper Methods
+
+ private static ODataResource CreateCustomerResource(int customerId)
+ {
+ return new ODataResource
+ {
+ TypeName = "NS.Customer",
+ Properties = new List
+ {
+ new ODataProperty
+ {
+ Name = "Id",
+ Value = customerId,
+ SerializationInfo = new ODataPropertySerializationInfo { PropertyKind = ODataPropertyKind.Key }
+ },
+ new ODataProperty { Name = "Name", Value = $"Customer {customerId}" },
+ new ODataProperty { Name = "Type", Value = new ODataEnumValue("Retail") }
+ },
+ SerializationInfo = new ODataResourceSerializationInfo
+ {
+ NavigationSourceName = "Customers",
+ ExpectedTypeName = "NS.Customer",
+ NavigationSourceEntityTypeName = "NS.Customer",
+ NavigationSourceKind = EdmNavigationSourceKind.EntitySet
+ }
+ };
+ }
+
+ private static ODataResource CreateOrderResource(int orderId)
+ {
+ return new ODataResource
+ {
+ TypeName = "NS.Order",
+ Properties = new List
+ {
+ new ODataProperty
+ {
+ Name = "Id",
+ Value = orderId,
+ SerializationInfo = new ODataPropertySerializationInfo { PropertyKind = ODataPropertyKind.Key }
+ },
+ new ODataProperty { Name = "CustomerId", Value = 1 },
+ new ODataProperty { Name = "Amount", Value = 13M }
+ },
+ SerializationInfo = new ODataResourceSerializationInfo
+ {
+ NavigationSourceName = "Orders",
+ ExpectedTypeName = "NS.Order",
+ NavigationSourceEntityTypeName = "NS.Order",
+ NavigationSourceKind = EdmNavigationSourceKind.EntitySet
+ }
+ };
+ }
+
+ #endregion Helper Methods
+ }
+}