{"id":1085,"date":"2018-01-25T12:42:43","date_gmt":"2018-01-25T04:42:43","guid":{"rendered":"https:\/\/blogs.msdn.microsoft.com\/seteplia\/?p=1085"},"modified":"2019-06-11T21:59:33","modified_gmt":"2019-06-12T04:59:33","slug":"the-performance-characteristics-of-async-methods","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/premier-developer\/the-performance-characteristics-of-async-methods\/","title":{"rendered":"The performance characteristics of async methods in C#"},"content":{"rendered":"<p><strong>The async series<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-async-methods-in-c\/\">Dissecting the async methods in C#<\/a>.<\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/extending-the-async-methods-in-c\/\">Extending the async methods in C#<\/a>.<\/li>\n<li><strong>The performance characteristics of the async methods in C#<\/strong>.<\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/\/one-user-scenario-to-rule-them-all\/\">One user scenario to rule them all<\/a>.<\/li>\n<\/ul>\n<p>In the last two blog posts we&#8217;ve covered <a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-async-methods-in-c\/\">the internals of async methods<\/a> in C# and then we looked at <a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/extending-the-async-methods-in-c\/\">the extensibility points<\/a> the C# compiler provides to adjust the behavior of async methods. Today we&#8217;re going to explore the performance characteristics of async methods.<\/p>\n<p>As you should already know from the <a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-async-methods-in-c\/\">first post of the series<\/a>, the compiler does a lot of transformations to make asynchronous programming experience very similar to a synchronous one. But to do that the compiler creates a state machine instance, pass it around to an async method builder, that calls task awaiter etc. Obviously, all of that logic has its own cost, but how much do we pay?<\/p>\n<p>Back in pre-TPL days, asynchronous operations usually were fairly coarse-grained so the overhead of an asynchronous operation was likely negligible. But today even relatively simple application could have hundreds if not thousands asynchronous operations per second. The TPL was designed with this workload in mind but it&#8217;s not magic, it has some overhead.<\/p>\n<p>To measure an overhead of async methods will use a slightly modified example that we used in the first blog post.<\/p>\n<pre class=\"lang:default decode:true \">public class StockPrices\r\n{\r\n    private const int Count = 100;\r\n    private List&lt;(string name, decimal price)&gt; _stockPricesCache;\r\n \r\n    \/\/ Async version\r\n    public async Task&lt;decimal&gt; GetStockPriceForAsync(string companyId)\r\n    {\r\n        await InitializeMapIfNeededAsync();\r\n        return DoGetPriceFromCache(companyId);\r\n    }\r\n \r\n    \/\/ Sync version that calls async init\r\n    public decimal GetStockPriceFor(string companyId)\r\n    {\r\n        InitializeMapIfNeededAsync().GetAwaiter().GetResult();\r\n        return DoGetPriceFromCache(companyId);\r\n    }\r\n \r\n    \/\/ Purely sync version\r\n    public decimal GetPriceFromCacheFor(string companyId)\r\n    {\r\n        InitializeMapIfNeeded();\r\n        return DoGetPriceFromCache(companyId);\r\n    }\r\n \r\n    private decimal DoGetPriceFromCache(string name)\r\n    {\r\n        foreach (var kvp in _stockPricesCache)\r\n        {\r\n            if (kvp.name == name)\r\n            {\r\n                return kvp.price;\r\n            }\r\n        }\r\n \r\n        throw new InvalidOperationException($\"Can't find price for '{name}'.\");\r\n    }\r\n \r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private void InitializeMapIfNeeded()\r\n    {\r\n        \/\/ Similar initialization logic.\r\n    }\r\n \r\n    private async Task InitializeMapIfNeededAsync()\r\n    {\r\n        if (_stockPricesCache != null)\r\n        {\r\n            return;\r\n        }\r\n \r\n        await Task.Delay(42);\r\n \r\n        \/\/ Getting the stock prices from the external source.\r\n        \/\/ Generate 1000 items to make cache hit somewhat expensive\r\n        _stockPricesCache = Enumerable.Range(1, Count)\r\n            .Select(n =&gt; (name: n.ToString(), price: (decimal)n))\r\n            .ToList();\r\n        _stockPricesCache.Add((name: \"MSFT\", price: 42));\r\n    }\r\n}<\/pre>\n<p><code>StockPrices<\/code> class populates the cache with the stock prices from an external source and provides an API to query it. The main difference from the first post&#8217;s example is the switch from the dictionary to a list of prices. To measure the overhead of different forms of async methods compared to synchronous ones the operation itself should do at least some work and linear search of stock prices models this aspect.<\/p>\n<p><code>GetPricesFromCache<\/code> is intentionally built using a plain loop to avoid any allocations.<\/p>\n<h4>Synchronous vs. Task-based asynchronous versions<\/h4>\n<p>In the first benchmark, we comparing async method that calls async initialization method (<code>GetStockPriceForAsync<\/code>), a synchronous method that calls asynchronous initialization method (<code>GetStockPriceFor<\/code>) and synchronous method that calls synchronous initialization method.<\/p>\n<pre class=\"lang:default decode:true \">private readonly StockPrices _stockPrices = new StockPrices();\r\n \r\npublic SyncVsAsyncBenchmark()\r\n{\r\n    \/\/ Warming up the cache\r\n    _stockPrices.GetStockPriceForAsync(\"MSFT\").GetAwaiter().GetResult();\r\n}\r\n \r\n[Benchmark]\r\npublic decimal GetPricesDirectlyFromCache()\r\n{\r\n    return _stockPrices.GetPriceFromCacheFor(\"MSFT\");\r\n}\r\n \r\n[Benchmark(Baseline = true)]\r\npublic decimal GetStockPriceFor()\r\n{\r\n    return _stockPrices.GetStockPriceFor(\"MSFT\");\r\n}\r\n \r\n[Benchmark]\r\npublic decimal GetStockPriceForAsync()\r\n{\r\n    return _stockPrices.GetStockPriceForAsync(\"MSFT\").GetAwaiter().GetResult();\r\n}<\/pre>\n<p>The results are:<\/p>\n<pre class=\"lang:default decode:true \">                     Method |     Mean | Scaled |  Gen 0 | Allocated |\r\n--------------------------- |---------:|-------:|-------:|----------:|\r\n GetPricesDirectlyFromCache | 2.177 us |   0.96 |      - |       0 B |\r\n           GetStockPriceFor | 2.268 us |   1.00 |      - |       0 B |\r\n      GetStockPriceForAsync | 2.523 us |   1.11 | 0.0267 |      88 B |<\/pre>\n<p>This data is already very interesting:<\/p>\n<ul>\n<li>The async method is rather fast. <code>GetPricesForAsync<\/code> completes synchronously in this benchmark and it&#8217;s about 15% (*) slower than the purely synchronous method.<\/li>\n<li>The synchronous <code>GetPricesFor<\/code> method that calls asynchronous <code>InitializeMapIfNeededAsync<\/code> method has even lower overhead, but the most surprising thing that it does <strong>not allocate at all<\/strong> (Allocated column in the previous table has 0 for both <code>GetPricesDirectlyFromCache<\/code> and for <code>GetStockPriceFor<\/code>).<\/li>\n<\/ul>\n<p>(*) Of course, you can&#8217;t say that the overhead of async machinery when the async method runs synchronously is 15% for all possible cases. The percentage is very specific to the amount of work the method is doing. Measuring a pure method call overhead for async method (that does nothing) with a synchronous method (that does nothing) will show a huge difference. The idea of this benchmark is to show that the overhead of an async method that does a relatively small amount of work is moderate.<\/p>\n<p>How is it possible that the call to <code>InitializeMapIfNeededAsync<\/code> caused no allocations at all? I&#8217;ve mentioned in the first post of the series that the async method <em>have to<\/em> allocate at least one object in the managed head &#8211; the task instance itself. Let&#8217;s explore this aspect.<\/p>\n<h4>Optimization #1. Cache the task instance if possible<\/h4>\n<p>The answer to the previous question is very simple: <strong><code>AsyncMethodBuilder<\/code> uses a single task instance for every successfully completed async operations<\/strong>. An async method that returns <code>Task<\/code> relies on <code>AsyncMethodBuilder<\/code> that has the following logic in <a href=\"http:\/\/referencesource.microsoft.com\/#mscorlib\/system\/runtime\/compilerservices\/AsyncMethodBuilder.cs,378\"><code>SetResult<\/code><\/a> method:<\/p>\n<pre class=\"lang:default decode:true \">\/\/ AsyncMethodBuilder.cs from mscorlib\r\npublic void SetResult()\r\n{\r\n    \/\/ I.e. the resulting task for all successfully completed\r\n    \/\/ methods is the same -- s_cachedCompleted.\r\n            \r\n    m_builder.SetResult(s_cachedCompleted);\r\n}<\/pre>\n<p><code>SetResult<\/code> method is called only for async methods that completed successfully and <strong>the successful result for every <code>Task<\/code>-based method can be easily shared<\/strong>. We can even observe this behavior with the following test:<\/p>\n<pre class=\"lang:default decode:true \">[Test]\r\npublic void AsyncVoidBuilderCachesResultingTask()\r\n{\r\n    var t1 = Foo();\r\n    var t2 = Foo();\r\n \r\n    Assert.AreSame(t1, t2);\r\n            \r\n    async Task Foo() { }\r\n}<\/pre>\n<p>But this is not the only optimization that could happen. <code>AsyncTaskMethodBuilder&lt;T&gt;<\/code> does a similar optimization: it caches tasks for <code>Task&lt;bool&gt;<\/code> and for some other primitive types. For instance, it caches all the default values for a bunch of integral types and has a special cache for <code>Task&lt;int&gt;<\/code> for the values in range [-1; 9) (see <code>AsyncTaskMethodBuilder&lt;T&gt;.GetTaskForResult()<\/code> for more details).<\/p>\n<p>The following test proofs that this is indeed the case:<\/p>\n<pre class=\"lang:default decode:true \">[Test]\r\npublic void AsyncTaskBuilderCachesResultingTask()\r\n{\r\n    \/\/ These values are cached\r\n    Assert.AreSame(Foo(-1), Foo(-1));\r\n    Assert.AreSame(Foo(8), Foo(8));\r\n \r\n    \/\/ But these are not\r\n    Assert.AreNotSame(Foo(9), Foo(9));\r\n    Assert.AreNotSame(Foo(int.MaxValue), Foo(int.MaxValue));\r\n \r\n    async Task&lt;int&gt; Foo(int n) =&gt; n;\r\n}<\/pre>\n<p>You <strong>should not rely on this behavior too much<\/strong> but its good to know that the language and framework authors try their best to fine-tune performance in every possible way. Caching a task is a common optimization pattern that is used in other places as well. For instance, new <a href=\"https:\/\/github.com\/dotnet\/corefx\/blob\/12e6bb4a7f525323b827e3ee0d26bdd2691c6a34\/src\/System.Net.Sockets\/src\/System\/Net\/Sockets\/Socket.Tasks.cs#L27\"><code>Socket<\/code><\/a> implementation in <a href=\"https:\/\/github.com\/dotnet\/corefx\/\">corefx repo<\/a> heavily relies on this optimization and uses <a href=\"https:\/\/github.com\/dotnet\/corefx\/blob\/12e6bb4a7f525323b827e3ee0d26bdd2691c6a34\/src\/System.Net.Sockets\/src\/System\/Net\/Sockets\/Socket.Tasks.cs#L575\">cached tasks<\/a> whenever possible.<\/p>\n<h4>Optimization #2: use <code>ValueTask<\/code><\/h4>\n<p>The optimization mentioned above works only in a few cases. Instead of relying on it, we can use <code>ValueTask&lt;T&gt;<\/code> (**): a special task-like value type that will not allocate if the method completes synchronously.<\/p>\n<p><code>ValueTask&lt;T&gt;<\/code> is effectively a discriminated union of <code>T<\/code> and <code>Task&lt;T&gt;<\/code>: if the &#8220;value task&#8221; is completed then the underlying value would be used. If the underlying promise is not finished yet, then the task would be allocated.<\/p>\n<p>This special type helps to avoid unnecessary heap allocations when the operation completes synchronously. To use <code>ValueTask&lt;T&gt;<\/code> we just need to change the return type of <code>GetStockPriceForAsync<\/code> from <code>Task&lt;decimal<\/code> to <code>ValueTask&lt;decimal&gt;<\/code>:<\/p>\n<pre class=\"lang:default decode:true\">public async ValueTask&lt;decimal&gt; GetStockPriceForAsync(string companyId)\r\n{\r\n    await InitializeMapIfNeededAsync();\r\n    return DoGetPriceFromCache(companyId);\r\n}<\/pre>\n<p>And now we can measure the difference with this additional benchmark:<\/p>\n<pre class=\"lang:default decode:true \">[Benchmark]\r\npublic decimal GetStockPriceWithValueTaskAsync_Await()\r\n{\r\n    return _stockPricesThatYield.GetStockPriceValueTaskForAsync(\"MSFT\").GetAwaiter().GetResult();\r\n}<\/pre>\n<pre class=\"lang:default decode:true \">                          Method |     Mean | Scaled |  Gen 0 | Allocated |\r\n-------------------------------- |---------:|-------:|-------:|----------:|\r\n      GetPricesDirectlyFromCache | 1.260 us |   0.90 |      - |       0 B |\r\n                GetStockPriceFor | 1.399 us |   1.00 |      - |       0 B |\r\n           GetStockPriceForAsync | 1.552 us |   1.11 | 0.0267 |      88 B |\r\n GetStockPriceWithValueTaskAsync | 1.519 us |   1.09 |      - |       0 B |<\/pre>\n<p>As you may see the <code>ValueTask<\/code>-based version is just a bit faster than the <code>Task<\/code>-based version. The main difference is the lack of heap allocations. We&#8217;ll discuss in a moment whether it worth doing this switch or not, but before that, I would like cover one tricky optimization.<\/p>\n<h4>Optimization #3: avoid async machinery on a common path<\/h4>\n<p>If you have an extremely widely used async method and you want to reduce the overhead even more, you may consider the following optimization: you can remove <code>async<\/code> modifier, check the task&#8217;s state inside the method and perform the entire operation synchronously without dealing with async machinery at all.<\/p>\n<p>Sounds complicated? Let&#8217;s look at the example.<\/p>\n<pre class=\"lang:default decode:true \">public ValueTask&lt;decimal&gt; GetStockPriceWithValueTaskAsync_Optimized(string companyId)\r\n{\r\n    var task = InitializeMapIfNeededAsync();\r\n \r\n    \/\/ Optimizing for acommon case: no async machinery involved.\r\n    if (task.IsCompleted)\r\n    {\r\n        return new ValueTask&lt;decimal&gt;(DoGetPriceFromCache(companyId));\r\n    }\r\n \r\n    return DoGetStockPricesForAsync(task, companyId);\r\n \r\n    async ValueTask&lt;decimal&gt; DoGetStockPricesForAsync(Task initializeTask, string localCompanyId)\r\n    {\r\n        await initializeTask;\r\n        return DoGetPriceFromCache(localCompanyId);\r\n    }\r\n}<\/pre>\n<p>In this case, the method <code>GetStockPriceWithValueTaskAsync_Optimized<\/code> does not have <code>async<\/code> modifier and when it gets the task from <code>InitializeMapIfNeededAsync<\/code>method it checks whether the task is completed or not. If the task is completed, it just calls <code>DoGetPriceFromCache<\/code> to get the results immediately. But if the initialization task is still running, it calls the local function to await the results.<\/p>\n<p>Using a local function is not the only option but one of the simplest one. But there is a caveat. The most natural implementation of the local function would capture an enclosing state: the local variable and the argument:<\/p>\n<pre class=\"lang:default decode:true \">public ValueTask&lt;decimal&gt; GetStockPriceWithValueTaskAsync_Optimized2(string companyId)\r\n{\r\n    \/\/ Oops! This will lead to a closure allocation at the beginning of the method!\r\n    var task = InitializeMapIfNeededAsync();\r\n \r\n    \/\/ Optimizing for acommon case: no async machinery involved.\r\n    if (task.IsCompleted)\r\n    {\r\n        return new ValueTask&lt;decimal&gt;(DoGetPriceFromCache(companyId));\r\n    }\r\n \r\n    return DoGetStockPricesForAsync();\r\n \r\n    async ValueTask&lt;decimal&gt; DoGetStockPricesForAsync()\r\n    {\r\n        await task;\r\n        return DoGetPriceFromCache(companyId);\r\n    }\r\n}<\/pre>\n<p>But unfortunately, due to <a href=\"https:\/\/github.com\/dotnet\/roslyn\/issues\/18946\">a compiler bug<\/a> this code would allocate a closure even when the method completes on a common path. Here how this method looks under the hood:<\/p>\n<pre class=\"lang:default decode:true \">public ValueTask&lt;decimal&gt; GetStockPriceWithValueTaskAsync_Optimized(string companyId)\r\n{\r\n    var closure = new __DisplayClass0_0()\r\n    {\r\n        __this = this,\r\n        companyId = companyId,\r\n        task = InitializeMapIfNeededAsync()\r\n    };\r\n \r\n    if (closure.task.IsCompleted)\r\n    {\r\n        return ...\r\n    }\r\n \r\n    \/\/ The rest of the code\r\n}<\/pre>\n<p>As we discussed in <a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-local-functions-in-c-7\/\">&#8220;Dissecting the local functions in C#&#8221;<\/a> the compiler uses the shared closure instance for all locals\/arguments in the given scope. So this code generation kind-of makes sense, but it makes the whole fight with heap allocations useless.<\/p>\n<p><strong>TIP<\/strong> <em>This optimization is very tricky. The benefits are very small and even you write the original local function <strong>right<\/strong> you can easily make the change in the future and accidentally capture an enclosing state causing a heap allocation. You still may use the optimization if you work on a highly reusable library like BCL on a method that will be definitely used on hot paths. <\/em><\/p>\n<h4>The overhead of awaiting the task<\/h4>\n<p>So far we&#8217;ve covered only one specific case: the overhead of an async method that completes synchronously. This was intentional. &#8220;Smaller&#8221; the async method is, more visible the overhead would be for its overall performance. Fine-grained async methods tend to do less work and tend to complete synchronously more often. And we tend to call them more frequently.<\/p>\n<p>But we should know the overhead of the async machinery when a method &#8220;awaits&#8221; a non-completed task. To measure this overhead we change <code>InitializeMapIfNeededAsync<\/code> to call <code>Task.Yield()<\/code> even when the cache is initialized:<\/p>\n<pre class=\"lang:default decode:true \">private async Task InitializeMapIfNeededAsync()\r\n{\r\n    if (_stockPricesCache != null)\r\n    {\r\n        await Task.Yield();\r\n        return;\r\n    }\r\n \r\n    \/\/ Old initialization logic\r\n}<\/pre>\n<p>Let&#8217;s extend our performance benchmark suite with the following methods:<\/p>\n<pre class=\"lang:default decode:true \">[Benchmark]\r\npublic decimal GetStockPriceFor_Await()\r\n{\r\n    return _stockPricesThatYield.GetStockPriceFor(\"MSFT\");\r\n}\r\n \r\n[Benchmark]\r\npublic decimal GetStockPriceForAsync_Await()\r\n{\r\n    return _stockPricesThatYield.GetStockPriceForAsync(\"MSFT\").GetAwaiter().GetResult();\r\n}\r\n \r\n[Benchmark]\r\npublic decimal GetStockPriceWithValueTaskAsync_Await()\r\n{\r\n    return _stockPricesThatYield.GetStockPriceValueTaskForAsync(\"MSFT\").GetAwaiter().GetResult();\r\n}<\/pre>\n<pre class=\"lang:default decode:true \">                          Method |     Mean | Scaled |  Gen 0 | Allocated |\r\n-------------------------------- |---------:|-------:|-------:|----------:|\r\n      GetPricesDirectlyFromCache | 1.260 us |   0.90 |      - |       0 B |\r\n                GetStockPriceFor | 1.399 us |   1.00 |      - |       0 B |\r\n           GetStockPriceForAsync | 1.552 us |   1.11 | 0.0267 |      88 B |\r\n GetStockPriceWithValueTaskAsync | 1.519 us |   1.09 |      - |       0 B |<\/pre>\n<p>As we can see the difference is way more visible, both in terms of speed and memory. Here is a short explanation of the results.<\/p>\n<ul>\n<li>Each &#8216;await&#8217; operation for an unfinished task takes about 4us and allocates almost 300B (**) per invocation. This explains why <code>GetStockPriceFor<\/code> is almost twice as fast than <code>GetStockPriceForAsync<\/code> and why its allocate less memory.<\/li>\n<li>A <code>ValueTask<\/code>-based async method is a bit slower than a <code>Task<\/code>-based async method when the method is not completed synchronously. The state machine of a <code>ValueTask&lt;T&gt;<\/code>-based method needs to keep more data compared to a state machine for a <code>Task&lt;T&gt;<\/code>-based method.<\/li>\n<\/ul>\n<p>(**) It depends on the platform (x64 vs. x86), and a number of local variables\/arguments of the async method.<\/p>\n<h4>Async methods performance 101<\/h4>\n<ul>\n<li>If the async method completes synchronously the performance overhead is fairly small.<\/li>\n<li>If the async method completes synchronously the following memory overhead will occur: for <code>async Task<\/code> methods there is no overhead, for <code>async Task&lt;T&gt;<\/code>methods the overhead is 88 bytes per operation (on x64 platform).<\/li>\n<li><code>ValueTask&lt;T&gt;<\/code> can remove the overhead mentioned above for async methods that complete synchronously.<\/li>\n<li>A <code>ValueTask&lt;T&gt;<\/code>-based async method is a bit faster than a <code>Task&lt;T&gt;<\/code>-based method if the method completes synchronously and a bit slower otherwise.<\/li>\n<li>A performance overhead of async methods that await non-completed task is way more substantial (~300 bytes per operation on x64 platform).<\/li>\n<\/ul>\n<p>And, as always, measure first. If you see that an async operation causes a performance problem, you may switch from <code>Task&lt;T&gt;<\/code> to <code>ValueTask&lt;T&gt;<\/code>, cache a task or make a common execution path synchronous if possible. But you may also try to make your async operations coarser grained. This can improve performance, simplify debugging and overall make your code easier to reason. <strong>Not every small piece of code has to be asynchronous<\/strong>.<\/p>\n<h4>Additional references<\/h4>\n<ul>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-async-methods-in-c\/\">Dissecting the async methods in C#<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/extending-the-async-methods-in-c\/\">Extending the async methods in C#<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/corefx\/issues\/4708#issuecomment-160658188\">Stephen Toub&#8217;s comment about <code>ValueTask<\/code>&#8216;s usage scenarios<\/a><\/li>\n<li><a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/dissecting-the-local-functions-in-c-7\/\">&#8220;Dissecting the local functions in C#&#8221;<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>The async series Dissecting the async methods in C#. Extending the async methods in C#. The performance characteristics of the async methods in C#. One user scenario to rule them all. In the last two blog posts we&#8217;ve covered the internals of async methods in C# and then we looked at the extensibility points the [&hellip;]<\/p>\n","protected":false},"author":4004,"featured_media":37840,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[6700,128],"tags":[6695],"class_list":["post-1085","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net-internals","category-performance","tag-seteplia"],"acf":[],"blog_post_summary":"<p>The async series Dissecting the async methods in C#. Extending the async methods in C#. The performance characteristics of the async methods in C#. One user scenario to rule them all. In the last two blog posts we&#8217;ve covered the internals of async methods in C# and then we looked at the extensibility points the [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts\/1085","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/users\/4004"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/comments?post=1085"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/posts\/1085\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/media\/37840"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/media?parent=1085"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/categories?post=1085"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/premier-developer\/wp-json\/wp\/v2\/tags?post=1085"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}