/// <summary> /// Constructor /// </summary> /// <param name="cloudEvent">CloudEvent</param> /// <param name="contentMode">Content mode. Structured or binary.</param> /// <param name="formatter">Event formatter</param> public CloudEventHttpContent(CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) { byte[] content; ContentType contentType; switch (contentMode) { case ContentMode.Structured: content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); // This is optional in the specification, but can be useful. MapHeaders(cloudEvent, includeDataContentType: true); break; case ContentMode.Binary: content = formatter.EncodeBinaryModeEventData(cloudEvent); contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType); MapHeaders(cloudEvent, includeDataContentType: false); break; default: throw new ArgumentException($"Unsupported content mode: {contentMode}"); } inner = new InnerByteArrayContent(content); if (contentType is object) { Headers.ContentType = contentType.ToMediaTypeHeaderValue(); } else if (content.Length != 0) { throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent)); } }
/// <summary> /// Converts a CloudEvent to <see cref="HttpContent"/>. /// </summary> /// <param name="cloudEvent">The CloudEvent to convert. Must not be null, and must be a valid CloudEvent.</param> /// <param name="contentMode">Content mode. Structured or binary.</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> public static HttpContent ToHttpContent(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) { Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); Validation.CheckNotNull(formatter, nameof(formatter)); ReadOnlyMemory <byte> content; // The content type to include in the ContentType header - may be the data content type, or the formatter's content type. ContentType?contentType; switch (contentMode) { case ContentMode.Structured: content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); break; case ContentMode.Binary: content = formatter.EncodeBinaryModeEventData(cloudEvent); contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent)); break; default: throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); } ByteArrayContent ret = ToByteArrayContent(content); if (contentType is object) { ret.Headers.ContentType = MimeUtilities.ToMediaTypeHeaderValue(contentType); } else if (content.Length != 0) { throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent)); } // Map headers in either mode. // Including the headers in structured mode is optional in the spec (as they're already within the body) but // can be useful. ret.Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId)); foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes()) { CloudEventAttribute attribute = attributeAndValue.Key; string headerName = HttpUtilities.HttpHeaderPrefix + attribute.Name; object value = attributeAndValue.Value; // Skip the data content type attribute in binary mode, because it's already in the content type header. if (attribute == cloudEvent.SpecVersion.DataContentTypeAttribute && contentMode == ContentMode.Binary) { continue; } else { string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value)); ret.Headers.Add(headerName, headerValue); } } return(ret); }
/// <summary> /// Copies a <see cref="CloudEvent"/> into an <see cref="HttpListenerResponse" />. /// </summary> /// <param name="cloudEvent">The CloudEvent to copy. Must not be null, and must be a valid CloudEvent.</param> /// <param name="destination">The response to copy the CloudEvent to. Must not be null.</param> /// <param name="contentMode">Content mode (structured or binary)</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> /// <returns>A task representing the asynchronous operation.</returns> public static async Task CopyToHttpListenerResponseAsync(this CloudEvent cloudEvent, HttpListenerResponse destination, ContentMode contentMode, CloudEventFormatter formatter) { Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); Validation.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(formatter, nameof(formatter)); byte[] content; ContentType contentType; switch (contentMode) { case ContentMode.Structured: content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); break; case ContentMode.Binary: content = formatter.EncodeBinaryModeEventData(cloudEvent); contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType); destination.ContentType = cloudEvent.DataContentType?.ToString() ?? "application/json"; break; default: throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); } if (contentType is object) { destination.ContentType = contentType.ToString(); } else if (content.Length != 0) { throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent)); } // Map headers in either mode. // Including the headers in structured mode is optional in the spec (as they're already within the body) but // can be useful. destination.Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId)); foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes()) { var attribute = attributeAndValue.Key; var value = attributeAndValue.Value; // The content type is already handled based on the content mode. if (attribute != cloudEvent.SpecVersion.DataContentTypeAttribute) { string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value)); destination.Headers.Add(HttpUtilities.HttpHeaderPrefix + attribute.Name, headerValue); } } await destination.OutputStream.WriteAsync(content, 0, content.Length).ConfigureAwait(false); }
/// <summary> /// Converts this HTTP request into a CloudEvent object. /// </summary> /// <param name="httpRequest">The HTTP request to decode. Must not be null.</param> /// <param name="formatter">The event formatter to use to process the request body. Must not be null.</param> /// <param name="extensionAttributes">The extension attributes to use when populating the CloudEvent. May be null.</param> /// <returns>The decoded CloudEvent.</returns> /// <exception cref="ArgumentException">The request does not contain a CloudEvent.</exception> public static async Task <CloudEvent> ToCloudEventAsync( this HttpRequest httpRequest, CloudEventFormatter formatter, IEnumerable <CloudEventAttribute>?extensionAttributes) { Validation.CheckNotNull(httpRequest, nameof(httpRequest)); Validation.CheckNotNull(formatter, nameof(formatter)); if (HasCloudEventsContentType(httpRequest)) { var contentType = MimeUtilities.CreateContentTypeOrNull(httpRequest.ContentType); return(await formatter.DecodeStructuredModeMessageAsync(httpRequest.Body, contentType, extensionAttributes).ConfigureAwait(false)); } else { var headers = httpRequest.Headers; headers.TryGetValue(HttpUtilities.SpecVersionHttpHeader, out var versionId); if (versionId.Count == 0) { throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpRequest)); } var version = CloudEventsSpecVersion.FromVersionId(versionId.FirstOrDefault()) ?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", nameof(httpRequest)); if (version is null) { throw new ArgumentException($"Unsupported CloudEvents spec version '{versionId.First()}'"); } var cloudEvent = new CloudEvent(version, extensionAttributes); foreach (var header in headers) { string?attributeName = HttpUtilities.GetAttributeNameFromHeaderName(header.Key); if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) { continue; } string attributeValue = HttpUtilities.DecodeHeaderValue(header.Value.First()); cloudEvent.SetAttributeFromString(attributeName, attributeValue); } cloudEvent.DataContentType = httpRequest.ContentType; if (httpRequest.Body is Stream body) { ReadOnlyMemory <byte> data = await BinaryDataUtilities.ToReadOnlyMemoryAsync(body).ConfigureAwait(false); formatter.DecodeBinaryModeEventData(data, cloudEvent); } return(Validation.CheckCloudEventArgument(cloudEvent, nameof(httpRequest))); } }
// TODO: HttpWebResponse as well? /// <summary> /// Copies a <see cref="CloudEvent"/> into the specified <see cref="HttpWebRequest"/>. /// </summary> /// <param name="cloudEvent">CloudEvent to copy. Must not be null, and must be a valid CloudEvent.</param> /// <param name="destination">The request to populate. Must not be null.</param> /// <param name="contentMode">Content mode (structured or binary)</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> /// <returns>A task representing the asynchronous operation.</returns> public static async Task CopyToHttpWebRequestAsync(this CloudEvent cloudEvent, HttpWebRequest destination, ContentMode contentMode, CloudEventFormatter formatter) { Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); Validation.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(formatter, nameof(formatter)); ReadOnlyMemory <byte> content; // The content type to include in the ContentType header - may be the data content type, or the formatter's content type. ContentType?contentType; switch (contentMode) { case ContentMode.Structured: content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); break; case ContentMode.Binary: content = formatter.EncodeBinaryModeEventData(cloudEvent); contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent)); break; default: throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); } if (contentType is object) { destination.ContentType = contentType.ToString(); } else if (content.Length != 0) { throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent)); } // Map headers in either mode. // Including the headers in structured mode is optional in the spec (as they're already within the body) but // can be useful. destination.Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId)); foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes()) { var attribute = attributeAndValue.Key; var value = attributeAndValue.Value; if (attribute != cloudEvent.SpecVersion.DataContentTypeAttribute) { string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value)); destination.Headers.Add(HttpUtilities.HttpHeaderPrefix + attribute.Name, headerValue); } } await BinaryDataUtilities.CopyToStreamAsync(content, destination.GetRequestStream()).ConfigureAwait(false); }
private async static Task <CloudEvent> ToCloudEventAsyncImpl(HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, IEnumerable <CloudEventAttribute>?extensionAttributes, bool async) { Validation.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); Validation.CheckNotNull(formatter, nameof(formatter)); var stream = httpListenerRequest.InputStream; if (HasCloudEventsContentType(httpListenerRequest)) { var contentType = MimeUtilities.CreateContentTypeOrNull(httpListenerRequest.ContentType); return(async ? await formatter.DecodeStructuredModeMessageAsync(stream, contentType, extensionAttributes).ConfigureAwait(false) : formatter.DecodeStructuredModeMessage(stream, contentType, extensionAttributes)); } else { string versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader]; if (versionId is null) { throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpListenerRequest)); } var version = CloudEventsSpecVersion.FromVersionId(versionId) ?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", nameof(httpListenerRequest)); var cloudEvent = new CloudEvent(version, extensionAttributes); var headers = httpListenerRequest.Headers; foreach (var key in headers.AllKeys) { string?attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key); if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) { continue; } string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]); cloudEvent.SetAttributeFromString(attributeName, attributeValue); } // The data content type should not have been set via a "ce-" header; instead, // it's in the regular content type. cloudEvent.DataContentType = httpListenerRequest.ContentType; ReadOnlyMemory <byte> data = async ? await BinaryDataUtilities.ToReadOnlyMemoryAsync(stream).ConfigureAwait(false) : BinaryDataUtilities.ToReadOnlyMemory(stream); formatter.DecodeBinaryModeEventData(data, cloudEvent); return(Validation.CheckCloudEventArgument(cloudEvent, nameof(httpListenerRequest))); } }
/// <summary> /// Converts this HTTP request into a batch of CloudEvents. /// </summary> /// <param name="httpRequest">The HTTP request to decode. Must not be null.</param> /// <param name="formatter">The event formatter to use to process the request body. Must not be null.</param> /// <param name="extensionAttributes">The extension attributes to use when populating the CloudEvent. May be null.</param> /// <returns>The decoded CloudEvent.</returns> /// <exception cref="ArgumentException">The request does not contain a CloudEvent.</exception> public static async Task <IReadOnlyList <CloudEvent> > ToCloudEventBatchAsync( this HttpRequest httpRequest, CloudEventFormatter formatter, IEnumerable <CloudEventAttribute> extensionAttributes) { Validation.CheckNotNull(httpRequest, nameof(httpRequest)); Validation.CheckNotNull(formatter, nameof(formatter)); if (HasCloudEventsBatchContentType(httpRequest)) { var contentType = MimeUtilities.CreateContentTypeOrNull(httpRequest.ContentType); return(await formatter.DecodeBatchModeMessageAsync(httpRequest.Body, contentType, extensionAttributes).ConfigureAwait(false)); } else { throw new ArgumentException("HTTP message does not represent a CloudEvents batch.", nameof(httpRequest)); } }
private async static Task <IReadOnlyList <CloudEvent> > ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, IEnumerable <CloudEventAttribute>?extensionAttributes, bool async) { Validation.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); Validation.CheckNotNull(formatter, nameof(formatter)); if (HasCloudEventsBatchContentType(httpListenerRequest)) { var contentType = MimeUtilities.CreateContentTypeOrNull(httpListenerRequest.ContentType); return(async ? await formatter.DecodeBatchModeMessageAsync(httpListenerRequest.InputStream, contentType, extensionAttributes).ConfigureAwait(false) : formatter.DecodeBatchModeMessage(httpListenerRequest.InputStream, contentType, extensionAttributes)); } else { throw new ArgumentException("HTTP message does not represent a CloudEvents batch.", nameof(httpListenerRequest)); } }
public async Task CopyToHttpWebRequestAsync_Batch() { var batch = CreateSampleBatch(); HttpWebRequest request = WebRequest.CreateHttp(ListenerAddress + "ep"); request.Method = "POST"; await batch.CopyToHttpWebRequestAsync(request, new JsonEventFormatter()); var(bytes, contentType) = await SendRequestAsync(request, async context => { var bytes = await BinaryDataUtilities.ToReadOnlyMemoryAsync(context.Request.InputStream); var contentType = context.Request.Headers["Content-Type"]; return(bytes, contentType); }); Assert.Equal(MimeUtilities.BatchMediaType + "+json; charset=utf-8", contentType); var parsedBatch = new JsonEventFormatter().DecodeBatchModeMessage(bytes, MimeUtilities.CreateContentTypeOrNull(contentType), extensionAttributes: null); AssertBatchesEqual(batch, parsedBatch); }
public void CreateContentTypeOrNull_WithContentType(string text) { ContentType?ct = MimeUtilities.CreateContentTypeOrNull(text); Assert.Equal(text, ct?.ToString()); }
/// <summary> /// Converts this listener request into a CloudEvent object, with the given extension attributes. /// </summary> /// <param name="httpListenerRequest">Listener request</param> /// <param name="formatter"></param> /// <param name="extensions">List of extension instances</param> /// <returns>The CloudEvent corresponding to the given request.</returns> /// <exception cref="ArgumentException">The request does not represent a CloudEvent, /// or the event's specification version is not supported, /// or the event formatter cannot interpret it.</exception> public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, params CloudEventAttribute[] extensionAttributes) { if (HasCloudEventsContentType(httpListenerRequest)) { return(formatter.DecodeStructuredModeMessage(httpListenerRequest.InputStream, MimeUtilities.CreateContentTypeOrNull(httpListenerRequest.ContentType), extensionAttributes)); } else { string versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader]; if (versionId is null) { throw new ArgumentException("Request is not a CloudEvent"); } var version = CloudEventsSpecVersion.FromVersionId(versionId); if (version is null) { throw new ArgumentException($"Unsupported CloudEvents spec version '{versionId}'"); } var cloudEvent = new CloudEvent(version, extensionAttributes); var headers = httpListenerRequest.Headers; foreach (var key in headers.AllKeys) { string attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key); if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) { continue; } string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]); cloudEvent.SetAttributeFromString(attributeName, attributeValue); } // The data content type should not have been set via a "ce-" header; instead, // it's in the regular content type. cloudEvent.DataContentType = httpListenerRequest.ContentType; formatter.DecodeBinaryModeEventData(BinaryDataUtilities.ToByteArray(httpListenerRequest.InputStream), cloudEvent); return(cloudEvent); } }