<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: DEAC European Data Center Operator</title>
    <description>The latest articles on Forem by DEAC European Data Center Operator (@deacdatacenter).</description>
    <link>https://forem.com/deacdatacenter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F5473%2F2fcf58af-7863-407b-bccf-8899a58f4ee3.jpg</url>
      <title>Forem: DEAC European Data Center Operator</title>
      <link>https://forem.com/deacdatacenter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/deacdatacenter"/>
    <language>en</language>
    <item>
      <title>SQL Server Query Optimizer. And how it can hang your business.</title>
      <dc:creator>Jānis Veinbergs</dc:creator>
      <pubDate>Wed, 03 Aug 2022 09:55:00 +0000</pubDate>
      <link>https://forem.com/deacdatacenter/sql-server-query-optimizer-and-how-it-can-hang-your-business-5cjb</link>
      <guid>https://forem.com/deacdatacenter/sql-server-query-optimizer-and-how-it-can-hang-your-business-5cjb</guid>
      <description>&lt;p&gt;Dynamics AX works well until it comes to a grinding halt. Without any software or hardware configuration change. How come?&lt;/p&gt;

&lt;p&gt;Enter SQL Server Query Optimizer. And how to tame it.&lt;/p&gt;

&lt;p&gt;So occasionally some part of functionality looses performance (i.e you can never finish your operation, it just hangs). Sometimes it impacts so many areas that various real life operations are severely impacted. Having experience administering this particular system meant I should look at SQL Server that causes most of performance issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the issue
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The first thing is to look at queries which take most of the time in particular timeframe. So someone complains something is going bad - I look at last 2 hours of data collected with SQL Sentry (Now &lt;a href="https://www.sentryone.com/"&gt;SentryOne&lt;/a&gt;)&lt;code&gt;*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A-ha, some very suspicious query - average execution time is ~25s. Moreover it does many physical reads (reads from disk, data not cached into RAM). This was very easy to spot, but sometimes you get bad queries with 100ms execution time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SHiO4hTq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlsitfl1piha7r6so7ml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SHiO4hTq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlsitfl1piha7r6so7ml.png" alt="Query Stats - Top SQL - identifying bad query performance" width="880" height="305"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;P1&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;P2&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;P3&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;list_of_224_columns&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;CUSTINVOICEJOUR&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; 
&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;DATAAREAID&lt;/span&gt;&lt;span class="o"&gt;=@&lt;/span&gt;&lt;span class="n"&gt;P1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;LEDGERVOUCHER&lt;/span&gt;&lt;span class="o"&gt;=@&lt;/span&gt;&lt;span class="n"&gt;P2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INVOICEDATE&lt;/span&gt;&lt;span class="o"&gt;=@&lt;/span&gt;&lt;span class="n"&gt;P3&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;A super simple query if you ask me, albeit pretty wide one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Lets look at the query plan. On Query History page I can right click and press Open Plan. At first I see a Clustered Index Seek and nothing else - looks perfect. &lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DfhKK7vN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fsb07ogvsj8h7p1du6is.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DfhKK7vN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fsb07ogvsj8h7p1du6is.png" alt="Query Plan - Index Seek" width="385" height="114"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until I put my mouse over that index to see what columns it seeks over. I look at Seek Predicates (columns which utilize index) and Predicate (columns which could utilize index, but does not). And we see only 1 column &lt;code&gt;DATAAREAID&lt;/code&gt; at Seek Predicates, which is a very rough data categorization column with only a few distinct values in there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZLIqyI-K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfdh77hkkjf5trv6e5a1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZLIqyI-K--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xfdh77hkkjf5trv6e5a1.png" alt="Query Plan - Index Seek Details" width="566" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So it is more like data scanning most of the table rows each time this query executes. At previous screenshot we see it did that 180 times over period of ~2hrs.&lt;br&gt;
What we ideally want to see there is many Seek Predicate columns and possibly few Predicate ones (but it depends on the query whether it is possible). So we are dealing with &lt;a href="https://docs.microsoft.com/en-us/azure/azure-sql/identify-query-performance-issues?view=azuresql#ParamSniffing"&gt;Queries that have parameter sensitive plan (PSP)&lt;/a&gt; problems.&lt;/p&gt;

