<?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>DEV Community: Conner Phillis</title>
    <description>The latest articles on DEV Community by Conner Phillis (@connerphillis).</description>
    <link>https://dev.to/connerphillis</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1051657%2Fb017e3b2-1cbf-47e2-9adc-690d1c3d3927.jpeg</url>
      <title>DEV Community: Conner Phillis</title>
      <link>https://dev.to/connerphillis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/connerphillis"/>
    <language>en</language>
    <item>
      <title>The Hidden Scaling Trap in Entity Framework's .Contains</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:40:31 +0000</pubDate>
      <link>https://dev.to/connerphillis/the-hidden-scaling-trap-in-entity-frameworks-contains-1ppe</link>
      <guid>https://dev.to/connerphillis/the-hidden-scaling-trap-in-entity-frameworks-contains-1ppe</guid>
      <description>&lt;p&gt;tl;dr; - If Entity Framework processes a query that holds greater than 2100 parameters with a &lt;code&gt;.Contains&lt;/code&gt;, Entity Framework generates a query that SQL Server cannot adequately plan around, and it can absolutely devastate performance.&lt;/p&gt;

&lt;p&gt;Let me cut straight to the point on this one - there is a subtle behavior in Entity Framework Core that you may or may not be aware of when you're using .Contains with SQL Server that can absolutely tank your performance.&lt;/p&gt;

&lt;p&gt;SQL Server has a 2100 parameter limit. If you didn't know that, you should either A) be counting yourself lucky your queries are small enough where you've never had to think about that, or B) you might be feeling nervous about the contents of the rest of this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  How &lt;code&gt;.Contains&lt;/code&gt; Works in SQL Server
&lt;/h3&gt;

&lt;p&gt;Whenever you use .Contains - EF Core compiles that query in one of two ways, depending on the number of items in the collection that you are filtering by. &lt;/p&gt;

&lt;p&gt;Option #1 comes up when the number of parameters in your query (as a total) is &lt;em&gt;less&lt;/em&gt; than 2100. In that case, SQL generates a &lt;code&gt;WHERE ... IN&lt;/code&gt; clause with each of the parameters - it would look something like this:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;UserKey&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value512&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Option #2 comes up whenever your number of parameters exceeds the 2100 parameter limit. In that case, Entity Framework cheats by using the SQL Server Table value function &lt;code&gt;OPENJSON&lt;/code&gt;. Those queries are generated to look something like this&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;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'["1", "2", "3", ... "2500"]'&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;UserKey&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;OPENJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;these queries may not be exactly accurate, I'm doing this from memory, please be kind :)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In EF Core 8, dotnet introduced the WHERE IN syntax for all .Contains queries. Immediately after this was released in dotnet 8, people began to complain that this new query compiler behavior was absolutely &lt;em&gt;wrecking&lt;/em&gt; their query performance &lt;a href="https://github.com/dotnet/efcore/issues/32394" rel="noopener noreferrer"&gt;(#32394)&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the problem
&lt;/h3&gt;

&lt;p&gt;To get into the &lt;em&gt;why&lt;/em&gt; of what caused the degradation in performance - we need to take a quick detour into how the query planner and Table Value Functions in SQL Server work.&lt;/p&gt;

&lt;p&gt;In order to determine the best way to evaluate a query - SQL Server generates query plans which are meant to determine the "best" strategies that it should use when working with a given set of tables. When Table Value Functions are used, SQL Server isn't able to natively get heuristics about the "table" that the function will return, so it uses a best guess in the range of 0 - 100. In our case - 50 was chosen.&lt;/p&gt;

&lt;p&gt;Some of you may be beginning to spot the issue. Something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;wait, so I'm passing in &amp;gt; 2100 parameters, but SQLs best guess is 50? That doesn't add up...&lt;/em&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And you, astute reader, are perfectly on track!&lt;/p&gt;

&lt;p&gt;See - with this erroneous estimate, SQL has lost its ability to correctly plan for the query that it will be executing. For us - we believe that this manifested as SQL deciding that it didn't need to create a hash set for the JSON parameter collection as it believed that spooling over the parsed table wouldn't incur many additional cycles. Which was woefully wrong.&lt;/p&gt;

