{"id":45044,"date":"2023-03-29T10:05:00","date_gmt":"2023-03-29T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=45044"},"modified":"2024-12-13T14:16:46","modified_gmt":"2024-12-13T22:16:46","slug":"improving-multiplatform-container-support","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/improving-multiplatform-container-support\/","title":{"rendered":"Improving multi-platform container support"},"content":{"rendered":"<blockquote>\n<p>This post was updated on April 15, 2024 to reflect the latest releases, fix broken links, and add illustrative images.<\/p>\n<\/blockquote>\n<p>Docker makes it straightforward to build container images that target a specific hardware architecture (Arm and x64). This is particularly useful if you have an Arm64 M1\/M2\/M3 Apple Mac and want to build containers for an x64 cloud service. We are encouraging a new pattern for building container images for multiple architectures. It has better performance and integrates with the <a href=\"https:\/\/www.docker.com\/blog\/how-to-rapidly-build-multi-architecture-images-with-buildx\/\">BuildKit<\/a> <a href=\"https:\/\/docs.docker.com\/build\/buildkit\/\">build model<\/a>.<\/p>\n<p>This post is focused on building container images with <code>docker build<\/code>. You can also <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/streamline-container-build-dotnet-8\/\">build container images directly with the SDK<\/a> but the pattern discussed in this post isn&#8217;t required or relevant if you build container images with <code>dotnet publish<\/code>.<\/p>\n<h2>Problem to solve<\/h2>\n<p>Many users build x64 .NET container images on Arm64 hardware. Doing that can result in the .NET SDK running in an emulated environment, which doesn&#8217;t work well. You can see in the following screenshot that <code>dotnet restore<\/code> is taking a very long time to run when run with emulation. The pattern described in this post avoids this problem.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2024\/04\/docker-build-emulation.png\" alt=\"dotnet restore hangs when run with processor architecture emulation\" \/><\/p>\n<h2>Solution<\/h2>\n<p>You can run <code>docker build<\/code> with a multi-platform friendly pattern. It has two aspects.<\/p>\n<p>Use the <code>--platform<\/code> switch with <code>docker build<\/code>, which you already would be doing to target an alternate architecture:<\/p>\n<pre><code class=\"language-bash\">docker build --platform linux\/amd64 -t app .<\/code><\/pre>\n<p>Use <code>$BUILDPLATFORM<\/code> and <code>$TARGETARCH<\/code> environment variables in your <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/822166bbf88e23cbbbba6fedc3f56b99276a1e96\/samples\/aspnetapp\/Dockerfile\">Dockerfile<\/a> to get the SDK to run natively and cross-compile\/publish.<\/p>\n<pre><code class=\"language-Dockerfile\">FROM --platform=$BUILDPLATFORM mcr.microsoft.com\/dotnet\/sdk:8.0 AS build<\/code><\/pre>\n<p>and<\/p>\n<pre><code class=\"language-Dockerfile\">RUN dotnet publish -a $TARGETARCH<\/code><\/pre>\n<p>This pattern can be used with any .NET version. In particular, we added support for <code>-a<\/code> to support Docker <code>$TARGETARCH<\/code> values (which are really Golang processor architecture values and different from .NET ones) in the .NET 8 SDK (<a href=\"https:\/\/github.com\/dotnet\/sdk\/pull\/30762\">dotnet\/sdk #30762<\/a>).<\/p>\n<p>The rest of the post will go into much more detail.<\/p>\n<h2>Multi-platform build<\/h2>\n<p>There are really two different scenarios at play, which I&#8217;m calling &#8220;multi-platform&#8221;.<\/p>\n<ul>\n<li>Build a container image for a specific architecture (different than your machine).<\/li>\n<li>Build multiple container images at once, for multiple architectures.<\/li>\n<\/ul>\n<p>Everything we&#8217;re going to look at applies to both of these scenarios.<\/p>\n<p>Let&#8217;s start with the Docker <a href=\"https:\/\/docs.docker.com\/build\/building\/multi-platform\/\">multi-platform model<\/a>. We&#8217;ll assume that the user is using Apple Arm64 hardware. In fact, I&#8217;m writing this all on a MacBook Air M1 laptop, which will make it easy for me to demonstrate the behavior.<\/p>\n<p>Docker has a <code>--platform<\/code> switch that you can use to control the output of your images. I&#8217;m going to show you how this system works using Alpine, to make the explanation as simple as possible.<\/p>\n<p>Here&#8217;s a simple Dockerfile to test the behavior. The <code>alpine<\/code> tag is a multi-platform tag (more on that shortly).<\/p>\n<pre><code class=\"language-dockerfile\">FROM alpine<\/code><\/pre>\n<p>Let&#8217;s build it and validate the results. Again, I&#8217;m on an Arm64 machine.<\/p>\n<pre><code class=\"language-bash\">% docker build -t image .\n% docker inspect image -f \"{{.Os}}\/{{.Architecture}}\" \nlinux\/arm64<\/code><\/pre>\n<p>We have an Arm64 image, as expected.<\/p>\n<p>We can target x64 by using the <code>--platform<\/code> switch.<\/p>\n<pre><code class=\"language-bash\">% docker build -t image --platform linux\/amd64 .\n% docker inspect image -f \"{{.Os}}\/{{.Architecture}}\"\nlinux\/amd64<\/code><\/pre>\n<p>Now we have an x64 image. If we run the image, we can see that it is x64 &#8212; via the <code>x86_64<\/code> string &#8212; but there is a warning due to the image (which will be <a href=\"https:\/\/www.qemu.org\/\">emulated<\/a>) and the implicit platform not matching.<\/p>\n<pre><code class=\"language-bash\">% docker run --rm image uname -a\nWARNING: The requested image's platform (linux\/amd64) does not match the detected host platform (linux\/arm64\/v8) and no specific platform was requested\nLinux 188008b850d3 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 UTC 2022 x86_64 Linux<\/code><\/pre>\n<p><code>--platform<\/code> can be used to get rid of that error message.<\/p>\n<pre><code class=\"language-bash\">% docker run --rm --platform linux\/amd64 image uname -a\nLinux 40b8d36a90f8 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 UTC 2022 x86_64 Linux<\/code><\/pre>\n<p>The <code>--platform<\/code> switch is a bit subtle. I&#8217;ll explain.<\/p>\n<p>The first take-away is that <code>--platform<\/code> is always set, implicitly as your machine platform or an explicitly specified one.<\/p>\n<p>The bigger point is that <code>--platform<\/code> affects (A) multi-platform tags (like <code>alpine<\/code>) and (B) the configuration of the final image.<\/p>\n<p>We can see that <code>alpine<\/code> is a multi-platform tag, supporting several architectures:<\/p>\n<pre><code class=\"language-bash\">% docker manifest inspect alpine | grep architecture\n            \"architecture\": \"amd64\",\n            \"architecture\": \"arm\",\n            \"architecture\": \"arm\",\n            \"architecture\": \"arm64\",\n            \"architecture\": \"386\",\n            \"architecture\": \"ppc64le\",\n            \"architecture\": \"s390x\",<\/code><\/pre>\n<p>The platform value (implicitly or explicitly specified) is used to pick which of those architecture-specific images to pull. Each one of those is a distinct image with its own separately compiled copy of Alpine Linux, all referenced from the <code>alpine<\/code> tag.<\/p>\n<p>The Alpine team also publishes architecture-specific images, such as <a href=\"https:\/\/hub.docker.com\/r\/arm64v8\/alpine\/\"><code>arm64v8\/alpine<\/code><\/a>.<\/p>\n<pre><code class=\"language-bash\">% docker manifest inspect -v arm64v8\/alpine | grep architecture\n            \"architecture\": \"arm64\",<\/code><\/pre>\n<p>We can still use the <code>--platform<\/code> switch with this tag, but we&#8217;ll get incorrect results.<\/p>\n<pre><code class=\"language-bash\">% echo \"FROM arm64v8\/alpine\" &gt; Dockerfile\n% docker build -t image .\n% docker inspect image -f \"{{.Os}}\/{{.Architecture}}\"\nlinux\/arm64\n% docker build -t image --platform linux\/amd64 .\n% docker inspect image -f \"{{.Os}}\/{{.Architecture}}\"\nlinux\/amd64\n% docker run --rm -it image uname -a\nWARNING: The requested image's platform (linux\/amd64) does not match the detected host platform (linux\/arm64\/v8) and no specific platform was requested\nLinux d9abfaec07a1 5.15.49-linuxkit #1 SMP PREEMPT Tue Sep 13 07:51:32 UTC 2022 aarch64 Linux<\/code><\/pre>\n<p>If you look closely, you&#8217;ll see that this image is misconfigured. It is an <code>aarch64<\/code> image but is marked as <code>linux\/amd64<\/code>. That&#8217;s not very helpful.<\/p>\n<p>In case you missed it in bash output, our Dockerfile now looks like the following.<\/p>\n<pre><code class=\"language-dockerfile\">FROM arm64v8\/alpine<\/code><\/pre>\n<p>You can safely and correctly use the <code>--platform<\/code> tag with Dockerfiles that have a mix of multi-platform and platform-specific tags, however, the final stage must reference a multi-platform image. In our updated Dockerfile, we have one <code>FROM<\/code> statement with a platform-specific tag, so using the <code>--platform<\/code> switch is user error and expected to result in a bad image.<\/p>\n<p>We&#8217;re almost done on background context. Docker also defines a bunch of <code>ARG<\/code> values, as you can see in the following <a href=\"https:\/\/gist.github.com\/richlander\/70cde3f0176d36862af80c41722acd47\">Dockerfile<\/a>.<\/p>\n<pre><code class=\"language-dockerfile\">FROM alpine\n\nARG TARGETPLATFORM\nARG TARGETOS\nARG TARGETARCH\nARG TARGETVARIANT\nARG BUILDPLATFORM\nARG BUILDOS\nARG BUILDARCH\nARG BUILDVARIANT\nRUN echo \"Building on $BUILDPLATFORM, targeting $TARGETPLATFORM\"\nRUN echo \"Building on ${BUILDOS} and ${BUILDARCH} with optional variant ${BUILDVARIANT}\"\nRUN echo \"Targeting ${TARGETOS} and ${TARGETARCH} with optional variant ${TARGETVARIANT}\"<\/code><\/pre>\n<p>We can build the Dockerfile targeting x64 (on Arm64).<\/p>\n<pre><code class=\"language-bash\">% docker build -t image --platform linux\/amd64 .\n[+] Building 1.7s (8\/8) FINISHED                                                \n =&gt; [internal] load build definition from Dockerfile                       0.0s\n =&gt; =&gt; transferring dockerfile: 428B                                       0.0s\n =&gt; [internal] load .dockerignore                                          0.0s\n =&gt; =&gt; transferring context: 2B                                            0.0s\n =&gt; [internal] load metadata for docker.io\/library\/alpine:latest           0.0s\n =&gt; [1\/4] FROM docker.io\/library\/alpine                                    0.0s\n =&gt; [2\/4] RUN echo \"Building on linux\/arm64, targeting linux\/amd64\"        0.2s\n =&gt; [3\/4] RUN echo \"Building on linux and arm64 with optional variant \"    0.3s\n =&gt; [4\/4] RUN echo \"Targeting linux and amd64 with optional variant \"      0.3s\n =&gt; exporting to image                                                     0.0s\n =&gt; =&gt; exporting layers                                                    0.0s\n =&gt; =&gt; writing image sha256:c70637a6942e07f3a00eb03c1c6bf1cd8a709e91cd627  0.0s\n =&gt; =&gt; naming to docker.io\/library\/image<\/code><\/pre>\n<p>Make sure to pay close attention to those <code>RUN echo<\/code> lines.<\/p>\n<p>These environment variables will prove useful when we look at our solution for .NET, in the next section.<\/p>\n<h2>Build .NET images for any architecture<\/h2>\n<p>We&#8217;re now going to look at how to build .NET images using similar techniques to what we just looked at with Alpine.<\/p>\n<p>There are a few characteristics that we want:<\/p>\n<ul>\n<li>A single Dockerfile should work for multiple architectures.<\/li>\n<li>The result should be optimal.<\/li>\n<li>The SDK should always run with the native architecture, both because it is faster and because <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/6.0\/supported-os.md#qemu\">.NET doesn&#8217;t support QEMU<\/a>.<\/li>\n<\/ul>\n<p>We&#8217;ll use <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/aspnetapp\/Dockerfile\">Dockerfile<\/a>. This Dockerfile has been updated for .NET 8, both enabling <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/securing-containers-with-rootless\/\">non-root hosting<\/a> and multi-platform targeting.<\/p>\n<p>There are a few lines in this Dockerfile that do something special that help us achieve those characteristics.<\/p>\n<pre><code class=\"language-dockerfile\">FROM --platform=$BUILDPLATFORM mcr.microsoft.com\/dotnet\/sdk:8.0 AS build<\/code><\/pre>\n<p>The Dockerfile format enables specifying the <code>--platform<\/code> switch for a <code>FROM<\/code> statement and to use a built-in <code>ARG<\/code> to provide the value. In this case, we&#8217;re saying that the <code>$BUILDPLATFORM<\/code> (AKA the local machine architecture) should always be used. On an Arm64 machine, this will always be Arm64, even if targeting x64.<\/p>\n<p>This pattern doesn&#8217;t actually require .NET 8. It works with any .NET version. In fact, it will work with any multi-platform tag, like for Node.js or Java. It&#8217;s just a Docker feature.<\/p>\n<pre><code class=\"language-dockerfile\">RUN dotnet restore -a $TARGETARCH\n\n# copy everything else and build app\nCOPY aspnetapp\/. .\nRUN dotnet publish -a $TARGETARCH --no-restore -o \/app<\/code><\/pre>\n<p>We&#8217;re now restoring and publishing the app for the target architecture, using another built-in argument.<\/p>\n<p>Underneath the covers, the SDK is creating a Runtime ID (RID). That&#8217;s the same as specifying <code>-r<\/code> argument. The <code>-a<\/code> argument is a shortcut for that. We recommend publishing apps as <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/deploy-with-cli#native-dependencies\">RID-specific<\/a> in containers. Some NuGet packages include multiple copies of native dependencies. By <a href=\"https:\/\/gist.github.com\/richlander\/4c07862f6434944f6eee1815237dabe1\">publish an app as RID-specific<\/a>, you will get just one copy of those dependencies (reducing container size), matching your target environment.<\/p>\n<p>If you are building an Alpine image and use this pattern, make sure that use an Alpine-based SDK, so that <code>linux-musl<\/code> will be inferred when you use <code>-a<\/code> to specify the architecture. Read <a href=\"https:\/\/github.com\/dotnet\/sdk\/issues\/30369\">dotnet\/sdk #30369<\/a> for more information.<\/p>\n<p>You might be used to seeing <code>-c Release<\/code> in Dockerfiles. We&#8217;ve made that the default for <code>dotnet publish<\/code> with .NET 8, making it optional. We&#8217;ve been working on improving .NET SDK defaults.<\/p>\n<pre><code class=\"language-dockerfile\">FROM mcr.microsoft.com\/dotnet\/aspnet:8.0<\/code><\/pre>\n<p>The second (and last) <code>FROM<\/code> statement is a multi-platform tag. It will be affected by the <code>--platform<\/code> switch (from <code>docker build<\/code>). That&#8217;s what we want.<\/p>\n<p>Let&#8217;s try it out.<\/p>\n<pre><code class=\"language-bash\">% pwd\n\/Users\/rich\/git\/dotnet-docker\/samples\/aspnetapp\n% docker build --pull -t aspnetapp .\n% docker inspect aspnetapp -f \"{{.Os}}\/{{.Architecture}}\" \nlinux\/arm64<\/code><\/pre>\n<p>We see an Arm64 image.<\/p>\n<p>Let&#8217;s try targeting x64.<\/p>\n<pre><code class=\"language-bash\">% docker build --pull -t aspnetapp --platform linux\/amd64 .\n% docker inspect aspnetapp -f \"{{.Os}}\/{{.Architecture}}\"\nlinux\/amd64\n% docker run --rm --platform linux\/amd64 --entrypoint bash aspnetapp -c \"uname -a\" \nLinux 70dd5f0635cd 6.6.16-linuxkit #1 SMP Fri Feb 16 11:54:02 UTC 2024 x86_64 GNU\/Linux<\/code><\/pre>\n<p>That looks great. We have an x64 image.<\/p>\n<p>You can try this pattern for .NET 8 apps, but also .NET 6 and 7 apps, too. The .NET 8 SDK can build apps for those earlier versions with this same approach.<\/p>\n<h2>Build multi-platform images with <code>docker buildx<\/code><\/h2>\n<p><a href=\"https:\/\/www.docker.com\/blog\/how-to-rapidly-build-multi-architecture-images-with-buildx\/\">Buildx<\/a> is a nice addition to Docker tools. I think of it as &#8220;full BuildKit&#8221;. For our purposes, it enables specifying multiple platforms to build at once and to package them all up as a multi-platform tag. It will even push them to your registry, all with a single command.<\/p>\n<p>We first need to setup buildx.<\/p>\n<pre><code class=\"language-bash\">% docker buildx create\nwhimsical_sanderson<\/code><\/pre>\n<p>We can now build a multi-platform image for our app.<\/p>\n<pre><code class=\"language-bash\">% docker buildx build --pull -t aspnetapp --platform linux\/arm64,linux\/arm,linux\/amd64 .<\/code><\/pre>\n<p>You may see this error:<\/p>\n<pre><code class=\"language-bash\">ERROR: Multi-platform build is not supported for the docker driver.\nSwitch to a different driver, or turn on the containerd image store, and try again.<\/code><\/pre>\n<p>Enable the <a href=\"https:\/\/docs.docker.com\/desktop\/containerd\/\">containerd image store<\/a> to resolve that.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2024\/04\/containerd-image-store.png\" alt=\"Enable containerd image store in Docker Desktop\" \/><\/p>\n<p>Here, we&#8217;re building for three architectures. In some environments, you can also specify just the architectures as a short-hand, avoiding repeating &#8220;linux&#8221;.<\/p>\n<p>With that command, you may the following warning.<\/p>\n<pre><code class=\"language-bash\">WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load<\/code><\/pre>\n<p>If you want to push your image to a registry, you need to add the <code>--push<\/code> argument and use a fully-specified registry name for the <code>-t<\/code> argument.<\/p>\n<p>Let&#8217;s try <code>--push<\/code> (with my private registry; you&#8217;ll need to switch to your own).<\/p>\n<pre><code class=\"language-bash\">$ docker buildx build --pull --push -t maplesugar.azurecr.io\/aspnetapp --platform linux\/arm64,linux\/arm,linux\/amd64 .<\/code><\/pre>\n<p>That command pushed 3 images and 1 tag to the registry.<\/p>\n<p>I can now try pulling the image on my Apple laptop. It would work the same on my Raspberry Pi.<\/p>\n<pre><code class=\"language-bash\">$ docker pull maplesugar.azurecr.io\/aspnetapp\n$ docker run --rm --name aspnetapp -d -p 8000:8080 maplesugar.azurecr.io\/aspnetapp\n08968dcce418db4d6f746bfa3a5f2afdcf66570bc8a726c4f5a4859e8666e354\n$ curl http:\/\/localhost:8000\/Environment\n{\"runtimeVersion\":\".NET 8.0.4\",\"osVersion\":\"Debian GNU\/Linux 12 (bookworm)\",\"osArchitecture\":\"Arm64\",\"user\":\"app\",\"processorCount\":8,\"totalAvailableMemoryBytes\":4113563648,\"memoryLimit\":0,\"memoryUsage\":32227328,\"hostName\":\"7a2b079005a5\"}\n% docker exec aspnetapp uname -a\nLinux 7a2b079005a5 6.6.22-linuxkit #1 SMP Fri Mar 29 12:21:27 UTC 2024 aarch64 GNU\/Linux\n% docker kill aspnetapp\naspnetapp<\/code><\/pre>\n<p>I&#8217;ll now try the same image on an x64 machine.<\/p>\n<pre><code class=\"language-bash\">$ docker run --rm --name aspnetapp -d -p 8000:8080 maplesugar.azurecr.io\/aspnetapp\n6dac425acc325da1c085608d503d6c884610cfa5b2a7dd93575f20355daec1a2\n$ curl http:\/\/localhost:8000\/Environment\n{\"runtimeVersion\":\".NET 8.0.4\",\"osVersion\":\"Debian GNU\/Linux 12 (bookworm)\",\"osArchitecture\":\"X64\",\"user\":\"app\",\"processorCount\":8,\"totalAvailableMemoryBytes\":67382321152,\"memoryLimit\":0,\"memoryUsage\":31629312,\"hostName\":\"78fed92fd353\"}\n$ docker exec aspnetapp uname -a\nLinux 78fed92fd353 6.8.0-22-generic #22-Ubuntu SMP PREEMPT_DYNAMIC Thu Apr  4 22:30:32 UTC 2024 x86_64 GNU\/Linux\n$ docker kill aspnetapp\naspnetapp<\/code><\/pre>\n<p>The primary difference is the value of the <code>osArchitecture<\/code> property in the JSON returned from the endpoint. We&#8217;re about to demonstrate that the <code>maplesugar.azurecr.io\/aspnetapp<\/code> tag we pushed is indeed a multi-platform image.<\/p>\n<h2>Summary<\/h2>\n<p>This new Dockerfile pattern for .NET apps makes it simpler, easier, and faster to build containers for whichever architecture you want on whichever machine you want. We&#8217;ve had <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/discussions\/3848\">past difficulties with x64 emulation with QEMU<\/a>. This approach side-steps QEMU completely, which avoid the problems we&#8217;ve had and improves performance at the same time.<\/p>\n<p>We wonder how teams will use the multi-platform container capability. Will you target multiple architectures at the developer desktop or also in CI? In particular, we wonder if teams will push both Arm64 and x64 container images to their registry even if only one of those architectures is needed by a cloud container service. Will pushing multiple architectures help developers investigate production issues when their local machine doesn&#8217;t match the architecture of the cloud service? Please tell us if you have plans or existing practices on that topic.<\/p>\n<p>We hope that this new pattern works for you, that it makes it easier to target multiple platforms and that you appreciate having <code>docker build --platform<\/code> and <code>docker buildx build --platform<\/code> as more straightforward options.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to build containers for multiple architectures, including building containers on Apple M1 machines for an x64 cloud.<\/p>\n","protected":false},"author":1312,"featured_media":51459,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7237,7720],"tags":[57,92],"class_list":["post-45044","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-containers","category-linux","tag-containers","tag-linux"],"acf":[],"blog_post_summary":"<p>Learn how to build containers for multiple architectures, including building containers on Apple M1 machines for an x64 cloud.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/45044","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1312"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=45044"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/45044\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/51459"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=45044"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=45044"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=45044"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}