&lt;p&gt;To get a sense of how indexes are used, what does clustered index mean, what is key lookup (or bookmarked lookup) look at &lt;a href="https://vladmihalcea.com/clustered-index/"&gt;Clustered Index&lt;/a&gt; article.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;So culprit is identified (diagnostics is usually most of the work). Isn't there a better index for this query to use? Let's check. Execute &lt;code&gt;sp_helpindex 'dbo.CUSTINVOICEJOUR'&lt;/code&gt; - or better yet &lt;code&gt;sp_sqlskills_helpindex 'dbo.CUSTINVOICEJOUR'&lt;/code&gt; (&lt;a href="https://www.sqlskills.com/blogs/kimberly/use-these-updates-to-sqlskills-index-procedures/"&gt;sp_sqlskills_helpindex&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MoKB58Jk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mugmg3sxcgwdzb4sjyxc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MoKB58Jk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mugmg3sxcgwdzb4sjyxc.png" alt="sp_sqlskills_helpindex result" width="880" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From query plan we saw it uses &lt;code&gt;INVOICENUMIDX&lt;/code&gt; which is clustered index and it can only use first column for seek. However there is much better index: &lt;code&gt;LEDGERVOUCHERIDX&lt;/code&gt; which has all 3 key columns that could be used for seeking: &lt;code&gt;DATAAREAID&lt;/code&gt;, &lt;code&gt;LEDGERVOUCHER&lt;/code&gt; and &lt;code&gt;INVOICEDATE&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why are we facing this issue?
&lt;/h2&gt;

&lt;p&gt;Why did SQL Server choose obviously much more worse index? My theory is as follows: The Query plan estimated (out of &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/statistics/statistics?view=sql-server-ver16"&gt;STATISTICS&lt;/a&gt;) that there will be only 1 row. Why? If you look at query plan XML, you can see on what query parameters (Actual values for &lt;code&gt;@P1&lt;/code&gt;, &lt;code&gt;@P2&lt;/code&gt;, &lt;code&gt;@P3&lt;/code&gt;) were present when SQL decided to generate &amp;amp; cache this particular query plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;                &lt;span class="nt"&gt;&amp;lt;ParameterList&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;ColumnReference&lt;/span&gt; &lt;span class="na"&gt;Column=&lt;/span&gt;&lt;span class="s"&gt;"@P3"&lt;/span&gt; &lt;span class="na"&gt;ParameterCompiledValue=&lt;/span&gt;&lt;span class="s"&gt;"'2022-07-26 00:00:00.000'"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;ColumnReference&lt;/span&gt; &lt;span class="na"&gt;Column=&lt;/span&gt;&lt;span class="s"&gt;"@P2"&lt;/span&gt; &lt;span class="na"&gt;ParameterCompiledValue=&lt;/span&gt;&lt;span class="s"&gt;"N'&amp;lt;something&amp;gt;'"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;ColumnReference&lt;/span&gt; &lt;span class="na"&gt;Column=&lt;/span&gt;&lt;span class="s"&gt;"@P1"&lt;/span&gt; &lt;span class="na"&gt;ParameterCompiledValue=&lt;/span&gt;&lt;span class="s"&gt;"N'DMT'"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/ParameterList&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@P1&lt;/code&gt; corresponds to &lt;code&gt;DATAAREAID&lt;/code&gt;. Lets look at statistics - how much rows does SQL server expect to find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;SHOW_STATISTICS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dbo.CUSTINVOICEJOUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'DATAAREAID'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Could&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;locate&lt;/span&gt; &lt;span class="k"&gt;statistics&lt;/span&gt; &lt;span class="s1"&gt;'DATAAREAID'&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt; &lt;span class="n"&gt;catalogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alright, no statistics for particular column - we have to look at index statistics then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;DBCC&lt;/span&gt; &lt;span class="n"&gt;SHOW_STATISTICS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'dbo.CUSTINVOICEJOUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'I_062LEDGERVOUCHERIDX'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5Sza5Y2g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlp8uoqk92mjf3fpkmds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5Sza5Y2g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlp8uoqk92mjf3fpkmds.png" alt="SHOW_STATISTICS histogram result" width="736" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sorry I'm not showing you actual values, however none of the rows say &lt;code&gt;DMT&lt;/code&gt;. Basically, when SQL server updates statistics (it looks at each 5th row or whatever percentage of rows you ask SQL to look at) - and it didn't see even one where there would be DATAAREAID = DMT. Thus SQL Server estimates there is 1 row (it never estimates 0). However SQL thinks: well, if I'll get 1 row from index LEDGERVOUCHERIDX using only first key column DATAAREAID, I'll have to lookup all those selected columns within clustered index INVOICENUMIDX - why not just directly get that row from INVOICENUMIDX which also begins with DATAAREAID and spare a key lookup (as all the data will already be within the page)? (&lt;a href="https://www.brentozar.com/blitzcache/expensive-key-lookups/"&gt;Understanding Key Lookups&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;For these particular parameters SQL Server has really found the best way to return the data, no questions about that.&lt;/p&gt;

&lt;p&gt;However, the problem (or a feature) is that SQL Server caches this query plan. Suppose DATAAREAID now would be something that statistics shows us we have 352 million rows - and SQL will now get each of this row, filter out linearly INVOCIEDATE and LEDGERVOUCHER (because chosen index doesn't have data arranged in a way that index can be utilized to not read that data in the first place).&lt;/p&gt;

&lt;h2&gt;
  
  
  Solutions?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How about &lt;strong&gt;preventing SQL server cache this particular query plan at all&lt;/strong&gt;? It is possible to do with &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance/plan-guides?view=sql-server-ver16"&gt;plan guides&lt;/a&gt;, but it is a bad idea: if query is set to execute multiple hundreds of times per second, then query plan compilation adds &amp;gt;10ms of time to execute. Ideally this query would execute in ~1ms time (working few years on the same system kind of tells you how long that simple query should take), so we don't want at LEAST 10x overhead to query execution. &lt;strong&gt;Bad idea&lt;/strong&gt; in this case. But could work for queries that get executed few times OR queries that by itself takes 50ms or more to run.&lt;/li&gt;
&lt;li&gt;Previously mentioned &lt;a href="https://docs.microsoft.com/en-us/azure/azure-sql/identify-query-performance-issues?view=azuresql#ParamSniffing"&gt;Queries that have parameter sensitive plan (PSP)&lt;/a&gt; lists other hints we could use, but I'll go over and use my favorite, most stable option.&lt;/li&gt;
&lt;li&gt;Tell SQL Server to &lt;strong&gt;use particular index using plan guides&lt;/strong&gt;. That's what we are going to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What are plan guides? As documentation puts it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Plan guides let you optimize the performance of queries when you cannot or do not want to directly change the text of the actual query in SQL Server. Plan guides influence the optimization of queries by attaching query hints or a fixed query plan to them. Plan guides can be useful when a small subset of queries in a database application provided by a third-party vendor are not performing as expected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perfect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I cannot directly change the query&lt;/li&gt;
&lt;li&gt;I can influence query optimization&lt;/li&gt;
&lt;li&gt;Subset of queries from third-party vendor query is not performing well. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating plan guide
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_create_plan_guide&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'[QNW-319-445612-custinvoicejour-perf]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SELECT &amp;lt;list_of_224_columns&amp;gt; FROM CUSTINVOICEJOUR A WHERE ((DATAAREAID=@P1) AND ((LEDGERVOUCHER=@P2) AND (INVOICEDATE=@P3)))'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'SQL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'@P1 nvarchar(5),@P2 nvarchar(21),@P3 datetime'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;hints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'OPTION(TABLE HINT(A, INDEX(I_062LEDGERVOUCHERIDX)))'&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When plan guide is created, it immediately invalidates the existing plan and when such query comes in, generates new query plan taking into account plan guide hints.&lt;/p&gt;

&lt;p&gt;Now we must validate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Whether new query plan uses this plan guide (i.e we provided correct &lt;code&gt;@stmt&lt;/code&gt; and &lt;code&gt;@params&lt;/code&gt; vars for &lt;code&gt;sp_create_plan_guide&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;What is the performance of the query - better or worse?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To validate the first point, we must get query from plan cache and find PlanGuideName attribute within XML. Here is a query that, among some statistical stuff, has uses_plan_guide column and actually provides a way to open query plane and validate that plan guide is in use:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--irE4v4xw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idkapg6qfztnwfn54bi7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--irE4v4xw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/idkapg6qfztnwfn54bi7.png" alt="Query plan XML - finding PlanGuideName" width="880" height="93"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;execution_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_plan_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query_plan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_plan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;creation_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_execution_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_logical_reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_logical_reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_logical_reads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_logical_writes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_logical_writes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_logical_writes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;avg_logical_reads&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_elapsed_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;min_elapsed_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;max_elapsed_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;avg_elapsed_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;min_worker_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;max_worker_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;avg_worker_time_ms&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;force_index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;CHARINDEX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' INDEX('&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uses_plan_guide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;CHARINDEX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;' PlanGuideName='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;create_plan_guide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sp_create_plan_guide_from_handle @name = N&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;[provide_name]&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;, @plan_handle = '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CONVERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;', @statement_start_offset = '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;-- you can use this statement to force this query use currently cached query plan&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; 
   &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_stats&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;
   &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_sql_text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qt&lt;/span&gt;
   &lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_text_query_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
    &lt;span class="n"&gt;qt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'%&amp;lt;list_of_224_columns&amp;gt; FROM CUSTINVOICEJOUR A WHERE ((DATAAREAID=@P1) AND ((LEDGERVOUCHER=@P2) AND (INVOICEDATE=@P3)))'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this query actually gives answer to the second question too - &lt;code&gt;avg_elapsed_time_ms&lt;/code&gt; is of intereset. For me it reads &lt;strong&gt;0,32ms&lt;/strong&gt;. I can't ask for more! Moreover, within 41 seconds it has &lt;code&gt;execution_count&lt;/code&gt; of 515 times. &lt;strong&gt;Compare that to 249 executions in 186 minutes...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In fact, the job completed so fast, it is no more to be seen on SQL Sentry query stats which has configured 5 seconds of total duration for it to be included. So unless it executes 15k+ times within a minute, I'm not seeing it there.&lt;/p&gt;

&lt;p&gt;By the way, if you ever delete/rename index, that doesn't mean SQL Server will error out executing that query - it will disregard the plan guide when compiling query plan. There are performance counter to monitor to know when you have stale plan guides: &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance-monitor/sql-server-sql-statistics-object?view=sql-server-ver16"&gt;Misguided Plan Executions/sec&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft is on it
&lt;/h2&gt;

&lt;p&gt;This is certainly a flaw of SQL Server being dumb. However since SQL Server 2017 and onwards, Microsoft is tackling performance issues with some &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance/intelligent-query-processing?view=sql-server-ver16"&gt;intelligent query processing&lt;/a&gt; which includes various feedback mechanisms that SQL Server keeps an eye on whether the particular query plan strategy at hand is good enough and adjusts accordingly. These look promising:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance/cardinality-estimation-sql-server?view=sql-server-ver16#cardinality-estimation-ce-feedback"&gt;Cardinality Estimation (CE) feedback&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance/intelligent-query-processing-details?view=sql-server-ver16#parameter-sensitivity-plan-optimization"&gt;Parameter Sensitivity Plan Optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/azure-sql/identify-query-performance-issues?view=azuresql#parameter-sensitivity"&gt;Automatic tuning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If only the application supported latest-and-greatest SQL Server... so I haven't been able to test out those features.&lt;/p&gt;

&lt;p&gt;Anyways, I'm happy that the results are delivered and the system can continue working until the next query is to be found misbehaving...&lt;/p&gt;

&lt;p&gt;&lt;code&gt;*&lt;/code&gt; Note that for newer SQL Servers there are tools that may assist you with finding bad performing queries, like &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/performance/monitoring-performance-by-using-the-query-store?view=sql-server-ver16"&gt;Query Store&lt;/a&gt;. For more advanced DB Users &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/data-collection/data-collection?view=sql-server-ver16"&gt;Data Collection&lt;/a&gt; could be used or just querying plan cache and sorting by &lt;code&gt;qs.total_elapsed_time&lt;/code&gt; if the query plan is still there. System uses SQL Server 2008R2 that doesn't have Query Store, so using SQL Sentry is just doing the job faster.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>performance</category>
    </item>
    <item>
      <title>Bringing down ASP.NET deployment interruptions from minutes to blip of a time with Azure DevOps</title>
      <dc:creator>Jānis Veinbergs</dc:creator>
      <pubDate>Fri, 01 Apr 2022 08:07:32 +0000</pubDate>
      <link>https://forem.com/deacdatacenter/bringing-down-aspnet-deployment-interruptions-from-minutes-to-blip-of-a-time-with-azure-devops-112i</link>
      <guid>https://forem.com/deacdatacenter/bringing-down-aspnet-deployment-interruptions-from-minutes-to-blip-of-a-time-with-azure-devops-112i</guid>
      <description>&lt;p&gt;This is going to be about improving deployment process from classic file copy to something more effective, automated and low friction for developers and bringing no interruptions to users. &lt;a href="https://martinfowler.com/articles/developer-effectiveness.html" rel="noopener noreferrer"&gt;Maximizing Developer Effectiveness&lt;/a&gt; is actually competitive advantage after all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pain points
&lt;/h2&gt;

&lt;p&gt;Our customer, a national school management platform was experiencing inconvenience when deploying a fairly big classic .NET Framework application to 22 Windows IIS servers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deployments would be taken at night&lt;/li&gt;
&lt;li&gt;If a hotfix had to be issued at day with high load - it could take up to 30 minutes of complete system halt as IIS server farm compiles resources while pile of requests waits in a queue. &lt;/li&gt;
&lt;li&gt;The deployment is not atomic - that is, while files are being copied between v1 and v2, we would get v1.314 - while some files are updated, the others not.&lt;/li&gt;
&lt;li&gt;If we ever wanted a rollback - occasionally files would be locked, exacerbating already bad situation.&lt;/li&gt;
&lt;li&gt;The website itself consists of ~220MB of data across ~2500 number of files. And small file copy is SLOW.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The root cause of this is that .NET resources (.aspx pages, razor views) get &lt;a href="https://docs.microsoft.com/en-us/previous-versions/aspnet/ms178466(v=vs.100)" rel="noopener noreferrer"&gt;compiled to assemblies on first request&lt;/a&gt; whenever they change. When IIS is constantly bombarded with requests to all possible resources, it all must be compiled at the same time on every IIS server. On the other hand, when deployed at night, delay was minor as not many requests came in, and those that did, compiling was handled fairly quickly. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/previous-versions/aspnet/fxk122b4(v=vs.100)" rel="noopener noreferrer"&gt;ASP.NET Requests Queued&lt;/a&gt; is a good performance counter to monitor to identify when user browsers are just waiting for response and how quickly situation resolves after an update. A healthy value is 0 or something close to that. Here you see cumulative request queue for all IIS servers in evening, when load actually is not high. 4 minutes of interruptions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnje638ii0f3fdspbh1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpnje638ii0f3fdspbh1e.png" alt="IIS Request queue"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We thought about using robocopy to copy changed files based on timestamp, but that wouldn't work for us - as there could be multiple persons that would deploy the code. When Visual Studio Publishes the project, files between machines have different timestamps. Rsync would probably help greatly, but then again - some challenge to get that on developers windows machines and windows share server. And if different hosts build files, little differences in build tools may end up with different checksums. So we didn't go down this path.&lt;/p&gt;

&lt;p&gt;The webroot (and &lt;a href="https://docs.microsoft.com/en-us/iis/web-hosting/configuring-servers-in-the-windows-web-platform/shared-configuration_211" rel="noopener noreferrer"&gt;IIS Shared Configuration&lt;/a&gt;) is actually served from a single Windows share, which at least provided a low effort way to conveniently copy files to a single location and keep them, along with the IIS configuration, in sync.&lt;/p&gt;

&lt;p&gt;That's how we lived for years and it was clear that something had to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment strategy
&lt;/h2&gt;

&lt;p&gt;We came to a decision we would like to stop depending on a file share. We already started using Azure DevOps for some other stuff. So we got an idea 💡&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep files on IIS local disk (Duh)&lt;/li&gt;
&lt;li&gt;Azure DevOps Agent would be the one compiling code.&lt;/li&gt;
&lt;li&gt;We would precompile views in advance. Locking wouldn't be an issue as we will copy within a new folder. 💎&lt;/li&gt;
&lt;li&gt;Orchestrate deployment to all IIS servers via &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/release/?view=azure-devops" rel="noopener noreferrer"&gt;Azure DevOps Release Pipelines&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Run all .dll files through &lt;a href="https://docs.microsoft.com/en-us/dotnet/framework/tools/ngen-exe-native-image-generator" rel="noopener noreferrer"&gt;ngen&lt;/a&gt;. That would eliminate additional delay when assembly is first loaded. As C# code compiled is actually &lt;a href="https://docs.microsoft.com/en-us/dotnet/standard/managed-execution-process#compiling-to-msil" rel="noopener noreferrer"&gt;MSIL&lt;/a&gt; - an intermediate language that CPU doesn't understand any of that. JIT compiler is the one that translates that to bytecode for the processor architecture at hand. And it does so usually Just-In-Time or On-The-Fly. Or, in our case, &lt;em&gt;ngen&lt;/em&gt; (Native Image Generator)&lt;/li&gt;
&lt;li&gt;A deployment task would have to copy files within a new folder and then we would change IIS Physical path from where to serve site files. 💎&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So there are some gems there that are enough to solve our pain points. Now let's see some practical ways on achieving all of this!&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing build
&lt;/h2&gt;

&lt;p&gt;Build is the process of generating files we must copy onto IIS servers.&lt;/p&gt;

&lt;p&gt;Before we talk about build &amp;amp; deployment, let's get this straight: We are using &lt;a href="https://azure.microsoft.com/en-us/services/devops/" rel="noopener noreferrer"&gt;Azure DevOps online offering&lt;/a&gt;. However this could as well be &lt;a href="https://azure.microsoft.com/en-us/services/devops/server/" rel="noopener noreferrer"&gt;On-Premises installation&lt;/a&gt; or any other CI/CD platform of your choice. We are, however, using &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops&amp;amp;tabs=browser#install" rel="noopener noreferrer"&gt;self-hosted agents&lt;/a&gt; that perform the build tasks.&lt;/p&gt;

&lt;p&gt;We are talking about solution, which consists of multiple .NET projects. Our main interest is deploying &lt;code&gt;....Web.Application&lt;/code&gt; and &lt;code&gt;..Web.Application.Payments&lt;/code&gt;. The others are mostly dependencies which must also be built. So, the developers used to build app within Visual Studio. When deploying web application, you had to right click on particular project and choose publish. From there, some settings could be configured like whether we want precompilation or not, whether we deploy straight to IIS or filesystem. In our case, it was deployed to filesystem and then copied to production. More about &lt;a href="https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-aspnet-web-app?view=vs-2022&amp;amp;tabs=azure" rel="noopener noreferrer"&gt;Publishing an ASP.NET web app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When writing &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops" rel="noopener noreferrer"&gt;Azure DevOps pipeline&lt;/a&gt;, I have to figure out what kind of MSBuild properties I have to use to invoke the "Publish" process. So, after searching the web, reading &lt;a href="https://docs.microsoft.com/en-us/visualstudio/ide/how-to-view-save-and-configure-build-log-files?view=vs-2022#to-change-the-amount-of-information-included-in-the-build-log" rel="noopener noreferrer"&gt;MSBuild diagnostic output logs&lt;/a&gt;, reading MSBuild &lt;code&gt;.target&lt;/code&gt; files, I'v come up with properties I need.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By the way, here is a quick tip on how to find relevant .target files by some keyword:&lt;/p&gt;


&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gci&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;${env:ProgramFiles(x86)}&lt;/span&gt;&lt;span class="s2"&gt;\Microsoft Visual Studio\"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.targets"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MvcBuildViews"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SimpleMatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;C:\Program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x86&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;\Microsoft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Visual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Studio\2017\Professional\MSBuild\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.targets:849:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CleanupForBuildMvcViews"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;" '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;EnableCleanOnBuildForMvcViews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;'=='true' and '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;MVCBuildViews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;'=='true' "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BeforeTargets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MvcBuildViews"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;C:\Program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x86&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;\Microsoft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Visual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Studio\2019\Professional\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.targets:847:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CleanupForBuildMvcViews"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;" '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;EnableCleanOnBuildForMvcViews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;'=='true' and '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;MVCBuildViews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;'=='true' "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BeforeTargets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MvcBuildViews"&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Or better yet, use &lt;a href="https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.ProjectSystemTools" rel="noopener noreferrer"&gt;Project System Tools&lt;/a&gt; extension with &lt;a href="https://msbuildlog.com/" rel="noopener noreferrer"&gt;MSBuild Binary and Structured Log Viewer&lt;/a&gt; to sneak peek into what MSBuild is doing - you won't regret it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, the relevant "Publish" command within Azure Devops build pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Web.Application&lt;/span&gt;  
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Web.Application'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;/t:Build,GatherAllFilesToPublish&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishProfileName="$(publishProfileName)"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WebPublishMethod=FileSystem&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeleteExistingFiles=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeployOnBuild=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:MvcBuildViews="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PrecompileBeforePublish="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WDPMergeOption="MergeAllOutputsToASingleAssembly"&lt;/span&gt;
      &lt;span class="s"&gt;/p:SingleAssemblyName="Web.Application.Precompiled"&lt;/span&gt;
      &lt;span class="s"&gt;/p:UseMerge="true"&lt;/span&gt;
      &lt;span class="s"&gt;/p:DebugSymbols="True"&lt;/span&gt;
      &lt;span class="s"&gt;/p:EnableUpdateable="False"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishUrl="$(Build.BinariesDirectory)\my"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WPPAllFilesInSingleFolder="$(Build.BinariesDirectory)\my"&lt;/span&gt;
      &lt;span class="s"&gt;/p:RunNpmScripts="$(runNpmScripts)"&lt;/span&gt;
      &lt;span class="s"&gt;/p:AutoParameterizationWebConfigConnectionStrings="false"&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildPlatform)'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArchitecture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x64&lt;/span&gt;
    &lt;span class="na"&gt;logFileVerbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detailed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Note that specifying &lt;code&gt;publishProfileName&lt;/code&gt;, I use the actual publish profile used by Visual Studio Publish process and then override some properties by passing &lt;code&gt;/p&lt;/code&gt; msbuild arguments.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;WebPublishMethod&lt;/code&gt; ensures build files are put within &lt;code&gt;PublishUrl&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RunNpmScripts&lt;/code&gt; is our own custom build property used within &lt;code&gt;.pubxml&lt;/code&gt; to run some npm build process.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MvcBuildViews&lt;/code&gt;, &lt;code&gt;PrecompileBeforePublish&lt;/code&gt;, &lt;code&gt;SingleAssemblyName&lt;/code&gt;, &lt;code&gt;UseMerge&lt;/code&gt;, &lt;code&gt;EnableUpdateable&lt;/code&gt; all relate to precompilation. Before build, I can choose to disable precompilation if I want the build to happen much faster.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AutoParameterizationWebConfigConnectionStrings&lt;/code&gt; - required for copy/paste if publish profile contains connection string replacements. Without this, there will be a placeholder value that must be replaced afterwards. &lt;a href="https://stackoverflow.com/questions/16693500/is-autoparameterizationwebconfigconnectionstrings-option-the-only-way-to-preve" rel="noopener noreferrer"&gt;More on StackOverflow&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full build can be seen here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ASP.NET&lt;/span&gt;
&lt;span class="c1"&gt;# Build and test ASP.NET projects.&lt;/span&gt;
&lt;span class="c1"&gt;# Add steps that publish symbols, save build artifacts, deploy, and more:&lt;/span&gt;
&lt;span class="c1"&gt;# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4&lt;/span&gt;
&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(Build.SourceBranchName) $(Date:yyyyMMdd)$(Rev:.r)&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;win-dev-pool&lt;/span&gt;

&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Precompile&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Precompile&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;webApplicationProject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Web.Application'&lt;/span&gt;
  &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;solution.sln'&lt;/span&gt;
  &lt;span class="na"&gt;buildPlatform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x64'&lt;/span&gt;
  &lt;span class="na"&gt;buildConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release'&lt;/span&gt;
  &lt;span class="na"&gt;releaseArchiveFilename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webapp.7z'&lt;/span&gt;
  &lt;span class="na"&gt;releasePaymentsArchiveFilename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;payments.7z'&lt;/span&gt;
  &lt;span class="c1"&gt;# which .pubxml file to use. Don't append .pubxml&lt;/span&gt;
  &lt;span class="na"&gt;publishProfileName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STAGING&lt;/span&gt;
  &lt;span class="na"&gt;runNpmScripts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;artifactShareName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\\MY-APP-AGENT2\agentpublishedfiles&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetToolInstaller@1&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetCommand@2&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;restore'&lt;/span&gt;
    &lt;span class="na"&gt;restoreSolution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(solution)'&lt;/span&gt;
    &lt;span class="na"&gt;feedsToUse&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;config'&lt;/span&gt;
    &lt;span class="na"&gt;nugetConfigPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;NuGet.Config'&lt;/span&gt;

&lt;span class="c1"&gt;# npm script prerequisites&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeTool@0&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;versionSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;14.x'&lt;/span&gt;

&lt;span class="c1"&gt;# https://docs.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops#nodejsnpm&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache npm&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"$(Agent.OS)"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(webApplicationProject)/package.json'&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(webApplicationProject)/node_modules'&lt;/span&gt;
    &lt;span class="na"&gt;restoreKeys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"$(Agent.OS)"'&lt;/span&gt;
    &lt;span class="na"&gt;cacheHitVar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NPM_CACHE_RESTORED&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), variables.runNpmScripts)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npmAuthenticate@0&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workingFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(webApplicationProject)/.npmrc'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Npm@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;install'&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;install'&lt;/span&gt;
    &lt;span class="na"&gt;workingDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(webApplicationProject)/'&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), variables.runNpmScripts, ne(variables.NPM_CACHE_RESTORED, 'true'))&lt;/span&gt;

&lt;span class="c1"&gt;# Workaround for AspNetPrecompile to skip scanning node_modules directory and finding .c,.cpp,.h files... https://stackoverflow.com/a/20963170/50173&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CmdLine@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hide node_modules.&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;attrib&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+H&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$(webApplicationProject)/node_modules'&lt;/span&gt;

&lt;span class="c1"&gt;# Specificually pass PublishProfileName as empty. Because otherwise in consequent runs, this task will run npm run-script that is specified as BeforeBuild target within publish profile.&lt;/span&gt;
&lt;span class="c1"&gt;# We already run publish actions further.&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build $(solution)&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(solution)'&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildPlatform)'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArchitecture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x64&lt;/span&gt;
    &lt;span class="na"&gt;logFileVerbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detailed&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishProfileName=""&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSTest@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test $(solution)&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildPlatform)'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;
  &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ne(variables.NoTests, &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Target also Build, otherwise BeforeBuild target won't execute. BeforeBuild specifies npm run-script commands. So, this project gets built twice, but project build by itself is fast.&lt;/span&gt;
&lt;span class="c1"&gt;# PublishUrl actually unused as we won't use msdeploy to deploy stuff, just simple copy.&lt;/span&gt;
&lt;span class="c1"&gt;# Precompilation decision matrix: https://docs.microsoft.com/en-us/previous-versions/aspnet/bb398860(v=vs.100)&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Web.Application&lt;/span&gt;  
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Web.Application'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;/t:Build,GatherAllFilesToPublish&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishProfileName="$(publishProfileName)"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WebPublishMethod=FileSystem&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeleteExistingFiles=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeployOnBuild=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:MvcBuildViews="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PrecompileBeforePublish="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WDPMergeOption="MergeAllOutputsToASingleAssembly"&lt;/span&gt;
      &lt;span class="s"&gt;/p:SingleAssemblyName="Web.Application.Precompiled"&lt;/span&gt;
      &lt;span class="s"&gt;/p:UseMerge="true"&lt;/span&gt;
      &lt;span class="s"&gt;/p:DebugSymbols="True"&lt;/span&gt;
      &lt;span class="s"&gt;/p:EnableUpdateable="False"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishUrl="$(Build.BinariesDirectory)\my"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WPPAllFilesInSingleFolder="$(Build.BinariesDirectory)\my"&lt;/span&gt;
      &lt;span class="s"&gt;/p:RunNpmScripts="$(runNpmScripts)"&lt;/span&gt;
      &lt;span class="s"&gt;/p:AutoParameterizationWebConfigConnectionStrings="false"&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildPlatform)'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArchitecture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x64&lt;/span&gt;
    &lt;span class="na"&gt;logFileVerbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detailed&lt;/span&gt;


&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VSBuild@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish Web.Application.Payments&lt;/span&gt;  
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;solution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Web.Application.Payments'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;/t:Build,GatherAllFilesToPublish&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishProfileName="$(publishProfileName)"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WebPublishMethod=FileSystem&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeleteExistingFiles=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:DeployOnBuild=true&lt;/span&gt;
      &lt;span class="s"&gt;/p:MvcBuildViews="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PrecompileBeforePublish="${{ parameters.Precompile }}"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WDPMergeOption="MergeAllOutputsToASingleAssembly"&lt;/span&gt;
      &lt;span class="s"&gt;/p:SingleAssemblyName="Web.Application.Payments.Precompiled"&lt;/span&gt;
      &lt;span class="s"&gt;/p:UseMerge="true"&lt;/span&gt;
      &lt;span class="s"&gt;/p:DebugSymbols="True"&lt;/span&gt;
      &lt;span class="s"&gt;/p:EnableUpdateable="False"&lt;/span&gt;
      &lt;span class="s"&gt;/p:PublishUrl="$(Build.BinariesDirectory)\payments"&lt;/span&gt;
      &lt;span class="s"&gt;/p:WPPAllFilesInSingleFolder="$(Build.BinariesDirectory)\payments"&lt;/span&gt;
      &lt;span class="s"&gt;/p:AutoParameterizationWebConfigConnectionStrings="false"&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;AnyCPU'&lt;/span&gt;
    &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(buildConfiguration)'&lt;/span&gt;
    &lt;span class="na"&gt;msbuildArchitecture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;x64&lt;/span&gt;
    &lt;span class="na"&gt;logFileVerbosity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detailed&lt;/span&gt;

&lt;span class="c1"&gt;# Artifacts&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ArchiveFiles@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Archive $(releaseArchiveFilename)&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rootFolderOrFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.BinariesDirectory)\my'&lt;/span&gt;
    &lt;span class="na"&gt;includeRootFolder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;archiveType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7z'&lt;/span&gt;
    &lt;span class="na"&gt;sevenZipCompression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fastest'&lt;/span&gt;
    &lt;span class="na"&gt;archiveFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/$(releaseArchiveFilename)'&lt;/span&gt;
    &lt;span class="na"&gt;replaceExistingArchive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;verbose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ArchiveFiles@2&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Archive $(releasePaymentsArchiveFilename)&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rootFolderOrFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.BinariesDirectory)\payments'&lt;/span&gt;
    &lt;span class="na"&gt;includeRootFolder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;archiveType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;7z'&lt;/span&gt;
    &lt;span class="na"&gt;sevenZipCompression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fastest'&lt;/span&gt;
    &lt;span class="na"&gt;archiveFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/$(releasePaymentsArchiveFilename)'&lt;/span&gt;
    &lt;span class="na"&gt;replaceExistingArchive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;verbose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@1&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish website deployment Artifacts&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PathtoPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)'&lt;/span&gt;
    &lt;span class="na"&gt;ArtifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;drop'&lt;/span&gt;
    &lt;span class="na"&gt;publishLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FilePath'&lt;/span&gt;
    &lt;span class="na"&gt;TargetPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(artifactShareName)\my'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;What this does, is builds solution (dependency .dlls)&lt;/li&gt;