&lt;p&gt;Targeting a table with millions of rows, queries that had fewer than 2100 parameters were able to select their results in ~ 250 ms. Queries that used the &lt;code&gt;OPENJSON&lt;/code&gt; strategy could take up to &lt;strong&gt;six minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means that as your application continues to expand, the data you query could approach a point where an otherwise innocuous LINQ query suddenly absolutely &lt;em&gt;tanks&lt;/em&gt; your performance and usability. All of this without any sort of warning to the contrary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do other database providers have the same problem?
&lt;/h3&gt;

&lt;p&gt;🤷 No clue - I haven't profiled other database providers since my work is predominantly in SQL Server and my personal projects that use postgres don't have near the amount of required data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moving Forward
&lt;/h3&gt;

&lt;p&gt;I don't believe in presenting an issue without a potential solution. &lt;/p&gt;

&lt;p&gt;We were able to work around this issue with a short-term and a long-term fix.&lt;/p&gt;

&lt;p&gt;The short term fix was to chunk all of our queries. If we detected that we were going to call .Contains on a collection that would exceed the parameter limit, we dispatched &lt;code&gt;CEIL( CollectionSize / 2048 )&lt;/code&gt; queries - each with their own slice of the collection's data. Obviously this was less than ideal, as making N database roundtrips is a performance problem in of itself - though the N still didn't approach anywhere near the 6 minutes that we had regressed to.&lt;/p&gt;

&lt;p&gt;The long term fix was quite a bit more technical - and I'd be happy to write more about it if there is interest.&lt;/p&gt;

&lt;p&gt;What we found was that we could get our performance back if we could somehow shape the query that is going to the database in the format of&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;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;jsonArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[...]'&lt;/span&gt;

&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;tableParameter1&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; 
    &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;OPENJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Value0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="s1"&gt;'$'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;memory again, sorry for the fragmented query!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tableParameter1&lt;/code&gt; is what it says on the tin - a parameter that has been declared to store an ad-hoc table instead of a scalar value.&lt;/p&gt;

&lt;p&gt;Unfortunately, Entity Framework is hard-coded to only output a single &lt;code&gt;SELECT&lt;/code&gt;, so there was no way for us alter the query to form that shape...&lt;/p&gt;

&lt;p&gt;...is what I would have said if &lt;a href="https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors" rel="noopener noreferrer"&gt;interceptors&lt;/a&gt; hadn't been added in Entity Framework Core 3!!! &lt;/p&gt;

&lt;p&gt;With interceptors we have the ability to augment the actual query text that is being made to SQL Server. Typically these are used for adding things like query hints or creating some sort of common soft delete code. In our case, we were able to completely replace the occurrence of the &lt;code&gt;OPENJSON&lt;/code&gt; table value functions, and replace them with our table parameter.&lt;/p&gt;

&lt;p&gt;In order to make sure that we didn't globally change the behavior of &lt;code&gt;.Contains&lt;/code&gt; throughout the app (even if it may be warranted), we employed Entity Framework's &lt;a href="https://learn.microsoft.com/en-us/ef/core/querying/tags" rel="noopener noreferrer"&gt;query tags&lt;/a&gt; to add an extension method on &lt;code&gt;IQueryable&lt;/code&gt; to make sure that only queries we wanted to rewrite got rewritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Afterword
&lt;/h3&gt;

&lt;p&gt;I don't want this to come off as negative towards Entity Framework itself or the Entity Framework team. So far I have managed to maintain a very healthy career using the building blocks that the maintainers have provided us for free, and EF has to serve a massive number of use cases and providers. This particular change reads as well intentioned. A slow query beats the old Entity Framework behavior of just throwing an exception. My guess is that once the regression was discovered in teh wild, rolling it back was no longer a clean option.&lt;/p&gt;

&lt;p&gt;Still, when the failure mode is a six minute query with zero warning, I think that there needs to be some sort of bulletin &lt;em&gt;somewhere&lt;/em&gt; about what can happen whenever the parameter counts begin to get too large for what SQL Server can support. Maybe its part of an MSDN doc, or a warning message issued via &lt;code&gt;ILogger&lt;/code&gt;. Maybe it's this article itself (Hi! 👋). Who knows? &lt;/p&gt;

