diff --git a/src/CloudNative.CloudEvents/MimeUtilities.cs b/src/CloudNative.CloudEvents/MimeUtilities.cs
new file mode 100644
index 0000000..3b5fec1
--- /dev/null
+++ b/src/CloudNative.CloudEvents/MimeUtilities.cs
@@ -0,0 +1,65 @@
+// Copyright 2021 Cloud Native Foundation.
+// Licensed under the Apache 2.0 license.
+// See LICENSE file in the project root for full license information.
+
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+
+namespace CloudNative.CloudEvents
+{
+ // TODO: Consider this name and namespace carefully. It really does need to be public, as all the event formatters are elsewhere.
+ // But it's not ideal...
+
+ ///
+ /// Utility and extension methods around MIME.
+ ///
+ public static class MimeUtilities
+ {
+ // TODO: Should we return null, and force the caller to do the appropriate defaulting?
+ ///
+ /// Returns an encoding from a content type, defaulting to UTF-8.
+ ///
+ /// The content type, or null if no content type is known.
+ /// An encoding suitable for the charset specified in ,
+ /// or UTF-8 if no charset has been specified.
+ public static Encoding GetEncoding(this ContentType contentType) =>
+ contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8;
+
+ ///
+ /// Converts a into a .
+ ///
+ /// The header value to convert. May be null.
+ /// The converted content type, or null if is null.
+ public static ContentType ToContentType(this MediaTypeHeaderValue headerValue) =>
+ headerValue is null ? null : new ContentType(headerValue.ToString());
+
+ ///
+ /// Converts a into a .
+ ///
+ /// The content type to convert. May be null.
+ /// The converted media type header value, or null if is null.
+ public static MediaTypeHeaderValue ToMediaTypeHeaderValue(this ContentType contentType)
+ {
+ if (contentType is null)
+ {
+ return null;
+ }
+ var header = new MediaTypeHeaderValue(contentType.MediaType);
+ foreach (string parameterName in contentType.Parameters.Keys)
+ {
+ header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName].ToString()));
+ }
+ return header;
+ }
+
+ ///
+ /// Creates a from the given value, or returns null
+ /// if the input is null.
+ ///
+ /// The content type textual value. May be null.
+ /// The converted content type, or null if is null.
+ public static ContentType CreateContentTypeOrNull(string contentType) =>
+ contentType is null ? null : new ContentType(contentType);
+ }
+}
diff --git a/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs b/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs
new file mode 100644
index 0000000..1819a89
--- /dev/null
+++ b/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs
@@ -0,0 +1,82 @@
+// Copyright 2021 Cloud Native Foundation.
+// Licensed under the Apache 2.0 license.
+// See LICENSE file in the project root for full license information.
+
+using System.Linq;
+using System.Net.Http.Headers;
+using System.Net.Mime;
+using System.Text;
+using Xunit;
+
+namespace CloudNative.CloudEvents.UnitTests.Http
+{
+ public class MimeUtilitiesTest
+ {
+ [Theory]
+ [InlineData("application/json")]
+ [InlineData("application/json; charset=iso-8859-1")]
+ [InlineData("application/json; charset=iso-8859-1; name=some-name")]
+ [InlineData("application/json; charset=iso-8859-1; name=some-name; x=y; a=b")]
+ [InlineData("application/json; charset=iso-8859-1; name=some-name; boundary=xyzzy; x=y")]
+ public void ContentTypeConversions(string text)
+ {
+ var originalContentType = new ContentType(text);
+ var header = originalContentType.ToMediaTypeHeaderValue();
+ AssertEqualParts(text, header.ToString());
+ var convertedContentType = header.ToContentType();
+ AssertEqualParts(originalContentType.ToString(), convertedContentType.ToString());
+
+ // Conversions can end up reordering the parameters. In reality we're only
+ // likely to end up with a media type and charset, but our tests use more parameters.
+ // This just makes them deterministic.
+ void AssertEqualParts(string expected, string actual)
+ {
+ expected = string.Join(";", expected.Split(";").OrderBy(x => x));
+ actual = string.Join(";", actual.Split(";").OrderBy(x => x));
+ Assert.Equal(expected, actual);
+ }
+ }
+
+ [Fact]
+ public void ContentTypeConversions_Null()
+ {
+ Assert.Null(default(ContentType).ToMediaTypeHeaderValue());
+ Assert.Null(default(MediaTypeHeaderValue).ToContentType());
+ }
+
+ [Theory]
+ [InlineData("iso-8859-1")]
+ [InlineData("utf-8")]
+ public void ContentTypeGetEncoding(string charSet)
+ {
+ var contentType = new ContentType($"text/plain; charset={charSet}");
+ Encoding encoding = contentType.GetEncoding();
+ Assert.Equal(charSet, encoding.WebName);
+ }
+
+ [Fact]
+ public void ContentTypeGetEncoding_NoContentType()
+ {
+ ContentType contentType = null;
+ Encoding encoding = contentType.GetEncoding();
+ Assert.Equal(Encoding.UTF8, encoding);
+ }
+
+ [Fact]
+ public void ContentTypeGetEncoding_NoCharSet()
+ {
+ ContentType contentType = new ContentType("text/plain");
+ Encoding encoding = contentType.GetEncoding();
+ Assert.Equal(Encoding.UTF8, encoding);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("text/plain")]
+ public void CreateContentTypeOrNull_WithContentType(string text)
+ {
+ ContentType ct = MimeUtilities.CreateContentTypeOrNull(text);
+ Assert.Equal(text, ct?.ToString());
+ }
+ }
+}