&lt;li&gt;Builds JavaScript SPA (Single Page App). Commands are buried within MSBuild project.&lt;/li&gt;
&lt;li&gt;Publishes 2 applications (Just generates files on disk)&lt;/li&gt;
&lt;li&gt;7zips those files&lt;/li&gt;
&lt;li&gt;Publish artifact (Azure Devops thingie - makes them available for release pipeline). In this case, they are copied to a share.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementing deployment
&lt;/h2&gt;

&lt;p&gt;Once we have the artifacts ready (archive of website files), we are ready to implement Release. The release must copy given files to some directory and instruct IIS to change base path from where it will server files. We had 2 options to choose from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make Build agent connect to IIS servers and perform the deployment on each IIS server. In this case, we have to write some script that will copy files onto each server and issue some commands to IIS. Moreover, we must control/see whether deployment is successful or not. And the build agent could actually be in another domain, with no direct access to production.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/release/deployment-groups/?view=azure-devops#set-up-agents-on-deployment-groups" rel="noopener noreferrer"&gt;Install Deployment agent&lt;/a&gt; on all target IIS servers. Every host then receives deployment job and does whatever it is instructed to do. We don't have to bother about any other remote communication channel other than Deployment Agent with Azure DevOps server over 443/TCP. Plus we get nice UI of seeing whether deployment succeeded, partially succeeded (if partially, which hosts failed) and on which step it failed. Neat. 
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8fhwnxs74sjoiv4s4in.png" alt="Deploy partially succeeded in Azure DevOps"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We went for the second option. Deployment agent is actually almost same as Build agent, just carries on deployment tasks.&lt;/p&gt;