&lt;p&gt;Either way - this behavior is subtle, and it seems to strike without warning. Code that is performing fine one day can all ofa sudden just fall apart leaving you scratching your head when the pager goes off. IMO - people should know.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>No Indexes, No Parameters, No Problem</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Tue, 07 Apr 2026 20:56:16 +0000</pubDate>
      <link>https://dev.to/connerphillis/no-indexes-no-parameters-no-problem-73k</link>
      <guid>https://dev.to/connerphillis/no-indexes-no-parameters-no-problem-73k</guid>
      <description>&lt;p&gt;This story is from an organization I worked at a while back. A team had recently lost &lt;em&gt;all&lt;/em&gt; of its developers after they voluntarily left the company. Big red flag - but I was young and eager to prove myself, so I volunteered to take it on. The app had a multitude of issues: poor separation of concerns, copy-pasted code everywhere, awful performance, and outdated tech&lt;/p&gt;

&lt;p&gt;I could probably write a whole series of posts on that application - it ended up costing me quite a bit of hair - but I wanted to talk about one of the craziest things I found.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bug
&lt;/h3&gt;

&lt;p&gt;The first order of business was to fix the bugs that were preventing us from moving forward with customers that were using the software. There was a pretty standard create request that would fail somewhere inside of a 9,000 line function (yes... really...) for an unknown reason and customers were losing data. It had to be fixed.&lt;/p&gt;

&lt;p&gt;The debugging tooling was terrible. The server was at the company's data center with no remote debugger access, so I had to build locally, drop DLLs on the machine loaded with logger statements, and read the output to figure out what went wrong. To make matters worse, the API always returned a 200 after swallowing the exception regardless of whether it succeeded or failed. Unfortunately, the fastest way to verify my changes was to query the database directly and check if the row inserted.&lt;/p&gt;

&lt;p&gt;The primary key for the newly inserted record was something like &lt;code&gt;205&lt;/code&gt;, while the one previously was something like &lt;code&gt;190&lt;/code&gt;. This was weird - the entire time that I was debugging the application we hadn't even gotten to the point where we were inserting rows, there's no way we should have skipped that record.&lt;/p&gt;

&lt;p&gt;The issue was fixed - so I had time to figure out why it was doing that. I hadn't previously gone line by line on the function, in favor of focusing on the area of the code that was failing. At the start of the function, was an ominous looking function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DbUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNextId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TableName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  dbo.Ids
&lt;/h3&gt;

&lt;p&gt;Weird - I thought, why would we need a function to get the next id? SQL will create that for us, right?&lt;/p&gt;

