diff --git a/eng/MSBuild/LegacySupport.props b/eng/MSBuild/LegacySupport.props
index 9e83541b0d8..c1d19162ea1 100644
--- a/eng/MSBuild/LegacySupport.props
+++ b/eng/MSBuild/LegacySupport.props
@@ -74,4 +74,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/LegacySupport/FilePolyfills/FilePolyfills.cs b/src/LegacySupport/FilePolyfills/FilePolyfills.cs
new file mode 100644
index 00000000000..f00fcf2a8a3
--- /dev/null
+++ b/src/LegacySupport/FilePolyfills/FilePolyfills.cs
@@ -0,0 +1,82 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !NET9_0_OR_GREATER
+
+#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.IO;
+
+///
+/// Provides polyfill extension members for for older frameworks.
+///
+[ExcludeFromCodeCoverage]
+internal static class FilePolyfills
+{
+ extension(File)
+ {
+#if !NET
+ ///
+ /// Asynchronously reads all bytes from a file.
+ ///
+ /// The file to read from.
+ /// The token to monitor for cancellation requests.
+ /// A task that represents the asynchronous read operation, which wraps the byte array containing the contents of the file.
+ public static async Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default)
+ {
+ using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
+ byte[] data = new byte[stream.Length];
+ int totalRead = 0;
+ while (totalRead < data.Length)
+ {
+ int read = await stream.ReadAsync(data, totalRead, data.Length - totalRead, cancellationToken).ConfigureAwait(false);
+ if (read == 0)
+ {
+ break;
+ }
+
+ totalRead += read;
+ }
+
+ return data;
+ }
+#endif
+
+ ///
+ /// Asynchronously writes all bytes to a file.
+ ///
+ /// The file to write to.
+ /// The bytes to write to the file.
+ /// The token to monitor for cancellation requests.
+ /// A task that represents the asynchronous write operation.
+ public static async Task WriteAllBytesAsync(string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default)
+ {
+ // Try to avoid ToArray() if the data is backed by a byte[] with offset 0 and matching length
+ byte[] byteArray;
+ if (MemoryMarshal.TryGetArray(bytes, out ArraySegment segment) &&
+ segment.Offset == 0 &&
+ segment.Count == segment.Array!.Length)
+ {
+ byteArray = segment.Array;
+ }
+ else
+ {
+ byteArray = bytes.ToArray();
+ }
+
+#if NET
+ await File.WriteAllBytesAsync(path, byteArray, cancellationToken).ConfigureAwait(false);
+#else
+ using var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true);
+ await stream.WriteAsync(byteArray, 0, byteArray.Length, cancellationToken).ConfigureAwait(false);
+#endif
+ }
+ }
+}
+
+#endif
diff --git a/src/LegacySupport/FilePolyfills/README.md b/src/LegacySupport/FilePolyfills/README.md
new file mode 100644
index 00000000000..bb0834cef27
--- /dev/null
+++ b/src/LegacySupport/FilePolyfills/README.md
@@ -0,0 +1,9 @@
+# About FilePolyfills
+
+This folder contains C# 14 extension member polyfills for `System.IO.File` methods
+that are not available on older frameworks.
+
+- `File.ReadAllBytesAsync` - Added in .NET Core 2.0, not available in .NET Framework 4.6.2 or .NET Standard 2.0
+
+The polyfill uses C# 14 extension members so the call site can use `File.ReadAllBytesAsync` naturally
+and it will use the real one on supported platforms and the polyfill elsewhere.
diff --git a/src/LegacySupport/MediaTypeMap/MediaTypeMap.cs b/src/LegacySupport/MediaTypeMap/MediaTypeMap.cs
new file mode 100644
index 00000000000..ac28edb5bcb
--- /dev/null
+++ b/src/LegacySupport/MediaTypeMap/MediaTypeMap.cs
@@ -0,0 +1,777 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma warning disable SA1201
+#pragma warning disable SA1124
+#pragma warning disable CA1859
+#pragma warning disable IDE1006
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Net.Mime;
+
+///
+/// Provides methods for mapping between file extensions and media types (MIME types).
+///
+[ExcludeFromCodeCoverage]
+internal static class MediaTypeMap
+{
+ private static readonly Dictionary s_extensionToMediaType = new(StringComparer.OrdinalIgnoreCase)
+ {
+ [".3g2"] = "video/3gpp2",
+ [".3gp"] = "video/3gpp",
+ [".3gp2"] = "video/3gpp2",
+ [".3gpp"] = "video/3gpp",
+ [".7z"] = "application/x-7z-compressed",
+ [".aac"] = "audio/aac",
+ [".abw"] = "application/x-abiword",
+ [".accdb"] = "application/msaccess",
+ [".accde"] = "application/msaccess",
+ [".accdt"] = "application/msaccess",
+ [".adt"] = "audio/vnd.dlna.adts",
+ [".adts"] = "audio/vnd.dlna.adts",
+ [".ai"] = "application/postscript",
+ [".aif"] = "audio/x-aiff",
+ [".aifc"] = "audio/aifc",
+ [".aiff"] = "audio/aiff",
+ [".apk"] = "application/vnd.android.package-archive",
+ [".apng"] = "image/apng",
+ [".appcache"] = "text/cache-manifest",
+ [".application"] = "application/x-ms-application",
+ [".arc"] = "application/x-freearc",
+ [".asf"] = "video/x-ms-asf",
+ [".asm"] = "text/plain",
+ [".asr"] = "video/x-ms-asf",
+ [".asx"] = "video/x-ms-asf",
+ [".atom"] = "application/atom+xml",
+ [".au"] = "audio/basic",
+ [".avi"] = "video/x-msvideo",
+ [".avif"] = "image/avif",
+ [".azw"] = "application/vnd.amazon.ebook",
+ [".bas"] = "text/plain",
+ [".bcpio"] = "application/x-bcpio",
+ [".bmp"] = "image/bmp",
+ [".br"] = "application/brotli",
+ [".bz"] = "application/x-bzip",
+ [".bz2"] = "application/x-bzip2",
+ [".c"] = "text/plain",
+ [".cab"] = "application/vnd.ms-cab-compressed",
+ [".calx"] = "application/vnd.ms-office.calx",
+ [".cat"] = "application/vnd.ms-pki.seccat",
+ [".cbor"] = "application/cbor",
+ [".cdf"] = "application/x-cdf",
+ [".cer"] = "application/x-x509-ca-cert",
+ [".cjs"] = "text/javascript",
+ [".class"] = "application/java-vm",
+ [".clp"] = "application/x-msclip",
+ [".cmx"] = "image/x-cmx",
+ [".cnf"] = "text/plain",
+ [".config"] = "text/plain",
+ [".cpio"] = "application/x-cpio",
+ [".cpp"] = "text/plain",
+ [".crd"] = "application/x-mscardfile",
+ [".crl"] = "application/pkix-crl",
+ [".crt"] = "application/x-x509-ca-cert",
+ [".cs"] = "text/x-csharp",
+ [".csproj"] = "text/xml",
+ [".csh"] = "application/x-csh",
+ [".css"] = "text/css",
+ [".csv"] = "text/csv",
+ [".cue"] = "application/x-cue",
+ [".cts"] = "text/typescript",
+ [".dart"] = "text/x-dart",
+ [".db"] = "application/x-sqlite3",
+ [".dbf"] = "application/x-dbf",
+ [".deb"] = "application/vnd.debian.binary-package",
+ [".dcr"] = "application/x-director",
+ [".der"] = "application/x-x509-ca-cert",
+ [".dib"] = "image/bmp",
+ [".dir"] = "application/x-director",
+ [".disco"] = "text/xml",
+ [".dmg"] = "application/x-apple-diskimage",
+ [".doc"] = "application/msword",
+ [".docm"] = "application/vnd.ms-word.document.macroEnabled.12",
+ [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ [".dot"] = "application/msword",
+ [".dotm"] = "application/vnd.ms-word.template.macroEnabled.12",
+ [".dotx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ [".dtd"] = "application/xml-dtd",
+ [".dvi"] = "application/x-dvi",
+ [".dvr-ms"] = "video/x-ms-dvr",
+ [".dwf"] = "drawing/x-dwf",
+ [".dxr"] = "application/x-director",
+ [".eml"] = "message/rfc822",
+ [".eot"] = "application/vnd.ms-fontobject",
+ [".eps"] = "application/postscript",
+ [".epub"] = "application/epub+zip",
+ [".etx"] = "text/x-setext",
+ [".fdf"] = "application/vnd.fdf",
+ [".flac"] = "audio/flac",
+ [".flv"] = "video/x-flv",
+ [".geojson"] = "application/geo+json",
+ [".gif"] = "image/gif",
+ [".glb"] = "model/gltf-binary",
+ [".gltf"] = "model/gltf+json",
+ [".go"] = "text/x-go",
+ [".gpx"] = "application/gpx+xml",
+ [".graphql"] = "application/graphql",
+ [".gtar"] = "application/x-gtar",
+ [".gz"] = "application/gzip",
+ [".h"] = "text/plain",
+ [".hdf"] = "application/x-hdf",
+ [".heic"] = "image/heic",
+ [".heif"] = "image/heif",
+ [".hhc"] = "application/x-oleobject",
+ [".hlp"] = "application/winhlp",
+ [".hqx"] = "application/mac-binhex40",
+ [".hta"] = "application/hta",
+ [".htc"] = "text/x-component",
+ [".htm"] = "text/html",
+ [".html"] = "text/html",
+ [".hwp"] = "application/x-hwp",
+ [".ical"] = "text/calendar",
+ [".icalendar"] = "text/calendar",
+ [".ico"] = "image/vnd.microsoft.icon",
+ [".ics"] = "text/calendar",
+ [".ief"] = "image/ief",
+ [".ifb"] = "text/calendar",
+ [".ini"] = "text/plain",
+ [".ins"] = "application/x-internet-signup",
+ [".iso"] = "application/x-iso9660-image",
+ [".isp"] = "application/x-internet-signup",
+ [".ivf"] = "video/x-ivf",
+ [".jar"] = "application/java-archive",
+ [".java"] = "text/x-java-source",
+ [".jfif"] = "image/jpeg",
+ [".jpe"] = "image/jpeg",
+ [".jpeg"] = "image/jpeg",
+ [".jpg"] = "image/jpeg",
+ [".js"] = "text/javascript",
+ [".json"] = "application/json",
+ [".jsonld"] = "application/ld+json",
+ [".jsonl"] = "application/x-ndjson",
+ [".jsx"] = "text/jsx",
+ [".jxl"] = "image/jxl",
+ [".kt"] = "text/x-kotlin",
+ [".kts"] = "text/x-kotlin",
+ [".key"] = "application/vnd.apple.keynote",
+ [".kml"] = "application/vnd.google-earth.kml+xml",
+ [".kmz"] = "application/vnd.google-earth.kmz",
+ [".latex"] = "application/x-latex",
+ [".less"] = "text/x-less",
+ [".lit"] = "application/x-ms-reader",
+ [".log"] = "text/plain",
+ [".lsf"] = "video/x-la-asf",
+ [".lwp"] = "application/vnd.lotus-wordpro",
+ [".lsx"] = "video/x-la-asf",
+ [".m13"] = "application/x-msmediaview",
+ [".m14"] = "application/x-msmediaview",
+ [".m1v"] = "video/mpeg",
+ [".m2t"] = "video/vnd.dlna.mpeg-tts",
+ [".m2ts"] = "video/vnd.dlna.mpeg-tts",
+ [".m3u"] = "audio/x-mpegurl",
+ [".m3u8"] = "application/vnd.apple.mpegurl",
+ [".m4a"] = "audio/mp4",
+ [".m4v"] = "video/mp4",
+ [".man"] = "application/x-troff-man",
+ [".manifest"] = "application/x-ms-manifest",
+ [".markdown"] = "text/markdown",
+ [".md"] = "text/markdown",
+ [".mdb"] = "application/x-msaccess",
+ [".mdx"] = "text/mdx",
+ [".me"] = "application/x-troff-me",
+ [".mht"] = "message/rfc822",
+ [".mhtml"] = "message/rfc822",
+ [".mid"] = "audio/midi",
+ [".midi"] = "audio/midi",
+ [".mjs"] = "text/javascript",
+ [".mka"] = "audio/x-matroska",
+ [".mkv"] = "video/x-matroska",
+ [".mmf"] = "application/x-smaf",
+ [".mno"] = "text/xml",
+ [".mny"] = "application/x-msmoney",
+ [".mov"] = "video/quicktime",
+ [".movie"] = "video/x-sgi-movie",
+ [".mp2"] = "video/mpeg",
+ [".mp3"] = "audio/mpeg",
+ [".mp4"] = "video/mp4",
+ [".mp4v"] = "video/mp4",
+ [".mpa"] = "video/mpeg",
+ [".mpd"] = "application/dash+xml",
+ [".mpe"] = "video/mpeg",
+ [".mpeg"] = "video/mpeg",
+ [".mpg"] = "video/mpeg",
+ [".mpkg"] = "application/vnd.apple.installer+xml",
+ [".mpp"] = "application/vnd.ms-project",
+ [".mpv2"] = "video/mpeg",
+ [".mts"] = "video/mp2t",
+ [".mvb"] = "application/x-msmediaview",
+ [".mvc"] = "application/x-miva-compiled",
+ [".nc"] = "application/x-netcdf",
+ [".nsc"] = "video/x-ms-asf",
+ [".ndjson"] = "application/x-ndjson",
+ [".numbers"] = "application/vnd.apple.numbers",
+ [".nupkg"] = "application/zip",
+ [".nws"] = "message/rfc822",
+ [".oda"] = "application/oda",
+ [".odc"] = "text/x-ms-odc",
+ [".odf"] = "application/vnd.oasis.opendocument.formula",
+ [".odg"] = "application/vnd.oasis.opendocument.graphics",
+ [".odp"] = "application/vnd.oasis.opendocument.presentation",
+ [".ods"] = "application/vnd.oasis.opendocument.spreadsheet",
+ [".odt"] = "application/vnd.oasis.opendocument.text",
+ [".oga"] = "audio/ogg",
+ [".ogg"] = "audio/ogg",
+ [".ogv"] = "video/ogg",
+ [".ogx"] = "application/ogg",
+ [".one"] = "application/onenote",
+ [".onea"] = "application/onenote",
+ [".onepkg"] = "application/onenote",
+ [".onetmp"] = "application/onenote",
+ [".onetoc"] = "application/onenote",
+ [".onetoc2"] = "application/onenote",
+ [".opus"] = "audio/opus",
+ [".osdx"] = "application/opensearchdescription+xml",
+ [".otf"] = "font/otf",
+ [".pages"] = "application/vnd.apple.pages",
+ [".parquet"] = "application/vnd.apache.parquet",
+ [".p10"] = "application/pkcs10",
+ [".p12"] = "application/x-pkcs12",
+ [".p7b"] = "application/x-pkcs7-certificates",
+ [".p7c"] = "application/pkcs7-mime",
+ [".p7m"] = "application/pkcs7-mime",
+ [".p7r"] = "application/x-pkcs7-certreqresp",
+ [".p7s"] = "application/pkcs7-signature",
+ [".pbm"] = "image/x-portable-bitmap",
+ [".pdf"] = "application/pdf",
+ [".pem"] = "application/x-pem-file",
+ [".pfx"] = "application/x-pkcs12",
+ [".pgm"] = "image/x-portable-graymap",
+ [".php"] = "application/x-httpd-php",
+ [".pko"] = "application/vnd.ms-pki.pko",
+ [".pl"] = "text/x-perl",
+ [".pm"] = "text/x-perl",
+ [".pma"] = "application/x-perfmon",
+ [".pmc"] = "application/x-perfmon",
+ [".pml"] = "application/x-perfmon",
+ [".pmr"] = "application/x-perfmon",
+ [".pmw"] = "application/x-perfmon",
+ [".png"] = "image/png",
+ [".pnm"] = "image/x-portable-anymap",
+ [".pnz"] = "image/png",
+ [".pot"] = "application/vnd.ms-powerpoint",
+ [".potm"] = "application/vnd.ms-powerpoint.template.macroEnabled.12",
+ [".potx"] = "application/vnd.openxmlformats-officedocument.presentationml.template",
+ [".ppam"] = "application/vnd.ms-powerpoint.addin.macroEnabled.12",
+ [".ppm"] = "image/x-portable-pixmap",
+ [".pps"] = "application/vnd.ms-powerpoint",
+ [".ppsm"] = "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
+ [".ppsx"] = "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
+ [".ppt"] = "application/vnd.ms-powerpoint",
+ [".pptm"] = "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
+ [".pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ [".proto"] = "text/x-protobuf",
+ [".ps"] = "application/postscript",
+ [".ps1"] = "application/powershell",
+ [".pub"] = "application/x-mspublisher",
+ [".py"] = "text/x-python",
+ [".pyi"] = "text/x-python",
+ [".pyw"] = "text/x-python",
+ [".qt"] = "video/quicktime",
+ [".qtl"] = "application/x-quicktimeplayer",
+ [".rar"] = "application/vnd.rar",
+ [".ras"] = "image/x-cmu-raster",
+ [".rgb"] = "image/x-rgb",
+ [".rmi"] = "audio/mid",
+ [".roff"] = "application/x-troff",
+ [".rpm"] = "application/x-rpm",
+ [".rs"] = "text/x-rust",
+ [".rss"] = "application/rss+xml",
+ [".rtf"] = "application/rtf",
+ [".rtx"] = "text/richtext",
+ [".sass"] = "text/x-sass",
+ [".scd"] = "application/x-msschedule",
+ [".scss"] = "text/x-scss",
+ [".sct"] = "text/scriptlet",
+ [".sda"] = "application/vnd.stardivision.draw",
+ [".sdd"] = "application/vnd.stardivision.impress",
+ [".sdw"] = "application/vnd.stardivision.writer",
+ [".setpay"] = "application/set-payment-initiation",
+ [".setreg"] = "application/set-registration-initiation",
+ [".sgml"] = "text/sgml",
+ [".sh"] = "application/x-sh",
+ [".shar"] = "application/x-shar",
+ [".sit"] = "application/x-stuffit",
+ [".slk"] = "text/vnd.sylk",
+ [".sln"] = "text/plain",
+ [".sldm"] = "application/vnd.ms-powerpoint.slide.macroEnabled.12",
+ [".sldx"] = "application/vnd.openxmlformats-officedocument.presentationml.slide",
+ [".snd"] = "audio/basic",
+ [".snupkg"] = "application/zip",
+ [".spc"] = "application/x-pkcs7-certificates",
+ [".spx"] = "audio/ogg",
+ [".sql"] = "application/sql",
+ [".sqlite"] = "application/x-sqlite3",
+ [".sqlite3"] = "application/x-sqlite3",
+ [".src"] = "application/x-wais-source",
+ [".sst"] = "application/vnd.ms-pki.certstore",
+ [".sti"] = "application/vnd.sun.xml.impress.template",
+ [".stl"] = "model/stl",
+ [".stw"] = "application/vnd.sun.xml.writer.template",
+ [".sv4cpio"] = "application/x-sv4cpio",
+ [".sv4crc"] = "application/x-sv4crc",
+ [".svelte"] = "text/svelte",
+ [".svg"] = "image/svg+xml",
+ [".svgz"] = "image/svg+xml",
+ [".sxg"] = "application/vnd.sun.xml.writer.global",
+ [".sxi"] = "application/vnd.sun.xml.impress",
+ [".sxw"] = "application/vnd.sun.xml.writer",
+ [".swf"] = "application/x-shockwave-flash",
+ [".sylk"] = "text/vnd.sylk",
+ [".swift"] = "text/x-swift",
+ [".tar"] = "application/x-tar",
+ [".tcl"] = "application/x-tcl",
+ [".tex"] = "application/x-tex",
+ [".texi"] = "application/x-texinfo",
+ [".texinfo"] = "application/x-texinfo",
+ [".tgz"] = "application/x-compressed",
+ [".thmx"] = "application/vnd.ms-officetheme",
+ [".tif"] = "image/tiff",
+ [".tiff"] = "image/tiff",
+ [".toml"] = "application/toml",
+ [".tr"] = "application/x-troff",
+ [".trm"] = "application/x-msterminal",
+ [".ts"] = "text/typescript",
+ [".tsv"] = "text/tab-separated-values",
+ [".tsx"] = "text/tsx",
+ [".ttc"] = "font/collection",
+ [".ttf"] = "font/ttf",
+ [".tts"] = "video/vnd.dlna.mpeg-tts",
+ [".txt"] = "text/plain",
+ [".ustar"] = "application/x-ustar",
+ [".vbs"] = "text/vbscript",
+ [".vcf"] = "text/vcard",
+ [".vcs"] = "text/x-vcalendar",
+ [".vdx"] = "application/vnd.ms-visio.viewer",
+ [".vml"] = "text/xml",
+ [".vsd"] = "application/vnd.visio",
+ [".vss"] = "application/vnd.visio",
+ [".vst"] = "application/vnd.visio",
+ [".vsto"] = "application/x-ms-vsto",
+ [".vsw"] = "application/vnd.visio",
+ [".vsx"] = "application/vnd.visio",
+ [".vtt"] = "text/vtt",
+ [".vtx"] = "application/vnd.visio",
+ [".vue"] = "text/vue",
+ [".wasm"] = "application/wasm",
+ [".wav"] = "audio/wav",
+ [".wax"] = "audio/x-ms-wax",
+ [".wbmp"] = "image/vnd.wap.wbmp",
+ [".wcm"] = "application/vnd.ms-works",
+ [".wdb"] = "application/vnd.ms-works",
+ [".weba"] = "audio/webm",
+ [".webm"] = "video/webm",
+ [".webmanifest"] = "application/manifest+json",
+ [".webp"] = "image/webp",
+ [".wks"] = "application/vnd.ms-works",
+ [".wm"] = "video/x-ms-wm",
+ [".wma"] = "audio/x-ms-wma",
+ [".wmd"] = "application/x-ms-wmd",
+ [".wmf"] = "application/x-msmetafile",
+ [".wmp"] = "video/x-ms-wmp",
+ [".wmv"] = "video/x-ms-wmv",
+ [".wmx"] = "video/x-ms-wmx",
+ [".wmz"] = "application/x-ms-wmz",
+ [".woff"] = "font/woff",
+ [".woff2"] = "font/woff2",
+ [".wpd"] = "application/vnd.wordperfect",
+ [".wps"] = "application/vnd.ms-works",
+ [".wri"] = "application/x-mswrite",
+ [".wsdl"] = "text/xml",
+ [".wtv"] = "video/x-ms-wtv",
+ [".wvx"] = "video/x-ms-wvx",
+ [".xaml"] = "application/xaml+xml",
+ [".xbm"] = "image/x-xbitmap",
+ [".xdr"] = "text/plain",
+ [".xht"] = "application/xhtml+xml",
+ [".xhtml"] = "application/xhtml+xml",
+ [".xla"] = "application/vnd.ms-excel",
+ [".xlam"] = "application/vnd.ms-excel.addin.macroEnabled.12",
+ [".xlc"] = "application/vnd.ms-excel",
+ [".xlm"] = "application/vnd.ms-excel",
+ [".xls"] = "application/vnd.ms-excel",
+ [".xlsb"] = "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
+ [".xlsm"] = "application/vnd.ms-excel.sheet.macroEnabled.12",
+ [".xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ [".xlt"] = "application/vnd.ms-excel",
+ [".xltm"] = "application/vnd.ms-excel.template.macroEnabled.12",
+ [".xltx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+ [".xlw"] = "application/vnd.ms-excel",
+ [".xml"] = "application/xml",
+ [".xpm"] = "image/x-xpixmap",
+ [".xps"] = "application/vnd.ms-xpsdocument",
+ [".xsd"] = "text/xml",
+ [".xsf"] = "text/xml",
+ [".xsl"] = "text/xml",
+ [".xslt"] = "text/xml",
+ [".xul"] = "application/vnd.mozilla.xul+xml",
+ [".xwd"] = "image/x-xwindowdump",
+ [".yaml"] = "application/yaml",
+ [".yml"] = "application/yaml",
+ [".z"] = "application/x-compress",
+ [".zip"] = "application/zip",
+ [".zst"] = "application/zstd",
+ };
+
+ private static readonly Dictionary s_mediaTypeToExtension = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["application/atom+xml"] = ".atom",
+ ["application/brotli"] = ".br",
+ ["application/cbor"] = ".cbor",
+ ["application/dash+xml"] = ".mpd",
+ ["application/epub+zip"] = ".epub",
+ ["application/geo+json"] = ".geojson",
+ ["application/graphql"] = ".graphql",
+ ["application/gpx+xml"] = ".gpx",
+ ["application/gzip"] = ".gz",
+ ["application/hta"] = ".hta",
+ ["application/java-archive"] = ".jar",
+ ["application/java-vm"] = ".class",
+ ["application/json"] = ".json",
+ ["application/ld+json"] = ".jsonld",
+ ["application/mac-binhex40"] = ".hqx",
+ ["application/manifest+json"] = ".webmanifest",
+ ["application/msaccess"] = ".mdb",
+ ["application/msword"] = ".doc",
+ ["application/oda"] = ".oda",
+ ["application/ogg"] = ".ogx",
+ ["application/onenote"] = ".one",
+ ["application/opensearchdescription+xml"] = ".osdx",
+ ["application/pdf"] = ".pdf",
+ ["application/pkcs10"] = ".p10",
+ ["application/pkcs7-mime"] = ".p7c",
+ ["application/pkcs7-signature"] = ".p7s",
+ ["application/pkix-crl"] = ".crl",
+ ["application/powershell"] = ".ps1",
+ ["application/postscript"] = ".ps",
+ ["application/rtf"] = ".rtf",
+ ["application/rss+xml"] = ".rss",
+ ["application/set-payment-initiation"] = ".setpay",
+ ["application/set-registration-initiation"] = ".setreg",
+ ["application/sql"] = ".sql",
+ ["application/toml"] = ".toml",
+ ["application/vnd.amazon.ebook"] = ".azw",
+ ["application/vnd.android.package-archive"] = ".apk",
+ ["application/vnd.apache.parquet"] = ".parquet",
+ ["application/vnd.apple.installer+xml"] = ".mpkg",
+ ["application/vnd.apple.keynote"] = ".key",
+ ["application/vnd.apple.mpegurl"] = ".m3u8",
+ ["application/vnd.apple.numbers"] = ".numbers",
+ ["application/vnd.apple.pages"] = ".pages",
+ ["application/vnd.debian.binary-package"] = ".deb",
+ ["application/vnd.fdf"] = ".fdf",
+ ["application/vnd.google-earth.kml+xml"] = ".kml",
+ ["application/vnd.google-earth.kmz"] = ".kmz",
+ ["application/vnd.lotus-wordpro"] = ".lwp",
+ ["application/vnd.mozilla.xul+xml"] = ".xul",
+ ["application/vnd.ms-cab-compressed"] = ".cab",
+ ["application/vnd.ms-excel"] = ".xls",
+ ["application/vnd.ms-excel.addin.macroEnabled.12"] = ".xlam",
+ ["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] = ".xlsb",
+ ["application/vnd.ms-excel.sheet.macroEnabled.12"] = ".xlsm",
+ ["application/vnd.ms-excel.template.macroEnabled.12"] = ".xltm",
+ ["application/vnd.ms-fontobject"] = ".eot",
+ ["application/vnd.ms-office.calx"] = ".calx",
+ ["application/vnd.ms-officetheme"] = ".thmx",
+ ["application/vnd.ms-pki.certstore"] = ".sst",
+ ["application/vnd.ms-pki.pko"] = ".pko",
+ ["application/vnd.ms-pki.seccat"] = ".cat",
+ ["application/vnd.ms-powerpoint"] = ".ppt",
+ ["application/vnd.ms-powerpoint.addin.macroEnabled.12"] = ".ppam",
+ ["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] = ".pptm",
+ ["application/vnd.ms-powerpoint.slide.macroEnabled.12"] = ".sldm",
+ ["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] = ".ppsm",
+ ["application/vnd.ms-powerpoint.template.macroEnabled.12"] = ".potm",
+ ["application/vnd.ms-project"] = ".mpp",
+ ["application/vnd.ms-visio.viewer"] = ".vdx",
+ ["application/vnd.ms-word.document.macroEnabled.12"] = ".docm",
+ ["application/vnd.ms-word.template.macroEnabled.12"] = ".dotm",
+ ["application/vnd.ms-works"] = ".wcm",
+ ["application/vnd.ms-xpsdocument"] = ".xps",
+ ["application/vnd.oasis.opendocument.formula"] = ".odf",
+ ["application/vnd.oasis.opendocument.graphics"] = ".odg",
+ ["application/vnd.oasis.opendocument.presentation"] = ".odp",
+ ["application/vnd.oasis.opendocument.spreadsheet"] = ".ods",
+ ["application/vnd.oasis.opendocument.text"] = ".odt",
+ ["application/vnd.openxmlformats-officedocument.presentationml.presentation"] = ".pptx",
+ ["application/vnd.openxmlformats-officedocument.presentationml.slide"] = ".sldx",
+ ["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = ".ppsx",
+ ["application/vnd.openxmlformats-officedocument.presentationml.template"] = ".potx",
+ ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = ".xlsx",
+ ["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = ".xltx",
+ ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = ".docx",
+ ["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = ".dotx",
+ ["application/vnd.rar"] = ".rar",
+ ["application/vnd.stardivision.draw"] = ".sda",
+ ["application/vnd.stardivision.impress"] = ".sdd",
+ ["application/vnd.stardivision.writer"] = ".sdw",
+ ["application/vnd.sun.xml.impress"] = ".sxi",
+ ["application/vnd.sun.xml.impress.template"] = ".sti",
+ ["application/vnd.sun.xml.writer"] = ".sxw",
+ ["application/vnd.sun.xml.writer.global"] = ".sxg",
+ ["application/vnd.sun.xml.writer.template"] = ".stw",
+ ["application/vnd.visio"] = ".vsd",
+ ["application/vnd.wordperfect"] = ".wpd",
+ ["application/wasm"] = ".wasm",
+ ["application/winhlp"] = ".hlp",
+ ["application/x-7z-compressed"] = ".7z",
+ ["application/x-abiword"] = ".abw",
+ ["application/x-apple-diskimage"] = ".dmg",
+ ["application/xaml+xml"] = ".xaml",
+ ["application/x-bcpio"] = ".bcpio",
+ ["application/x-dbf"] = ".dbf",
+ ["application/x-brotli"] = ".br",
+ ["application/x-bzip"] = ".bz",
+ ["application/x-bzip2"] = ".bz2",
+ ["application/x-cdf"] = ".cdf",
+ ["application/x-compress"] = ".z",
+ ["application/x-compressed"] = ".tgz",
+ ["application/x-cpio"] = ".cpio",
+ ["application/x-cue"] = ".cue",
+ ["application/x-csh"] = ".csh",
+ ["application/x-director"] = ".dcr",
+ ["application/x-dvi"] = ".dvi",
+ ["application/x-freearc"] = ".arc",
+ ["application/x-gtar"] = ".gtar",
+ ["application/x-hdf"] = ".hdf",
+ ["application/x-hwp"] = ".hwp",
+ ["application/x-iso9660-image"] = ".iso",
+ ["application/xhtml+xml"] = ".xhtml",
+ ["application/x-httpd-php"] = ".php",
+ ["application/x-internet-signup"] = ".ins",
+ ["application/x-latex"] = ".latex",
+ ["application/x-miva-compiled"] = ".mvc",
+ ["application/xml"] = ".xml",
+ ["application/xml-dtd"] = ".dtd",
+ ["application/x-msaccess"] = ".mdb",
+ ["application/x-ms-application"] = ".application",
+ ["application/x-mscardfile"] = ".crd",
+ ["application/x-msclip"] = ".clp",
+ ["application/x-ms-manifest"] = ".manifest",
+ ["application/x-msmediaview"] = ".m13",
+ ["application/x-msmetafile"] = ".wmf",
+ ["application/x-msmoney"] = ".mny",
+ ["application/x-mspublisher"] = ".pub",
+ ["application/x-ms-reader"] = ".lit",
+ ["application/x-msschedule"] = ".scd",
+ ["application/x-msterminal"] = ".trm",
+ ["application/x-ms-vsto"] = ".vsto",
+ ["application/x-ms-wmd"] = ".wmd",
+ ["application/x-ms-wmz"] = ".wmz",
+ ["application/x-mswrite"] = ".wri",
+ ["application/x-netcdf"] = ".nc",
+ ["application/x-ndjson"] = ".jsonl",
+ ["application/x-oleobject"] = ".hhc",
+ ["application/x-perfmon"] = ".pma",
+ ["application/x-pem-file"] = ".pem",
+ ["application/x-pkcs12"] = ".p12",
+ ["application/x-pkcs7-certificates"] = ".p7b",
+ ["application/x-pkcs7-certreqresp"] = ".p7r",
+ ["application/x-quicktimeplayer"] = ".qtl",
+ ["application/x-rpm"] = ".rpm",
+ ["application/x-sh"] = ".sh",
+ ["application/x-shar"] = ".shar",
+ ["application/x-shockwave-flash"] = ".swf",
+ ["application/x-smaf"] = ".mmf",
+ ["application/x-sqlite3"] = ".sqlite",
+ ["application/x-stuffit"] = ".sit",
+ ["application/x-sv4cpio"] = ".sv4cpio",
+ ["application/x-sv4crc"] = ".sv4crc",
+ ["application/x-tar"] = ".tar",
+ ["application/x-tcl"] = ".tcl",
+ ["application/x-tex"] = ".tex",
+ ["application/x-texinfo"] = ".texi",
+ ["application/x-troff"] = ".roff",
+ ["application/x-troff-man"] = ".man",
+ ["application/x-troff-me"] = ".me",
+ ["application/x-ustar"] = ".ustar",
+ ["application/x-wais-source"] = ".src",
+ ["application/x-x509-ca-cert"] = ".crt",
+ ["application/yaml"] = ".yaml",
+ ["application/zip"] = ".zip",
+ ["application/zstd"] = ".zst",
+ ["audio/aac"] = ".aac",
+ ["audio/aifc"] = ".aifc",
+ ["audio/aiff"] = ".aiff",
+ ["audio/basic"] = ".au",
+ ["audio/flac"] = ".flac",
+ ["audio/mid"] = ".mid",
+ ["audio/midi"] = ".mid",
+ ["audio/mp4"] = ".m4a",
+ ["audio/mpeg"] = ".mp3",
+ ["audio/ogg"] = ".oga",
+ ["audio/opus"] = ".opus",
+ ["audio/vnd.dlna.adts"] = ".adt",
+ ["audio/wav"] = ".wav",
+ ["audio/webm"] = ".weba",
+ ["audio/x-aiff"] = ".aif",
+ ["audio/x-mpegurl"] = ".m3u",
+ ["audio/x-matroska"] = ".mka",
+ ["audio/x-ms-wax"] = ".wax",
+ ["audio/x-ms-wma"] = ".wma",
+ ["drawing/x-dwf"] = ".dwf",
+ ["font/collection"] = ".ttc",
+ ["font/otf"] = ".otf",
+ ["font/ttf"] = ".ttf",
+ ["font/woff"] = ".woff",
+ ["font/woff2"] = ".woff2",
+ ["image/apng"] = ".apng",
+ ["image/avif"] = ".avif",
+ ["image/bmp"] = ".bmp",
+ ["image/gif"] = ".gif",
+ ["image/heic"] = ".heic",
+ ["image/heif"] = ".heif",
+ ["image/ief"] = ".ief",
+ ["image/jpeg"] = ".jpg",
+ ["image/jxl"] = ".jxl",
+ ["image/png"] = ".png",
+ ["image/svg+xml"] = ".svg",
+ ["image/tiff"] = ".tif",
+ ["image/vnd.microsoft.icon"] = ".ico",
+ ["image/vnd.wap.wbmp"] = ".wbmp",
+ ["image/webp"] = ".webp",
+ ["image/x-cmu-raster"] = ".ras",
+ ["image/x-cmx"] = ".cmx",
+ ["image/x-icon"] = ".ico",
+ ["image/x-portable-anymap"] = ".pnm",
+ ["image/x-portable-bitmap"] = ".pbm",
+ ["image/x-portable-graymap"] = ".pgm",
+ ["image/x-portable-pixmap"] = ".ppm",
+ ["image/x-rgb"] = ".rgb",
+ ["image/x-xbitmap"] = ".xbm",
+ ["image/x-xpixmap"] = ".xpm",
+ ["image/x-xwindowdump"] = ".xwd",
+ ["message/rfc822"] = ".eml",
+ ["model/gltf+json"] = ".gltf",
+ ["model/gltf-binary"] = ".glb",
+ ["model/stl"] = ".stl",
+ ["text/cache-manifest"] = ".appcache",
+ ["text/calendar"] = ".ics",
+ ["text/css"] = ".css",
+ ["text/csv"] = ".csv",
+ ["text/html"] = ".html",
+ ["text/javascript"] = ".js",
+ ["text/jsx"] = ".jsx",
+ ["text/markdown"] = ".md",
+ ["text/mdx"] = ".mdx",
+ ["text/plain"] = ".txt",
+ ["text/richtext"] = ".rtx",
+ ["text/scriptlet"] = ".sct",
+ ["text/sgml"] = ".sgml",
+ ["text/svelte"] = ".svelte",
+ ["text/tab-separated-values"] = ".tsv",
+ ["text/tsx"] = ".tsx",
+ ["text/typescript"] = ".ts",
+ ["text/vbscript"] = ".vbs",
+ ["text/vcard"] = ".vcf",
+ ["text/vnd.sylk"] = ".sylk",
+ ["text/vtt"] = ".vtt",
+ ["text/x-vcalendar"] = ".vcs",
+ ["text/vue"] = ".vue",
+ ["text/x-component"] = ".htc",
+ ["text/x-csharp"] = ".cs",
+ ["text/x-dart"] = ".dart",
+ ["text/x-go"] = ".go",
+ ["text/x-java-source"] = ".java",
+ ["text/x-kotlin"] = ".kt",
+ ["text/x-less"] = ".less",
+ ["text/xml"] = ".xml",
+ ["text/x-ms-odc"] = ".odc",
+ ["text/yaml"] = ".yaml",
+ ["text/x-perl"] = ".pl",
+ ["text/x-protobuf"] = ".proto",
+ ["text/x-python"] = ".py",
+ ["text/x-rust"] = ".rs",
+ ["text/x-sass"] = ".sass",
+ ["text/x-scss"] = ".scss",
+ ["text/x-setext"] = ".etx",
+ ["text/x-swift"] = ".swift",
+ ["video/3gpp"] = ".3gp",
+ ["video/3gpp2"] = ".3g2",
+ ["video/mp2t"] = ".mts",
+ ["video/mp4"] = ".mp4",
+ ["video/mpeg"] = ".mpeg",
+ ["video/ogg"] = ".ogv",
+ ["video/quicktime"] = ".mov",
+ ["video/vnd.dlna.mpeg-tts"] = ".tts",
+ ["video/webm"] = ".webm",
+ ["video/x-flv"] = ".flv",
+ ["video/x-ivf"] = ".ivf",
+ ["video/x-la-asf"] = ".lsf",
+ ["video/x-matroska"] = ".mkv",
+ ["video/x-ms-asf"] = ".asf",
+ ["video/x-ms-dvr"] = ".dvr-ms",
+ ["video/x-msvideo"] = ".avi",
+ ["video/x-ms-wm"] = ".wm",
+ ["video/x-ms-wmp"] = ".wmp",
+ ["video/x-ms-wmv"] = ".wmv",
+ ["video/x-ms-wmx"] = ".wmx",
+ ["video/x-ms-wtv"] = ".wtv",
+ ["video/x-ms-wvx"] = ".wvx",
+ ["video/x-sgi-movie"] = ".movie",
+ };
+
+ ///
+ /// Gets the media type (MIME type) for the specified file path or extension.
+ ///
+ /// A file path or extension (with or without leading period).
+ /// The media type associated with the extension, or if no mapping exists.
+ public static string? GetMediaType(string? pathOrExtension)
+ {
+ if (string.IsNullOrEmpty(pathOrExtension))
+ {
+ return null;
+ }
+
+ string extension = IO.Path.GetExtension(pathOrExtension);
+
+ if (string.IsNullOrEmpty(extension))
+ {
+ // The input might be an extension itself (e.g., ".pdf" or "pdf")
+ extension = pathOrExtension!;
+ if (extension[0] != '.')
+ {
+ extension = "." + extension;
+ }
+ }
+
+ _ = s_extensionToMediaType.TryGetValue(extension, out string? result);
+ return result;
+ }
+
+ ///
+ /// Gets the file extension for the specified media type (MIME type).
+ ///
+ /// The media type (e.g. "application/pdf").
+ /// The file extension (with leading period) associated with the media type, or if no mapping exists.
+ public static string? GetExtension(string? mediaType)
+ {
+ if (string.IsNullOrEmpty(mediaType))
+ {
+ return null;
+ }
+
+ // Remove any parameters from the media type (e.g., "text/html; charset=utf-8")
+#pragma warning disable CA1307 // Specify StringComparison for clarity
+ int semicolonIndex = mediaType!.IndexOf(';');
+#pragma warning restore CA1307
+ if (semicolonIndex >= 0)
+ {
+ mediaType = mediaType.Substring(0, semicolonIndex).Trim();
+ }
+
+ _ = s_mediaTypeToExtension.TryGetValue(mediaType, out string? value);
+ return value;
+ }
+}
diff --git a/src/LegacySupport/MediaTypeMap/README.md b/src/LegacySupport/MediaTypeMap/README.md
new file mode 100644
index 00000000000..f4290678f54
--- /dev/null
+++ b/src/LegacySupport/MediaTypeMap/README.md
@@ -0,0 +1,9 @@
+# About MediaTypeMap
+
+This folder contains a polyfill for `System.Net.Mime.MediaTypeMap` which was added in .NET 10.
+It provides methods for mapping between file extensions and media types (MIME types).
+
+The implementation is a simplified version of the original that works on older frameworks
+that don't have the `AlternateLookup` dictionary feature.
+
+See: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Mail/src/System/Net/Mime/MediaTypeMap.cs
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
index fbc05e14405..45475fb930a 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
@@ -9,11 +9,15 @@
#endif
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.Text;
+using System.IO;
+using System.Net.Mime;
#if !NET
using System.Runtime.InteropServices;
#endif
+using System.Text;
using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
#pragma warning disable IDE0032 // Use auto property
@@ -124,6 +128,127 @@ public DataContent(ReadOnlyMemory data, string mediaType)
_data = data;
}
+ ///
+ /// Loads a from a file path asynchronously.
+ ///
+ ///
+ /// The absolute or relative file path to load the data from. Relative file paths are relative to the current working directory.
+ ///
+ ///
+ /// The media type (also known as MIME type) represented by the content. If not provided,
+ /// it will be inferred from the file extension. If it cannot be inferred, "application/octet-stream" is used.
+ ///
+ /// The to monitor for cancellation requests.
+ /// A containing the file data with the inferred or specified media type and name.
+ /// is .
+ /// is empty.
+ public static async ValueTask LoadFromAsync(string path, string? mediaType = null, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNullOrEmpty(path);
+
+ using FileStream fileStream = new(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1, useAsync: true);
+ return await LoadFromAsync(fileStream, mediaType, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Loads a from a stream asynchronously.
+ ///
+ /// The stream to load the data from.
+ ///
+ /// The media type (also known as MIME type) represented by the content. If not provided and
+ /// the stream is a , it will be inferred from the file extension.
+ /// If it cannot be inferred, "application/octet-stream" is used.
+ ///
+ /// The to monitor for cancellation requests.
+ /// A containing the stream data with the inferred or specified media type and name.
+ /// is .
+ public static async ValueTask LoadFromAsync(Stream stream, string? mediaType = null, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(stream);
+
+ string? name = null;
+
+ // If the stream is a FileStream, try to infer media type and name from its path.
+ if (stream is FileStream fileStream)
+ {
+ string? filePath = fileStream.Name;
+ string? fileName = Path.GetFileName(filePath);
+ if (!string.IsNullOrEmpty(fileName))
+ {
+ name = fileName;
+ }
+
+ mediaType ??= MediaTypeMap.GetMediaType(filePath);
+ }
+
+ // Fall back to default media type if still not set.
+ mediaType ??= DefaultMediaType;
+
+ // Read the stream contents
+ MemoryStream memoryStream = stream.CanSeek ? new((int)Math.Min(stream.Length, int.MaxValue)) : new();
+ await stream.CopyToAsync(memoryStream,
+#if !NET
+ 80 * 1024, // same as the default buffer size
+#endif
+ cancellationToken).ConfigureAwait(false);
+
+ return new DataContent(new ReadOnlyMemory(memoryStream.GetBuffer(), 0, (int)memoryStream.Length), mediaType)
+ {
+ Name = name
+ };
+ }
+
+ ///
+ /// Saves the data content to a file asynchronously.
+ ///
+ ///
+ /// The absolute or relative file path to save the data to. If the path is to an existing directory, the file name will be inferred
+ /// from the property, or a random name will be used with an extension based on the , if possible.
+ ///
+ /// The to monitor for cancellation requests.
+ /// The actual path where the data was saved, which may include an inferred file name and/or extension.
+ /// is .
+ public async ValueTask SaveToAsync(string path, CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(path);
+
+ // If path is a directory, infer the file name from the Name property if available,
+ // or use a random name along with an extension inferred from the media type.
+ // If the path is empty, treat it as the current directory.
+ if (path.Length == 0 || Directory.Exists(path))
+ {
+ string? name = null;
+
+ if (Name is not null)
+ {
+ name = Path.GetFileName(Name);
+ }
+
+ if (string.IsNullOrEmpty(name))
+ {
+ name = $"{Guid.NewGuid():N}{MediaTypeMap.GetExtension(MediaType)}";
+ }
+
+ path = path.Length == 0 ? name! : Path.Combine(path, name);
+ }
+
+ // Write the data to the file.
+ using FileStream fileStream = new(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 1, useAsync: true);
+
+#if NET
+ await fileStream.WriteAsync(Data, cancellationToken).ConfigureAwait(false);
+#else
+ if (!MemoryMarshal.TryGetArray(Data, out ArraySegment array))
+ {
+ array = new(Data.ToArray());
+ }
+
+ await fileStream.WriteAsync(array.Array, array.Offset, array.Count, cancellationToken).ConfigureAwait(false);
+#endif
+
+ return fileStream.Name;
+ }
+
///
/// Determines whether the 's top-level type matches the specified .
///
@@ -189,8 +314,14 @@ public string Uri
/// Gets or sets an optional name associated with the data.
///
+ ///
/// A service might use this name as part of citations or to help infer the type of data
/// being represented based on a file extension.
+ ///
+ ///
+ /// When using , if the path provided is a directory,
+ /// may be used as part of the output file's name.
+ ///
///
public string? Name { get; set; }
@@ -256,4 +387,7 @@ private string DebuggerDisplay
$"Data = {uri.Substring(0, MaxLength)}...";
}
}
+
+ /// The default media type for unknown file extensions.
+ private const string DefaultMediaType = "application/octet-stream";
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs
index fe976231635..6bf2086d133 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Image/ImageGeneratorExtensions.cs
@@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
@@ -15,18 +15,6 @@ namespace Microsoft.Extensions.AI;
[Experimental("MEAI001")]
public static class ImageGeneratorExtensions
{
- private static readonly Dictionary _extensionToMimeType = new(StringComparer.OrdinalIgnoreCase)
- {
- [".png"] = "image/png",
- [".jpg"] = "image/jpeg",
- [".jpeg"] = "image/jpeg",
- [".webp"] = "image/webp",
- [".gif"] = "image/gif",
- [".bmp"] = "image/bmp",
- [".tiff"] = "image/tiff",
- [".tif"] = "image/tiff",
- };
-
/// Asks the for an object of type .
/// The type of the object to be retrieved.
/// The generator.
@@ -203,13 +191,6 @@ public static Task EditImageAsync(
/// The inferred media type.
private static string GetMediaTypeFromFileName(string fileName)
{
- string extension = Path.GetExtension(fileName);
-
- if (_extensionToMimeType.TryGetValue(extension, out string? mediaType))
- {
- return mediaType;
- }
-
- return "image/png"; // Default to PNG if unknown extension
+ return MediaTypeMap.GetMediaType(fileName) ?? "image/png"; // Default to PNG if unknown extension
}
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
index 72c354ccb97..d2c3ccc3449 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj
@@ -22,6 +22,8 @@
true
+ true
+ true
true
true
true
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
index e401502d82b..6362fcaa00e 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json
@@ -1464,6 +1464,18 @@
{
"Member": "bool Microsoft.Extensions.AI.DataContent.HasTopLevelMediaType(string topLevelType);",
"Stage": "Stable"
+ },
+ {
+ "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DataContent.LoadFromAsync(string path, string? mediaType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
+ "Stage": "Stable"
+ },
+ {
+ "Member": "static System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DataContent.LoadFromAsync(System.IO.Stream stream, string? mediaType = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
+ "Stage": "Stable"
+ },
+ {
+ "Member": "System.Threading.Tasks.ValueTask Microsoft.Extensions.AI.DataContent.SaveToAsync(string path, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));",
+ "Stage": "Stable"
}
],
"Properties": [
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
index bf0841290e8..fe25368e83b 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/Microsoft.Extensions.AI.OpenAI.csproj
@@ -26,6 +26,7 @@
true
true
+ true
true
true
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
index 36d8677e70a..f558a148e80 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
@@ -5,6 +5,7 @@
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -218,15 +219,7 @@ internal static FunctionCallContent ParseCallContent(BinaryData utf8json, string
/// Gets a media type for an image based on the file extension in the provided URI.
internal static string ImageUriToMediaType(Uri uri)
{
- string absoluteUri = uri.AbsoluteUri;
- return
- absoluteUri.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ? "image/png" :
- absoluteUri.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) ? "image/jpeg" :
- absoluteUri.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) ? "image/jpeg" :
- absoluteUri.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) ? "image/gif" :
- absoluteUri.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) ? "image/bmp" :
- absoluteUri.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) ? "image/webp" :
- "image/*";
+ return MediaTypeMap.GetMediaType(uri.AbsoluteUri) ?? "image/*";
}
/// Sets $.model in to if not already set.
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs
index a51454d532c..631b710b640 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIImageGenerator.cs
@@ -6,6 +6,7 @@
using System.Drawing;
using System.IO;
using System.Linq;
+using System.Net.Mime;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
@@ -23,16 +24,6 @@ namespace Microsoft.Extensions.AI;
/// Represents an for an OpenAI or .
internal sealed class OpenAIImageGenerator : IImageGenerator
{
- private static readonly Dictionary _mimeTypeToExtension = new(StringComparer.OrdinalIgnoreCase)
- {
- ["image/png"] = ".png",
- ["image/jpeg"] = ".jpg",
- ["image/webp"] = ".webp",
- ["image/gif"] = ".gif",
- ["image/bmp"] = ".bmp",
- ["image/tiff"] = ".tiff",
- };
-
/// Metadata about the client.
private readonly ImageGeneratorMetadata _metadata;
@@ -72,20 +63,9 @@ public async Task GenerateAsync(ImageGenerationRequest
imageStream = MemoryMarshal.TryGetArray(dataContent.Data, out var array) ?
new MemoryStream(array.Array!, array.Offset, array.Count) :
new MemoryStream(dataContent.Data.ToArray());
- fileName = dataContent.Name;
-
- if (fileName is null)
- {
- // If no file name is provided, use the default based on the content type.
- if (dataContent.MediaType is not null && _mimeTypeToExtension.TryGetValue(dataContent.MediaType, out var extension))
- {
- fileName = $"image{extension}";
- }
- else
- {
- fileName = "image.png"; // Default to PNG if no content type is available.
- }
- }
+ fileName =
+ dataContent.Name ??
+ $"{Guid.NewGuid():N}{MediaTypeMap.GetExtension(dataContent.MediaType) ?? ".png"}"; // Default to PNG if no content type is available.
}
GeneratedImageCollection editResult = await _imageClient.GenerateImageEditsAsync(
diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/IngestionDocumentReader.cs b/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/IngestionDocumentReader.cs
index 8bdb651321a..cffd56f3c1c 100644
--- a/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/IngestionDocumentReader.cs
+++ b/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/IngestionDocumentReader.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
@@ -55,88 +56,6 @@ public virtual async Task ReadAsync(FileInfo source, string i
/// A task representing the asynchronous read operation.
public abstract Task ReadAsync(Stream source, string identifier, string mediaType, CancellationToken cancellationToken = default);
- private static string GetMediaType(FileInfo source)
- => source.Extension switch
- {
- ".123" => "application/vnd.lotus-1-2-3",
- ".602" => "application/x-t602",
- ".abw" => "application/x-abiword",
- ".bmp" => "image/bmp",
- ".cgm" => "image/cgm",
- ".csv" => "text/csv",
- ".cwk" => "application/x-cwk",
- ".dbf" => "application/vnd.dbf",
- ".dif" => "application/x-dif",
- ".doc" => "application/msword",
- ".docm" => "application/vnd.ms-word.document.macroEnabled.12",
- ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ".dot" => "application/msword",
- ".dotm" => "application/vnd.ms-word.template.macroEnabled.12",
- ".dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
- ".epub" => "application/epub+zip",
- ".et" => "application/vnd.ms-excel",
- ".eth" => "application/ethos",
- ".fods" => "application/vnd.oasis.opendocument.spreadsheet",
- ".gif" => "image/gif",
- ".htm" => "text/html",
- ".html" => "text/html",
- ".hwp" => "application/x-hwp",
- ".jpeg" => "image/jpeg",
- ".jpg" => "image/jpeg",
- ".key" => "application/x-iwork-keynote-sffkey",
- ".lwp" => "application/vnd.lotus-wordpro",
- ".mcw" => "application/macwriteii",
- ".mw" => "application/macwriteii",
- ".numbers" => "application/x-iwork-numbers-sffnumbers",
- ".ods" => "application/vnd.oasis.opendocument.spreadsheet",
- ".pages" => "application/x-iwork-pages-sffpages",
- ".pbd" => "application/x-pagemaker",
- ".pdf" => "application/pdf",
- ".png" => "image/png",
- ".pot" => "application/vnd.ms-powerpoint",
- ".potm" => "application/vnd.ms-powerpoint.template.macroEnabled.12",
- ".potx" => "application/vnd.openxmlformats-officedocument.presentationml.template",
- ".ppt" => "application/vnd.ms-powerpoint",
- ".pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
- ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- ".prn" => "application/x-prn",
- ".qpw" => "application/x-quattro-pro",
- ".rtf" => "application/rtf",
- ".sda" => "application/vnd.stardivision.draw",
- ".sdd" => "application/vnd.stardivision.impress",
- ".sdp" => "application/sdp",
- ".sdw" => "application/vnd.stardivision.writer",
- ".sgl" => "application/vnd.stardivision.writer",
- ".slk" => "text/vnd.sylk",
- ".sti" => "application/vnd.sun.xml.impress.template",
- ".stw" => "application/vnd.sun.xml.writer.template",
- ".svg" => "image/svg+xml",
- ".sxg" => "application/vnd.sun.xml.writer.global",
- ".sxi" => "application/vnd.sun.xml.impress",
- ".sxw" => "application/vnd.sun.xml.writer",
- ".sylk" => "text/vnd.sylk",
- ".tiff" => "image/tiff",
- ".tsv" => "text/tab-separated-values",
- ".txt" => "text/plain",
- ".uof" => "application/vnd.uoml+xml",
- ".uop" => "application/vnd.openofficeorg.presentation",
- ".uos1" => "application/vnd.uoml+xml",
- ".uos2" => "application/vnd.uoml+xml",
- ".uot" => "application/x-uo",
- ".vor" => "application/vnd.stardivision.writer",
- ".webp" => "image/webp",
- ".wpd" => "application/wordperfect",
- ".wps" => "application/vnd.ms-works",
- ".wq1" => "application/x-lotus",
- ".wq2" => "application/x-lotus",
- ".xls" => "application/vnd.ms-excel",
- ".xlsb" => "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
- ".xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.12",
- ".xlr" => "application/vnd.ms-works",
- ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- ".xlw" => "application/vnd.ms-excel",
- ".xml" => "application/xml",
- ".zabw" => "application/x-abiword",
- _ => "application/octet-stream"
- };
+ private static string GetMediaType(FileInfo source) =>
+ MediaTypeMap.GetMediaType(source.Extension) ?? "application/octet-stream";
}
diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/Microsoft.Extensions.DataIngestion.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/Microsoft.Extensions.DataIngestion.Abstractions.csproj
index e22de778e06..459bd2cc850 100644
--- a/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/Microsoft.Extensions.DataIngestion.Abstractions.csproj
+++ b/src/Libraries/Microsoft.Extensions.DataIngestion.Abstractions/Microsoft.Extensions.DataIngestion.Abstractions.csproj
@@ -15,6 +15,10 @@
$(NoWarn);S1694;S2368
+
+ true
+
+
diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs
index 753f0053b90..cbec488cb2e 100644
--- a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs
+++ b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/MarkItDownMcpReader.cs
@@ -43,21 +43,8 @@ public override async Task ReadAsync(FileInfo source, string
throw new FileNotFoundException("The specified file does not exist.", source.FullName);
}
- // Read file content and create DataContent
-#if NET
- ReadOnlyMemory fileBytes = await File.ReadAllBytesAsync(source.FullName, cancellationToken).ConfigureAwait(false);
-#else
- ReadOnlyMemory fileBytes;
- using (FileStream fs = new(source.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1, FileOptions.Asynchronous))
- {
- using MemoryStream ms = new((int)Math.Min(int.MaxValue, fs.Length));
- await fs.CopyToAsync(ms).ConfigureAwait(false);
- fileBytes = ms.GetBuffer().AsMemory(0, (int)ms.Length);
- }
-#endif
- DataContent dataContent = new(
- fileBytes,
- string.IsNullOrEmpty(mediaType) ? "application/octet-stream" : mediaType!);
+ // Read file content and create DataContent (media type is inferred from extension if not provided)
+ DataContent dataContent = await DataContent.LoadFromAsync(source.FullName, mediaType, cancellationToken).ConfigureAwait(false);
string markdown = await ConvertToMarkdownAsync(dataContent, cancellationToken).ConfigureAwait(false);
@@ -70,16 +57,8 @@ public override async Task ReadAsync(Stream source, string id
_ = Throw.IfNull(source);
_ = Throw.IfNullOrEmpty(identifier);
- // Read stream content and create DataContent
- using MemoryStream ms = source.CanSeek ? new((int)Math.Min(int.MaxValue, source.Length)) : new();
-#if NET
- await source.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
-#else
- await source.CopyToAsync(ms).ConfigureAwait(false);
-#endif
- DataContent dataContent = new(
- ms.GetBuffer().AsMemory(0, (int)ms.Length),
- string.IsNullOrEmpty(mediaType) ? "application/octet-stream" : mediaType);
+ // Read stream content and create DataContent (media type is inferred from FileStream path if applicable)
+ DataContent dataContent = await DataContent.LoadFromAsync(source, mediaType, cancellationToken).ConfigureAwait(false);
string markdown = await ConvertToMarkdownAsync(dataContent, cancellationToken).ConfigureAwait(false);
diff --git a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/Microsoft.Extensions.DataIngestion.MarkItDown.csproj b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/Microsoft.Extensions.DataIngestion.MarkItDown.csproj
index 013097ea6c7..381e524df97 100644
--- a/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/Microsoft.Extensions.DataIngestion.MarkItDown.csproj
+++ b/src/Libraries/Microsoft.Extensions.DataIngestion.MarkItDown/Microsoft.Extensions.DataIngestion.MarkItDown.csproj
@@ -18,6 +18,7 @@
+
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
index 47bc6ccfd62..2730ee596cb 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/DataContentTests.cs
@@ -2,8 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.IO;
using System.Text;
using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Extensions.AI;
@@ -276,4 +279,746 @@ public void FileName_Roundtrips()
content.Name = "test.bin";
Assert.Equal("test.bin", content.Name);
}
+
+ [Fact]
+ public async Task LoadFromAsync_Path_InfersMediaTypeAndName()
+ {
+ // Create a temporary file with known content
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.json");
+ try
+ {
+ byte[] testData = Encoding.UTF8.GetBytes("{\"key\": \"value\"}");
+ await File.WriteAllBytesAsync(tempPath, testData);
+
+ // Load from path
+ DataContent content = await DataContent.LoadFromAsync(tempPath);
+
+ // Verify the content
+ Assert.Equal("application/json", content.MediaType);
+ Assert.Equal(Path.GetFileName(tempPath), content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Path_UsesProvidedMediaType()
+ {
+ // Create a temporary file with known content
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.bin");
+ try
+ {
+ byte[] testData = new byte[] { 1, 2, 3, 4, 5 };
+ await File.WriteAllBytesAsync(tempPath, testData);
+
+ // Load from path with specified media type
+ DataContent content = await DataContent.LoadFromAsync(tempPath, "custom/type");
+
+ // Verify the content uses the provided media type, not inferred
+ Assert.Equal("custom/type", content.MediaType);
+ Assert.Equal(Path.GetFileName(tempPath), content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Path_FallsBackToOctetStream()
+ {
+ // Create a temporary file with unknown extension
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.unknownextension");
+ try
+ {
+ byte[] testData = new byte[] { 1, 2, 3 };
+ await File.WriteAllBytesAsync(tempPath, testData);
+
+ // Load from path
+ DataContent content = await DataContent.LoadFromAsync(tempPath);
+
+ // Verify the content falls back to octet-stream
+ Assert.Equal("application/octet-stream", content.MediaType);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Stream_InfersFromFileStream()
+ {
+ // Create a temporary file
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.png");
+ try
+ {
+ byte[] testData = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; // PNG signature
+ await File.WriteAllBytesAsync(tempPath, testData);
+
+ // Load from FileStream
+ using FileStream fs = new(tempPath, FileMode.Open, FileAccess.Read);
+ DataContent content = await DataContent.LoadFromAsync(fs);
+
+ // Verify inference from FileStream path
+ Assert.Equal("image/png", content.MediaType);
+ Assert.Equal(Path.GetFileName(tempPath), content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Stream_UsesProvidedMediaType()
+ {
+ // Create a MemoryStream with test data
+ byte[] testData = [1, 2, 3, 4];
+ using MemoryStream ms = new(testData);
+
+ // Load from stream with explicit media type
+ DataContent content = await DataContent.LoadFromAsync(ms, "video/mp4");
+
+ // Verify the explicit media type is used
+ Assert.Equal("video/mp4", content.MediaType);
+ Assert.Null(content.Name); // Name can be assigned after the call if needed
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Stream_FallsBackToOctetStream()
+ {
+ // Create a MemoryStream with test data (non-FileStream, no inference possible)
+ byte[] testData = new byte[] { 1, 2, 3 };
+ using MemoryStream ms = new(testData);
+
+ // Load from stream without media type or name
+ DataContent content = await DataContent.LoadFromAsync(ms);
+
+ // Verify fallback to octet-stream
+ Assert.Equal("application/octet-stream", content.MediaType);
+ Assert.Null(content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+
+ [Fact]
+ public async Task SaveToAsync_WritesDataToFile()
+ {
+ // Create DataContent with known data
+ byte[] testData = new byte[] { 1, 2, 3, 4, 5 };
+ DataContent content = new(testData, "application/octet-stream");
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.bin");
+ try
+ {
+ // Save to path
+ string actualPath = await content.SaveToAsync(tempPath);
+
+ // Verify data was written
+ Assert.Equal(tempPath, actualPath);
+ Assert.True(File.Exists(actualPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(actualPath));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_InfersExtension_WhenPathHasNoExtension()
+ {
+ // Create DataContent with JSON media type
+ byte[] testData = Encoding.UTF8.GetBytes("{}");
+ DataContent content = new(testData, "application/json");
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}");
+ try
+ {
+ // Save to path without extension
+ string actualPath = await content.SaveToAsync(tempPath);
+
+ // Verify extension was inferred
+ Assert.Equal(tempPath, actualPath);
+ Assert.True(File.Exists(actualPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(actualPath));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_UsesDirectoryAndInfersNameFromNameProperty()
+ {
+ // Create DataContent with JSON media type and a name
+ byte[] testData = Encoding.UTF8.GetBytes("{}");
+ DataContent content = new(testData, "application/json")
+ {
+ Name = "myfile.json"
+ };
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ string expectedPath = Path.Combine(tempDir, "myfile.json");
+ try
+ {
+ // Save to directory path
+ string actualPath = await content.SaveToAsync(tempDir);
+
+ // Verify the name was used
+ Assert.Equal(expectedPath, actualPath);
+ Assert.True(File.Exists(actualPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(actualPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_UsesRandomNameWhenDirectoryProvidedAndNoName()
+ {
+ // Create DataContent with JSON media type but no name
+ byte[] testData = Encoding.UTF8.GetBytes("{}");
+ DataContent content = new(testData, "application/json");
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ try
+ {
+ // Save to directory path
+ string actualPath = await content.SaveToAsync(tempDir);
+
+ // Verify a file was created with .json extension
+ Assert.StartsWith(tempDir, actualPath);
+ Assert.EndsWith(".json", actualPath);
+ Assert.True(File.Exists(actualPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(actualPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_DoesNotInferExtension_WhenPathAlreadyHasExtension()
+ {
+ // Create DataContent with JSON media type
+ byte[] testData = Encoding.UTF8.GetBytes("{}");
+ DataContent content = new(testData, "application/json");
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.txt");
+ try
+ {
+ // Save to path that already has an extension
+ string actualPath = await content.SaveToAsync(tempPath);
+
+ // Verify the original extension was preserved, not replaced
+ Assert.Equal(tempPath, actualPath);
+ Assert.True(File.Exists(actualPath));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_WithEmptyPath_UsesCurrentDirectory()
+ {
+ // Empty string path is valid - it uses the current directory with inferred filename.
+ // We use a unique name to avoid conflicts with concurrent tests.
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_empty_path_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ string uniqueName = $"test_{Guid.NewGuid()}.json";
+
+ try
+ {
+ // Save with empty path from within the temp directory context
+ // Since we can't change current directory, we test the directory path behavior instead
+ DataContent content = new(new byte[] { 1, 2, 3 }, "application/json") { Name = uniqueName };
+ string savedPath = await content.SaveToAsync(tempDir);
+
+ // When given a directory, it should use Name as the filename
+ Assert.Equal(Path.Combine(tempDir, uniqueName), savedPath);
+ Assert.True(File.Exists(savedPath));
+ Assert.Equal(new byte[] { 1, 2, 3 }, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Path_ThrowsOnNull()
+ {
+ await Assert.ThrowsAsync("path", async () => await DataContent.LoadFromAsync((string)null!));
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Path_ThrowsOnEmpty()
+ {
+ await Assert.ThrowsAsync("path", async () => await DataContent.LoadFromAsync(string.Empty));
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Stream_ThrowsOnNull()
+ {
+ await Assert.ThrowsAsync("stream", async () => await DataContent.LoadFromAsync((Stream)null!));
+ }
+
+ [Fact]
+ public async Task SaveToAsync_ThrowsOnNull()
+ {
+ DataContent content = new(new byte[] { 1 }, "application/octet-stream");
+ await Assert.ThrowsAsync("path", async () => await content.SaveToAsync(null!));
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_AbsolutePath_LoadsCorrectly()
+ {
+ // Create a temp file with an absolute path
+ string tempDir = Path.GetTempPath();
+ string absolutePath = Path.Combine(tempDir, $"test_absolute_{Guid.NewGuid()}.txt");
+
+ try
+ {
+ byte[] testData = Encoding.UTF8.GetBytes("absolute path test");
+ await File.WriteAllBytesAsync(absolutePath, testData);
+
+ DataContent content = await DataContent.LoadFromAsync(absolutePath);
+
+ Assert.Equal("text/plain", content.MediaType);
+ Assert.Equal(Path.GetFileName(absolutePath), content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(absolutePath))
+ {
+ File.Delete(absolutePath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_RelativePath_LoadsCorrectly()
+ {
+ // Test that LoadFromAsync works with paths that have different components.
+ // We use absolute paths but verify the Name extraction works correctly.
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_relative_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ string filePath = Path.Combine(tempDir, "relativefile.xml");
+ byte[] testData = Encoding.UTF8.GetBytes("");
+ await File.WriteAllBytesAsync(filePath, testData);
+
+ DataContent content = await DataContent.LoadFromAsync(filePath);
+
+ Assert.Equal("application/xml", content.MediaType);
+ Assert.Equal("relativefile.xml", content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_RelativePathWithSubdirectory_LoadsCorrectly()
+ {
+ // Test loading from a nested directory structure
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_subdir_{Guid.NewGuid()}");
+ string subDir = Path.Combine(tempDir, "subdir");
+ Directory.CreateDirectory(subDir);
+
+ try
+ {
+ string filePath = Path.Combine(subDir, "nested.html");
+ byte[] testData = Encoding.UTF8.GetBytes("");
+ await File.WriteAllBytesAsync(filePath, testData);
+
+ DataContent content = await DataContent.LoadFromAsync(filePath);
+
+ Assert.Equal("text/html", content.MediaType);
+ Assert.Equal("nested.html", content.Name);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_AbsolutePath_SavesCorrectly()
+ {
+ byte[] testData = [1, 2, 3, 4, 5];
+ DataContent content = new(testData, "application/octet-stream");
+
+ string tempDir = Path.GetTempPath();
+ string absolutePath = Path.Combine(tempDir, $"test_absolute_{Guid.NewGuid()}.bin");
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(absolutePath);
+
+ Assert.Equal(absolutePath, savedPath);
+ Assert.True(File.Exists(absolutePath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(absolutePath));
+ }
+ finally
+ {
+ if (File.Exists(absolutePath))
+ {
+ File.Delete(absolutePath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_ToSubdirectory_SavesCorrectly()
+ {
+ byte[] testData = [10, 20, 30];
+ DataContent content = new(testData, "image/png");
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_save_subdir_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ string filePath = Path.Combine(tempDir, "output.png");
+ string savedPath = await content.SaveToAsync(filePath);
+
+ Assert.Equal(filePath, savedPath);
+ Assert.True(File.Exists(savedPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_ToNestedSubdirectory_SavesCorrectly()
+ {
+ byte[] testData = [100, 200];
+ DataContent content = new(testData, "audio/wav");
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_save_nested_{Guid.NewGuid()}");
+ string subDir = Path.Combine(tempDir, "audio");
+ Directory.CreateDirectory(subDir);
+
+ try
+ {
+ string filePath = Path.Combine(subDir, "sound.wav");
+ string savedPath = await content.SaveToAsync(filePath);
+
+ Assert.Equal(filePath, savedPath);
+ Assert.True(File.Exists(savedPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_EmptyPath_WithoutName_GeneratesRandomName()
+ {
+ byte[] testData = [1, 2, 3];
+ DataContent content = new(testData, "image/png");
+
+ // Note: Name is NOT set - should generate a random GUID-based name.
+ // Test using a directory path instead of empty string to avoid current directory issues.
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_empty_noname_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(tempDir);
+
+ // Should generate a random GUID-based name with .png extension
+ Assert.StartsWith(tempDir, savedPath);
+ Assert.EndsWith(".png", savedPath);
+ Assert.True(File.Exists(savedPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_DirectoryPath_WithoutName_GeneratesRandomName()
+ {
+ byte[] testData = [5, 6, 7];
+ DataContent content = new(testData, "text/css");
+
+ // Note: Name is NOT set
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_dir_noname_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(tempDir);
+
+ // Should generate a random GUID-based name with .css extension
+ Assert.StartsWith(tempDir, savedPath);
+ Assert.EndsWith(".css", savedPath);
+ Assert.True(File.Exists(savedPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_WithCancellationToken_PropagatesToken()
+ {
+ // This test verifies that the cancellation token is properly propagated.
+ // With already-cancelled tokens, TaskCanceledException (a subclass of OperationCanceledException) may be thrown.
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_cancel_{Guid.NewGuid()}.txt");
+
+ try
+ {
+ await File.WriteAllBytesAsync(tempPath, new byte[] { 1, 2, 3 });
+
+ using CancellationTokenSource cts = new();
+ cts.Cancel();
+
+ await Assert.ThrowsAnyAsync(async () =>
+ await DataContent.LoadFromAsync(tempPath, cancellationToken: cts.Token));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_WithCancellationToken_PropagatesToken()
+ {
+ // This test verifies that the cancellation token is properly propagated.
+ // With already-cancelled tokens, TaskCanceledException (a subclass of OperationCanceledException) may be thrown.
+ DataContent content = new(new byte[] { 1, 2, 3 }, "application/octet-stream");
+
+ using CancellationTokenSource cts = new();
+ cts.Cancel();
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_save_cancel_{Guid.NewGuid()}.bin");
+
+ try
+ {
+ await Assert.ThrowsAnyAsync(async () =>
+ await content.SaveToAsync(tempPath, cts.Token));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_Stream_WithCancellationToken_PropagatesToken()
+ {
+ // This test verifies that the cancellation token is properly propagated.
+ // With already-cancelled tokens, TaskCanceledException (a subclass of OperationCanceledException) may be thrown.
+ byte[] testData = [1, 2, 3];
+ using MemoryStream ms = new(testData);
+
+ using CancellationTokenSource cts = new();
+ cts.Cancel();
+
+ await Assert.ThrowsAnyAsync(async () =>
+ await DataContent.LoadFromAsync(ms, cancellationToken: cts.Token));
+ }
+
+ [Fact]
+ public async Task SaveToAsync_ExistingDirectory_WithName_UsesNameAsFilename()
+ {
+ byte[] testData = [1, 2, 3];
+ DataContent content = new(testData, "application/json")
+ {
+ Name = "specific-output.json"
+ };
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_dir_name_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ string expectedPath = Path.Combine(tempDir, "specific-output.json");
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(tempDir);
+
+ Assert.Equal(expectedPath, savedPath);
+ Assert.True(File.Exists(expectedPath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(expectedPath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_NonExistentPath_TreatedAsFilePath()
+ {
+ // When the path doesn't exist, it's treated as a file path, not a directory
+ byte[] testData = [1, 2, 3];
+ DataContent content = new(testData, "application/json");
+
+ string tempDir = Path.Combine(Path.GetTempPath(), $"test_nonexist_{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempDir);
+ string filePath = Path.Combine(tempDir, "newfile");
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(filePath);
+
+ // Since the path doesn't exist as a directory, it's used as the file path directly
+ Assert.Equal(filePath, savedPath);
+ Assert.True(File.Exists(filePath));
+ Assert.Equal(testData, await File.ReadAllBytesAsync(filePath));
+ }
+ finally
+ {
+ if (Directory.Exists(tempDir))
+ {
+ Directory.Delete(tempDir, true);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task LoadFromAsync_LargeFile_LoadsCorrectly()
+ {
+ // Test with a larger file to ensure streaming works properly
+ byte[] testData = new byte[100_000];
+ new Random(42).NextBytes(testData);
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_large_{Guid.NewGuid()}.bin");
+
+ try
+ {
+ await File.WriteAllBytesAsync(tempPath, testData);
+
+ DataContent content = await DataContent.LoadFromAsync(tempPath);
+
+ Assert.Equal("application/octet-stream", content.MediaType);
+ Assert.Equal(testData, content.Data.ToArray());
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task SaveToAsync_LargeContent_SavesCorrectly()
+ {
+ // Test with larger content to ensure streaming works properly
+ byte[] testData = new byte[100_000];
+ new Random(42).NextBytes(testData);
+
+ DataContent content = new(testData, "application/octet-stream");
+
+ string tempPath = Path.Combine(Path.GetTempPath(), $"test_save_large_{Guid.NewGuid()}.bin");
+
+ try
+ {
+ string savedPath = await content.SaveToAsync(tempPath);
+
+ Assert.Equal(tempPath, savedPath);
+ Assert.Equal(testData, await File.ReadAllBytesAsync(savedPath));
+ }
+ finally
+ {
+ if (File.Exists(tempPath))
+ {
+ File.Delete(tempPath);
+ }
+ }
+ }
}
diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj
index 4c275e54993..93bc059c213 100644
--- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj
+++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Microsoft.Extensions.AI.Abstractions.Tests.csproj
@@ -20,6 +20,7 @@
true
true
true
+ true