&lt;p&gt;When creating release pipeline, you get to play with the UI which is nice. &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97a6rcpzso4yesdcr2n3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97a6rcpzso4yesdcr2n3.png" alt="Edit release pipeline in Azure DevOps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you see, I'v split some operations into multiple steps. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Root App Pool&lt;/em&gt; - ensures appropriate application pool is created on IIS. It is actually a one-time step and may have been created beforehand. 
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feb69ybf0ay4lpfcm7q46.png" alt="Ensure app pool in Azure DevOps"&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Copy my files&lt;/em&gt; - PowerShell script to extract 7z files:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Expand-7Zip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArchiveFileName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;System.DefaultWorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;\_Web.Application\drop\webapp.7z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TargetPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;DeployTargetFolder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Ngen my&lt;/em&gt; - runs &lt;code&gt;ngen.exe&lt;/code&gt; on all .dll files found within deployment folder. Also using custom condition so this step which may take a little more than a minute, could be turned off: &lt;code&gt;and(succeeded(), eq(variables.Ngen, 'true'))&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-Alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ngen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;windir&lt;/span&gt;&lt;span class="nx"&gt;\Microsoft.NET\Framework64\&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ngen.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fullname&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeployTargetFolder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*.&lt;/span&gt;&lt;span class="nf"&gt;dll&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ngen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fullname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/nologo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/verbose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Deploy my&lt;/em&gt; - Switchers virtual directory, issues some IIS configuration commands. When this stage is completed, IIS servers new code. 
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1arkdgfc5qbn296tgi62.png" alt="Switch IIS physical path in Azure DevOps"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The great thing is that if any of steps fail for ANY IIS server before Deploy step, deploy won't run for ANY IIS server and they will all be still consistent.&lt;/p&gt;