&lt;p&gt;The function definition contained a bit of SQL that looked something like this:&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;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;table-name&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;followed by&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;table-name&amp;gt;`
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To date, I have yet to see anything in my career that made my jaw hit the floor that badly. I quickly selected against &lt;code&gt;dbo.ids&lt;/code&gt; and found that &lt;em&gt;every single table&lt;/em&gt; in the database had an entry in this singular table. Every single entity had to call &lt;code&gt;DbUtils.GetNextId&lt;/code&gt; to get its next id. &lt;/p&gt;

&lt;p&gt;Not only were we not using auto-incrementing primary keys, the operation to get the next id wasn't even atomic. If two calls came in at the same time and selected before one updated, two entities got the same id!!&lt;/p&gt;

&lt;p&gt;Of course, I thought, at least the primary key constraint on the destination table should enforce uniqueness and keep us from getting into a bad state like that - that's just common sense, right?&lt;/p&gt;

&lt;p&gt;Just in case, I opened SSMS &amp;amp; expanded the target table's indexes. &lt;/p&gt;

&lt;p&gt;And it was empty.&lt;/p&gt;

&lt;p&gt;I did a double-take, reloaded SSMS, and still empty. I checked my permissions to the database, nothing wrong - I was logged in as an SA. I checked another table - and empty there too.&lt;/p&gt;

&lt;p&gt;Not one index in the entire database. Not a foreign key, a unique constraint, a primary key, nothing.&lt;/p&gt;

&lt;p&gt;The dev prior to me hadn't known what an index was - and instead of doing research to figure out if there was a better way to do what he wanted, he just crapped out something that seemed like it worked.&lt;/p&gt;

&lt;p&gt;Customers had always complained that bulk operations took forever - and now I knew why. Every single row insertion required at least three round trips to the database: two for the manual ID generation, and one for the actual insert. If only there was a better way...&lt;/p&gt;

&lt;h3&gt;
  
  
  The Aftermath
&lt;/h3&gt;

&lt;p&gt;I ended up doing an audit of the entire codebase after that. It got worse. Not a single database parameter in the whole application. Every query was built with raw string concatenation. We were wide open to SQL injection.&lt;/p&gt;

&lt;p&gt;There were enough bad findings in that application that I had to talk my boss in to putting an immediate moratorium on further sales. We had to rebuild the entire thing from scratch. &lt;/p&gt;

&lt;p&gt;There were further issues to fix at customers we'd already deployed to, and the rewrite took at ton of work - but at the end of day cleaning up that hot pile of garbage taught me more than any class in my university could have. As many sleepless nights as it took - I would have to say that without that experience I wouldn't have learned nearly as much as I know today. In the end, it led me to a career I enjoy that keeps me happy and fed.&lt;/p&gt;




&lt;p&gt;Anyways - I've always enjoyed reading software horror stories. Figured it was about time that I wrote my own worst software story. Thanks for reading - feel free to tell me about your own in the comments. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;no AI was used to write this post, this is just how I type lol&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>Optimizing CI/CD Pipelines: How Dynamic JavaScript Configuration Streamlined Our Deployments</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Mon, 17 Feb 2025 14:00:12 +0000</pubDate>
      <link>https://dev.to/connerphillis/one-build-multiple-environments-how-we-optimized-cicd-with-settingsjs-4fjb</link>
      <guid>https://dev.to/connerphillis/one-build-multiple-environments-how-we-optimized-cicd-with-settingsjs-4fjb</guid>
      <description>&lt;p&gt;I used to frequently run in to problems building CI / CD pipelines at both my previous and my current organization. &lt;/p&gt;

&lt;p&gt;Often times, I would be building a website that would need to be deployed multiple times, either internally or externally. Years ago, when I was researching how to support rollouts like this online, the general guidance that I found was that I should be creating a .env file for each environment, and then letting my build process sub in those environment variables.&lt;/p&gt;

&lt;p&gt;Years ago, the general advice I came across was to use a &lt;code&gt;.env&lt;/code&gt; file for each environment, and have the build process substitute the appropriate environment variables. While this worked in some cases, I quickly realized that managing separate builds for each environment was inefficient. Especially when deploying the same website to multiple locations. This approach was increasing the cost of our rented CI/CD agents and artifact storage, and adding unnecessary complexity to our workflows&lt;/p&gt;

&lt;p&gt;One possible solution that we piloted was to try to load the configuration into an API that would be fetched at runtime. While this seemed like a viable solution, it felt problematic. The primary issue was that we'd need to run an additional fetch call in our app, which slowed down our load times. Plus, we had to modify every API request or introduce guards to ensure that the configuration was loaded beforehand. This added unnecessary complexity and potential failure points.&lt;/p&gt;

&lt;p&gt;What I eventually settled on was creating a JavaScript file to inject these settings into globalThis. By including this file at the start of the document, we ensure that the configuration is available as soon as the app code begins executing. This method provides a few key benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single Build, Multiple Deployments:&lt;/strong&gt; Since you only need to run one build, you can generate your deployment artifacts once, and then customize the settings for each environment by using unique &lt;code&gt;settings.js&lt;/code&gt; files for each deployment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Need for Extra Checks:&lt;/strong&gt; With the configuration already loaded when the app starts, there's no need to worry about checking whether the configuration has been successfully loaded. The app can rely on it being available immediately, which simplifies the code and removes unnecessary validation steps."&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="c"&gt;&amp;lt;!-- index.html --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./assets/favicon.svg"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/x-icon"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/settings.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/main.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- remaining site assets ...  --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, you can see that the &lt;code&gt;settings.js&lt;/code&gt; file is loaded in the same breath as the &lt;code&gt;main.js&lt;/code&gt; file, inside of that &lt;code&gt;settings.js&lt;/code&gt; file we have the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// settings.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;siteSettings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// sso&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;client-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;tenant-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audienceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;audience-id&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// api&lt;/span&gt;
  &lt;span class="na"&gt;apiAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;api-address&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// auditing&lt;/span&gt;
  &lt;span class="na"&gt;metricsEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;metrics-endpoint&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;siteSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this script is loaded, it declares a new property against globalThis which holds all of our site specific configuration.&lt;/p&gt;

&lt;p&gt;A few notes that I should add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NEVER EVER store sensitive configuration information in your settings.js files&lt;/strong&gt; - If you do this, it's essentially broadcasting your secrets to the world. This probably goes without saying, but its better that I spell this out before someone blames me for their secrets getting leaked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Potential Namespace Pollution&lt;/strong&gt; - I recommend that you choose a unique name (not configuration) when assigning to globalThis or window. There is no guarantee that one of your dependencies hasn't decided that &lt;em&gt;they&lt;/em&gt; want to use &lt;code&gt;globalThis.configuration&lt;/code&gt; to hold some global state that it needs. You should use a &lt;code&gt;globalThis&lt;/code&gt; key that gives you reasonable assurance that no other code is writing to it. Think &lt;code&gt;globalThis.&amp;lt;my-app-name&amp;gt;_settings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep it lean&lt;/strong&gt; - don't put anything in your &lt;code&gt;settings.js&lt;/code&gt; file that you wouldn't put in a &lt;code&gt;.env&lt;/code&gt; file. This file should be lightweight, no massive base64 strings for your favicon, or other weird stuff like that. Keep it to keys and values so your users have no idea that there was a couple lines of extra JS loaded.&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;I've found this approach great for simplifying CI/CD pipelines. It has reduced the total executions of our build pipeline and made it much easier for us to manage our release builds. &lt;/p&gt;

&lt;p&gt;It does come with trade-offs; there are some security risks (particularly with exposing sensitive configuration values) and potential conflicts in the global namespace. However, it is important to note that these are not risks introduced by the method, rather they are risks inherent to developing public applications. &lt;/p&gt;

&lt;p&gt;By ensuring sensitive data is handled separately and by implementing best practices around naming and performance, these drawbacks can be mitigated.&lt;/p&gt;

&lt;p&gt;As always, feel free to critique this in the comments. I am always happy to create revisions and offer corrections. I recognize that this is a rather niche option that most people wouldn't be able to take advantage of, but I still think it should be published for people with similar issues.&lt;/p&gt;




&lt;p&gt;note - My organization uses a template &lt;code&gt;settings.js&lt;/code&gt; file and replaces the individual keys of the settings using environment variables. If there is interest in this approach I can post and link that approach as well.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Sequential GUIDs in Entity Framework Core Might Not Be Sequential</title>
      <dc:creator>Conner Phillis</dc:creator>
      <pubDate>Fri, 24 Mar 2023 16:15:57 +0000</pubDate>
      <link>https://dev.to/connerphillis/sequential-guids-in-entity-framework-core-might-not-be-sequential-3408</link>
      <guid>https://dev.to/connerphillis/sequential-guids-in-entity-framework-core-might-not-be-sequential-3408</guid>
      <description>&lt;p&gt;&lt;em&gt;Edit April 27, 2023 -&lt;/em&gt; The Entity Framework Core team has since opened &lt;a href="https://github.com/dotnet/efcore/issues/30753" rel="noopener noreferrer"&gt;this issue&lt;/a&gt; in response to this article. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Background
&lt;/h3&gt;

&lt;p&gt;Our customers more often than not chose to host our application on their own machines, so we frequently get asked what the minimum hardware requirements are. We based the estimates we provide on the requirements of similar applications.&lt;/p&gt;

&lt;p&gt;With our latest release we decided we'd get a conclusive answer for ourselves, so we put some resources into running benchmarks. We settled on a simple setup. Write a simple script that would simulate a series of requests that would run through hot paths, and see how many operations we could complete in a fixed time frame. The script would run X number of concurrent requests for N minutes, log the statistics to a CSV file and export our results to a CSV file for analysis.&lt;/p&gt;

&lt;p&gt;We architected our test server to simulate an organization stressed for resources. On a single virtual machine we installed SQL Server, IIS, and our application. For the hardware behind the virtual machine we used an Azure F4s v2 (4 vCPU, 8GB).&lt;/p&gt;

&lt;p&gt;For our warm up, we ran the script with 20 concurrent tasks for 10 minutes, the results that we got?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;263724 total requests completed
67431 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this may not seem like a lot for some, this was great for us. We consider our workloads somewhat computationally expensive, and didn't imagine we would get these sort of numbers out of our code. Especially when hosting the server and database on the same machine.&lt;/p&gt;

&lt;p&gt;Our logs indicated that we were on average consuming about 70% of the CPU. The data that we got was plenty for us to determine our hardware requirements, but just for fun we decided to see how far we could push it. We resized the VM to an F8s V2 (8 vCPU 16GB) expecting linear results.&lt;/p&gt;

&lt;p&gt;The script was set, 50 concurrent tasks instead of 20 to account for the increase in core count, running for ten minutes. The results?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;275532 total requests completed
68883 form submissions in 10 minute across 50 tasks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;What!?!?&lt;/em&gt;&lt;/strong&gt; We doubled the hardware, 2.5x'd the number of concurrent runs, and ended up with only ~3% more completed requests. This set off an alarm for us, we obviously had a large issue with the scalability of our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Investigating the Issue
&lt;/h3&gt;

&lt;p&gt;The first thing that we theorized was that the increased number of tasks was causing problems with IIS, causing connections to stay open for longer than they should. We altered our the parameters of our test script to use 20 tasks over 10 minutes, mirroring the test against the F4s machine. After 10 minutes, the results were...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;275916 total requests completed
68979 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;The same??&lt;/em&gt;&lt;/strong&gt; There was only a marginal difference in the results. Less than 1% from the original run. The test machine was hardly using a fraction of the processing power and network it could utilize. Something bigger was afoot.&lt;/p&gt;

&lt;p&gt;We started a Remote Desktop session with the server and ran another test, 10 minutes, 20 cores. We observed SQL Server start by consuming ~30% of our CPU time, and watched it move up to as much as 60% of the CPU by the end of the run. Over time, our performance was getting &lt;em&gt;worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On a whim, we ran a query to check for index fragmentation of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2Fx7jtah0y6v1rqyh8ppob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2Fx7jtah0y6v1rqyh8ppob.png" alt="Production Index Fragmentation" width="800" height="765"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The index fragmentation was far above what could be expected out of a healthy database. North of 50% for some indexes. While we can't &lt;em&gt;prove&lt;/em&gt; right now that this is what is causing our scaling issue&lt;sup&gt;1&lt;/sup&gt; it does explain how SQL server can continuously need more resources. As the size of the data grows, SQL is having to spend more time doing table scans and expending more resources on IO.&lt;/p&gt;

&lt;p&gt;We found this puzzling, we were using Entity Framework Core's &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.valuegeneration.sequentialguidvaluegenerator?view=efcore-7.0" rel="noopener noreferrer"&gt;&lt;code&gt;Sequential Guid Value Generator&lt;/code&gt;&lt;/a&gt; With the &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.schema.databasegeneratedoption?view=net-7.0" rel="noopener noreferrer"&gt;&lt;code&gt;DatabaseGeneratedOption.Identity&lt;/code&gt;&lt;/a&gt; option. The documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generates sequential Guid values optimized for use in Microsoft SQL server clustered keys or indexes, yielding better performance than random values. This is the default generator for SQL Server Guid columns which are set to be generated on add.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's important to note in addition to this documentation for those that aren't aware, setting a column to use a GUID as a key with &lt;code&gt;DatabaseGeneratedOption.Identity&lt;/code&gt; &lt;strong&gt;does not mean that it will be generated by the database&lt;/strong&gt;. Instead, EF Core generates the sequential GUID itself, and then inserts it into the database (&lt;a href="https://weblogs.asp.net/ricardoperes/current-limitations-of-entity-framework-core#:~:text=For%20GUIDs%2C%20EF%20Core%20automatically%20generates%2C%20on%20the,makes%20it%20database-specific%20%E2%80%93%20currently%2C%20SQL%20Server%20only." rel="noopener noreferrer"&gt;read here&lt;/a&gt;). This can be observed when comparing GUIDs generated normally to those generated by &lt;code&gt;NEWSEQUENTIALID&lt;/code&gt; later in this post.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;a href="https://github.com/dotnet/efcore/pull/20528#issuecomment-612889464" rel="noopener noreferrer"&gt;this issue&lt;/a&gt; in the EF core repository shows that EF core generates GUIDs &lt;em&gt;better&lt;/em&gt; than SQL Server does. The documentation wasn't lining up with what we were seeing, it was time to recreate the EF tests, and see if we could simulate the behavior we were getting from our server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running our Own Benchmarks
&lt;/h3&gt;

&lt;p&gt;The first thing we did was see if we could reproduce the test done by &lt;a href="https://github.com/roji" rel="noopener noreferrer"&gt;roji&lt;/a&gt; on the EF core team with 100000. And...&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Average page space used in %&lt;/th&gt;
&lt;th&gt;Average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NEWSEQUENTIALID&lt;/td&gt;
&lt;td&gt;99.91 %&lt;/td&gt;
&lt;td&gt;1.04 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF Core Sequential Guid Value Generator&lt;/td&gt;
&lt;td&gt;99.86 %&lt;/td&gt;
&lt;td&gt;0.56 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same results as the team found. The EF Core value generator is still generating GUIDs optimally as of SQL Server 2022.&lt;/p&gt;

&lt;p&gt;But wait... this isn't really how a web server works. Entities aren't just inserted one after another when coming from a web server. Entries are created in response to user activity, and that can happen whenever. Database activity happens spontaneously, whenever a user performs an action, and different user hardware can mean these operations can take different amounts of time. What if we modify the test, instead to simulate a large degree of parallel actions rather than pure sequential inserts? &lt;/p&gt;

&lt;p&gt;We altered our script, instead of inserting 100,000 sequential ids into the database, we created 20 tasks, and told each of those tasks to insert 5000 rows into the database. Once this was done we looked at index fragmentation again.&lt;/p&gt;

&lt;h5&gt;
  
  
  Parallel Entity Framework Sequential Guid Generation
&lt;/h5&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;57.93 %&lt;/td&gt;
&lt;td&gt;44.53 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Multithreaded Simulation Code&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlogContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureDeletedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCreatedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;globalCtx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlogContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Interlocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Blog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Foo"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogContext&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt; &lt;span class="n"&gt;Blogs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnConfiguring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Server=.;Database=Testing;Trusted_Connection=true;Encrypt=false;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Blog&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The top 10 results returned when querying the database illuminate the issue: &lt;br&gt;
&lt;a href="https://media2.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%2Fnuyhgc7qg7toj7y0fovt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2Fnuyhgc7qg7toj7y0fovt.png" alt="Top 10 Results From SequentialGuidValueGenerator" width="508" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our conclusion? &lt;strong&gt;Entity Framework seeks to create an efficient value generation strategy optimized for SQL Server, but after the network stack has its say, its likely that some rows will be inserted out of their original order.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compare that to the results that you get when running the same code, but setting &lt;code&gt;HasDefaultValueSql("NEWSEQUENTIALID()")&lt;/code&gt; in the &lt;code&gt;OnModelCreating&lt;/code&gt; method in the database context:&lt;/p&gt;
&lt;h5&gt;
  
  
  Parallel Guid Generation with NEWSEQUENTIALID()
&lt;/h5&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;96.03 %&lt;/td&gt;
&lt;td&gt;7.67 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fragmentation percentage is still not as good as inserting the rows one after the other, and the average page space used is a bit lower, but I think we can all agree that it's better than generating the IDs in memory with Entity Framework Core.&lt;/p&gt;

&lt;p&gt;This method has drawbacks too, however. Looking at the GUIDs that SQL generates it's hard to say that they have the same uniqueness guarantee that standard GUIDs have. It appears that the leading bits of the GUIDs are all that change when taking a sample of the first 10 inserted in the database after our concurrent test:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2F74vgm9xj7u3rnih586q5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2F74vgm9xj7u3rnih586q5.png" alt="NEWSEQUENTIALID Results" width="608" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(in case anyone is curious, generating the GUIDs randomly led to a fragmentation percentage of almost 99%)&lt;/p&gt;
&lt;h3&gt;
  
  
  Studying the Issue
&lt;/h3&gt;

&lt;p&gt;There were two main benefits that initially brought us to use GUIDs as primary keys in our database.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We sometimes have to export data across servers, so the (near) uniqueness guarantee meant that it should be trivial to merge the data&lt;/li&gt;
&lt;li&gt;Certain actions don't require our users to be connected to our server all the time as long as they do a periodic sync. In this case we could let the client generate IDs and after the sync turn the IDs into sequential ones. Once we were done with the transformation we just had to inform the client of the new IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unfortunately, the SQL server GUIDs don't seem like they would be able to cut it for us, as it seems likely that a collision could occur when exporting from one server to another.&lt;/p&gt;

&lt;p&gt;This led us to a tough crossroad. Do we&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep going, knowing that scaling up our application leads to highly diminishing returns necessitating expensive hardware OR&lt;/li&gt;
&lt;li&gt;Lose the benefits GUIDs give us in favor of another primary key format that would be better suited for parallel inserts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, we decided that our best path forward was to go with a hybrid approach. We would alter our tables to have two IDs where GUIDs are required. This involved using an integer primary key generated by the database, and GUID value as a non-clustered index with a unique constraint. These GUIDs would use the &lt;code&gt;SequentialGuidValueGenerator&lt;/code&gt; to try to "presort" some of the items in the non-clustered index, but we wouldn't enforce that it had to be a sequential GUID.&lt;/p&gt;

&lt;p&gt;After performing our parallel benchmark, we ended up with the following results:&lt;/p&gt;
&lt;h6&gt;
  
  
  Hybrid Key Generation Approach
&lt;/h6&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;average page space used in %&lt;/th&gt;
&lt;th&gt;average fragmentation in percent&lt;/th&gt;
&lt;th&gt;Record Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;94.15 %&lt;/td&gt;
&lt;td&gt;10.38 %&lt;/td&gt;
&lt;td&gt;100000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just in case we ran the benchmark again with only an integer primary key, that yielded a fragmentation percentage of almost exactly 12%. It really just seems that some fragmentation is unavoidable in a parallel context.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Great Key Migration
&lt;/h3&gt;

&lt;p&gt;Armed with the results of the benchmarks we had ran, we decided that we would make a gamble. Every table that we had that used a GUID primary key we would alter to contain an auto-incrementing integer primary key, and a GUID UniqueId column with a unique constraint enforced. We would still use the Entity Framework Core GUID value generator to create these unique Ids so to reduce the amount of work SQL would have to do maintaining the unique constraint.&lt;/p&gt;

&lt;p&gt;In the end, it took roughly two weeks of work, and by the end we had modified 600 files according to Git. We ran the benchmark again with the new composite keys and our test script outputted the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;334192 total requests completed
83548 form submissions in 10 minutes across 20 tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This absolutely shocked us. We had more than doubled our throughput, obtaining a total boost of &lt;em&gt;~24%&lt;/em&gt; by changing our code to use integer primary keys instead of GUIDs.&lt;/p&gt;

&lt;p&gt;Furthermore, our 8 core results showed a near-linear increase of 153,076 submissions, and further analysis showed that the processor wasn't being 100% utilized in this benchmark. Some may say the time investment or the risk involved isn't worth it, but in our minds, the tradeoff we got was more than worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing
&lt;/h3&gt;

&lt;p&gt;I'd like to end this post with a couple of acknowledgements.&lt;/p&gt;

&lt;p&gt;First, I don't believe that using the sequential id generator strategy is bad. The Entity Framework Core team's benchmarks show that it does great work in a purely sequential workload. As long as you aren't expecting a high degree of parallelism, it seems that they are perfectly fine as a primary key. Even if you do have a parallel workload, its still possible to reorganize your clustered indexes.&lt;/p&gt;

&lt;p&gt;Second, I want to acknowledge that its totally possible that this is all a coincidence, and that the GUIDs weren't the cause of the performance issues that we were seeing in SQL Server. It's our belief that it's the culprit. It's also of secondary importance for us to raise awareness that the assumption that we made, that because &lt;code&gt;SequentialGUidValueGenerator&lt;/code&gt; uses a strategy optimized for sequential access in SQL server, that GUIDs aren't always going to be &lt;em&gt;inserted&lt;/em&gt; sequentially.&lt;/p&gt;

&lt;p&gt;Lastly, I encourage anyone who reads this to look into the methods enclosed and run their own benchmarks to draw their own conclusions. If there is a flaw in my methods I'm happy to make an edit or publish a correction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thank You!
&lt;/h3&gt;

&lt;p&gt;Thank you for reading my first blog post, please let me know what worked, and what didn't&lt;/p&gt;

&lt;p&gt;-- Conner&lt;/p&gt;

&lt;p&gt;&lt;a&gt; 1&lt;/a&gt; It still perplexes us as to how it didn't show up on the smaller machine. It's possible (spoiler) that since we had less cores we had a lesser degree of parallelism, so rows were not being inserted out of order as bad.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sqlserver</category>
      <category>entityframeworkcore</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
