{"id":46215,"date":"2023-06-20T09:30:00","date_gmt":"2023-06-20T16:30:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=46215"},"modified":"2023-06-20T09:06:02","modified_gmt":"2023-06-20T16:06:02","slug":"microsoft-forms-services-journey-to-dotnet-6","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/microsoft-forms-services-journey-to-dotnet-6\/","title":{"rendered":"Microsoft Forms Service\u2019s Journey to .NET 6"},"content":{"rendered":"<p>Microsoft Forms is a product for creating surveys and quizzes. It\u2019s widely used in Microsoft365 Business subscribers. Many Microsoft365 Education subscribers use quizzes to do class tests and homework.<\/p>\n<p><iframe width=\"752\" height=\"423\" src=\"https:\/\/www.youtube.com\/embed\/RGoclZxokJk\" frameborder=\"0\" allowfullscreen><\/iframe><\/p>\n<p>The Forms backend service has several microservices, which handle various workloads (e.g. serving static &amp; dynamic web content, providing REST APIs for Forms web client &amp; integration parties to consume, etc.). These micro services are built up on .NET (predominantly, ASP.NET WebForm\/WebAPI on .NET Framework 4.7.2).<\/p>\n<p>In 2022, we migrated the frontend REST API service to .NET 6, gained near 200% increase in CPU efficiency, and more importantly, refreshed team members\u2019 skillset (e.g. SDK style project file &amp; multi-targeting, ASP.NET Core app development, especially middleware &amp; filters pipeline). Earlier this year, we completed the .NET 6 migration of the backend service, which handles CRUD REST APIs to access data in SQL Azure databases.<\/p>\n<h2>Our approach<\/h2>\n<p>We prepared for the migration over a couple years in two stages. We started with targeting <code>netstandard2.0<\/code> or multi-targeting both <code>net472<\/code> &amp; <code>net6<\/code>.<\/p>\n<ul>\n<li><strong>First stage<\/strong>: most references to HttpContext (and System.Web namespace) were removed.<\/li>\n<li><strong>Second stage<\/strong>: most dependencies were upgraded or replaced to allow .NET Core app to consume, and most projects targeted <code>netstandard2.0<\/code> or multi-targeted <code>net472<\/code> &amp; <code>net6<\/code>.<\/li>\n<\/ul>\n<p>When we finally started migrating the web apps, we found that because of the work spent in stage 1 and 2, little effort was required to remove incompatibilities.<\/p>\n<p>We did, however, need to spend time on the configuration code in the web apps. Previously, configuration data was accessed in an ad-hoc way. Wherever a piece of configuration info was needed, code was written to retrieve it in some way. We took .NET 6 migration as a chance to apply the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/options\">options pattern<\/a>. Now all code structs access configuration data with injected IOptions&lt;&gt; or IOptionsMonitor&lt;&gt;. We found that in this way code readability &amp; maintainability are improved, and it became quite obvious which configuration data a component consumes.<\/p>\n<p>As time went by, we found that it became more and more problematic to target <code>netstandard2.0<\/code> or to multi-target.<\/p>\n<ul>\n<li>More and more dependencies removed <code>net472<\/code>, even .NET Framework build, so we had to stick to old versions or add Condition attributes here and there, in project files.\n<ul>\n<li>And Condition attribute only works for multi-targeting, so we needed to turn more and more <code>netstandard2.0<\/code> to multi-targeting.<\/li>\n<li>Sticking to old version doesn\u2019t always work, since old version starts to have bugs that only show up in .NET Core workload. And newer versions even start to be incompatible (at compile time) to old ones.<\/li>\n<\/ul>\n<\/li>\n<li>Targeting <code>netstandard2.0<\/code> means many .NET Core-only dependencies can\u2019t be used, typically those with better performance &amp; cost.<\/li>\n<li>Even for multi-targeting projects, conditional compilation is more and more needed here and there, for different perf\/cost characteristics of the 2 runtimes. For example, using interpolated string as log content is a bad pattern for <code>net472<\/code>, since that creates a string unconditionally, but for .NET Core, as long as the log method has an overload which accepts interpolated string handler, using interpolated string as log content is the most efficient and expressive way.<\/li>\n<li>Multi-targeting increases compile time. As we still need <code>net472<\/code> build to run legacy apps, the .NET team suggested to avoid targeting <code>netstandard2.0<\/code> and do multi-targeting more often.<\/li>\n<li>The progressive migration also brought us another problem \u2013 incompatibility in Authentication. To keep existing ASP.NET apps untouched, we chose to migrate quite a lot of OWIN and ASP.NET code to build a compatible authentication stack.<\/li>\n<\/ul>\n<h2>The results<\/h2>\n<p>We migrated several ASP.NET apps to ASP.NET Core, and we found that CPU efficiency (we use RPS\/CPU, i.e. RPS per CPU percentage as indicator) improved the most post-migration. We think most improvement was due to updating the hosting model from IIS to Kestrel. We think that\u2019s why the simpler the app, the more CPU efficiency improved. For example, the migration of 2 pretty simple web apps both gave over 400% improvement in CPU efficiency, while the relatively complex backend app, which accesses DB got 100+% improvement (still very prominent, of course).<\/p>\n<p>Another prominent change was the number of threads. We set the minimum worker thread count to 400 for both processes. Previously, the ASP.NET web app pushed it to 400+ in several hours (even quicker sometimes) after startup. Now, the ASP.NET Core app uses 60-80 threads during the entire lifetime.<\/p>\n<h3>Frontend service CPU usage<\/h3>\n<p>The improvements to CPU usage have been significant. In the following chart, the red line represents a service that was upgraded to .NET 6. The blue line represents a service that has remained on .NET Framework. You can see the point where the .NET 6 upgrade occurred and the improvement we delivered.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/06\/frontend-cpu.png\" alt=\"Frontend CPU chart\" \/><\/p>\n<h3>Frontend service P75 latency of a key API<\/h3>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/06\/frontend-latency.png\" alt=\"Frontend GetRuntimeForm P75 latency\" \/><\/p>\n<h3>Backend net472\/net6 service CPU efficiency comparison<\/h3>\n<p>Here, you can clearly see the significant CPU efficiency improvements that result from switching from IIS+ASP.NET (light blue line for RPS, green line for process CPU) to the .NET 6 process (blue line for RPS, orange line for process CPU).\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/06\/backend-efficiency.png\" alt=\"Backend CPU efficiency chart\" \/><\/p>\n<p>In conclusion, you can see prominent CPU efficiency improvement in both frontend and backend, request latency and working set improvements are not as prominent, but also exciting. With these improvements, we have cut down 30+% of our cloud service computation cost.<\/p>\n<p>Most importantly, the team has built new skills upon the modern .NET technology stack. Every team member is happy and excited about that and eagerly pursuing opportunities to take advantage of the new platform, to write more efficient code, and to design and build more attractive features. And now, we\u2019re starting to consider .NET 8!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Microsoft Forms is an Office product with 130M monthly active users, recently we migrated our services to .NET 6, and we have seen 100%-200% increase in CPU efficiency.<\/p>\n","protected":false},"author":121465,"featured_media":46216,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7509,756,7635],"tags":[],"class_list":["post-46215","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspnetcore","category-csharp","category-developer-stories"],"acf":[],"blog_post_summary":"<p>Microsoft Forms is an Office product with 130M monthly active users, recently we migrated our services to .NET 6, and we have seen 100%-200% increase in CPU efficiency.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/46215","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\/121465"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=46215"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/46215\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/46216"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=46215"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=46215"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=46215"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}