&lt;p&gt;In case of a &lt;strong&gt;rollback&lt;/strong&gt;, we can open appropriate release and for "Deploy my" stage, press Redeploy. IIS will immediately switch back to appropriate folder.&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fow578k7ord5kugt9dhff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fow578k7ord5kugt9dhff.png" alt="Redeploy stage in Azure DevOps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawbacks
&lt;/h2&gt;

&lt;p&gt;Using cloud hosted solution, if issues do happen, we won't be able to run our deployment pipeline. &lt;a href="https://status.dev.azure.com/_history" rel="noopener noreferrer"&gt;And they do happen&lt;/a&gt;. However, this may impact us on the rare case of rushing some kind of hotfix out. There are 2 steps we can take in this case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change IIS path manually to previous deployment folder (rollback)&lt;/li&gt;
&lt;li&gt;Build via developer Visual Studio and copy appropriate DLL manually. Hotfix usually doesn't involve much code changes and is probably contained within a single or few files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Otherwise we just wait for while DevOps issue gets resolved.&lt;/p&gt;

&lt;h2&gt;
  
  
  End result
&lt;/h2&gt;

&lt;p&gt;Everything is actually configured that, when &lt;code&gt;production&lt;/code&gt; branch gets new code, build is run automatically. After build, deployment is run automatically but stops for an approval. With &lt;a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/integrations/slack?view=azure-devops" rel="noopener noreferrer"&gt;Azure Pipelines Slack app&lt;/a&gt; we just get a message within channel where upon Approve button press, code goes into production.&lt;/p&gt;

&lt;p&gt;This doesn't include database updates, which still must be performed manually, but eventually &lt;a href="https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications?view=sql-server-ver15" rel="noopener noreferrer"&gt;DACPAC deployment&lt;/a&gt; can be incorporated within a pipeline too. But luckily, database updates are more rare than backend code/frontend updates.&lt;/p&gt;

&lt;p&gt;Judging from the Request Queue size, guess where did deployment happen?&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7hd4pqognk6m7lkyex8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7hd4pqognk6m7lkyex8.png" alt="Good IIS Requests queue size after deploy"&gt;&lt;/a&gt;&lt;br&gt;
Yeah, that little spike just before 09:00. Except, vertical axis shows max 30 instead of 3k and horizontally, just a blip of a time.&lt;/p&gt;

&lt;p&gt;For a fair picture I should share another deployment request queue graphic: &lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54gthaq2spv79ie7zgf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F54gthaq2spv79ie7zgf3.png" alt="IIS Request queue size after deploy with some spikes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Queue size spiked to almost 300. However it happens on some IIS servers, in this case 3 out of 10. After 60-90 seconds, queue size dropped, so fairly short period of time compared to what we experienced before. But it's not like those IIS completely stopped processing GET/POST requests - actually only minority of requests queued up. Currently I don't know the cause. Maybe if you have any thoughts on what may cause it, leave down in the comments.&lt;/p&gt;

&lt;p&gt;In the end, the results leave everyone happy!&lt;/p&gt;

&lt;p&gt;On an upcoming post, I'd like to share how these IIS server/Windows OS settings can be installed, managed and kept in sync with &lt;a href="https://docs.microsoft.com/en-us/powershell/dsc/getting-started/wingettingstarted?view=dsc-1.1" rel="noopener noreferrer"&gt;PowerShell DSC&lt;/a&gt;. And you don't have to keep a separate documentation file somewhere that may drift and be outdated in time. Stay tuned! 👋&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>devops</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
