Generated by All in One SEO Pro v5.0.0.1, this is an llms-full.txt file, used by LLMs to index the site. # Chris Woody Woodruff | Fractional Architect ## Posts ### [The N+1 Query Problem in EF Core: Detection, Diagnosis, and Permanent Fixes](https://www.woodruff.dev/the-n1-query-problem-in-ef-core/) **Published:** July 7, 2026 **Author:** Chris Woodruff **Content:** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/)An order summary page renders in 40 milliseconds against your development database. The same page takes eleven seconds in production. You open the profiler, and the controller method looks clean. You reread the LINQ, and it reads correctly. You check the indexes, and every column you need is covered. The code passed review. The tests are green. And the page still crawls. The cause is almost always the same defect, and it hides where nobody looks. Here is the shape of it. ``` var orders = await context.Orders .Where(o => o.OrderDate >= startOfMonth) .ToListAsync(); foreach (var order in orders) { // Every read of order.Customer fires its own SELECT Console.WriteLine($"{order.OrderNumber}: {order.Customer.Name}"); } ``` One query loads the orders. Then every trip through the loop touches order.Customer, and every touch fires its own SELECT against the database. Load 200 orders, and you have not run one query. You have run 201. Do the arithmetic that your development database hides from you. Each of those little queries is fast, say three milliseconds round-trip. Two hundred of them in sequence is 600 milliseconds of pure waiting, and that is before the database does a single millisecond of real work. Scale the page to 5,000 orders, and the same code issues 5,001 queries. The controller that felt instant on your laptop now holds a connection open for the better part of a minute. This defect has a name: the N+1 query problem. One query to load the parent rows, then N more to load a related value for each one. It survives code review because every line is idiomatic. It survives testing because test databases hold hundreds of rows, where production holds millions. And it survives profiling right up until a customer complains, because the ORM hides the flood of SQL behind property access that looks like plain C#. This post covers the whole problem: what N+1 actually is, including three shapes that most articles skip, how to catch it on purpose rather than by accident, and the full ladder of permanent fixes in Entity Framework Core 10. [Entity Framework Extensions](https://entityframework-extensions.net/) from [ZZZ Projects](https://zzzprojects.com/) gets honest treatment along the way. The classic read-side N+1 is a problem native EF Core solves completely, and this post says so plainly rather than reaching for a paid library to fix something the free tools already handle. # **The Four Shapes of N+1** Ask most developers to define N+1, and they will describe lazy loading. That answer is correct and incomplete. The same query storm arrives through four different doors, and three of them are immune to the fix people reach for first. Name all four now, so the rest of the post can point at them precisely. **Shape one is lazy loading in a loop.** Navigation property access inside iteration, each access firing a silent query. This is the classic, and it needs lazy loading proxies or an injected ILazyLoader to happen at all. **Shape two is the explicit query loop.** No proxies, no magic. A developer writes context.Customers.FirstOrDefault(c => c.Code == row.Code) inside a foreach and runs it once per incoming record. Import and reconciliation code is full of this shape. It ignores every lazy-loading fix in the book because there is no lazy loading involved. It needs set-based thinking instead. **Shape three is the serializer storm.** Return tracked entities straight from an API action with lazy loading on, and the JSON serializer walks every navigation property while building the response, firing queries after your action method has already returned. The storm comes from a layer nobody thinks to profile. **Shape four is the write-side N+1.** Call SaveChanges once per entity inside a loop, and you have the same defect pointed at inserts and updates instead of reads. Earlier posts in this series cover it in full, so this post names it as family and moves on. The thread joining all four is one sentence: per-item round trips where one set-based operation would have done the job. Hold that sentence. Every fix below is a variation on it. # **Detection: Making the Invisible Visible** Here is the rule that separates engineers from guessers: never diagnose N+1 by feel. Count the queries. A page either issues a bounded number of SQL statements or it does not, and you can measure which in under a minute. Here is the toolkit, ordered from zero setup to production grade. Start with the cheapest look you have. Point EF Core’s logger at the console and read what it emits. ``` optionsBuilder .UseSqlServer(connectionString) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging(); // development only ``` Run the page once and watch the output. If you see the same SELECT against Customers repeated forty times with only the parameter changing, you have found your storm. One change to note in EF Core 10: constant values in logged SQL are redacted by default, so turn on EnableSensitiveDataLogging in development if you want to see the actual parameter values rather than a redacted marker. Reading logs by eye works for a quick check. It does not scale to a test suite, and it will not prevent the defect from recurring. For that, count queries in code with an interceptor. ``` public sealed class QueryCountInterceptor : DbCommandInterceptor { private int _count; public int Count => _count; public void Reset() => Interlocked.Exchange(ref _count, 0); public override InterceptionResult ReaderExecuting( DbCommand command, CommandEventData eventData, InterceptionResult result) { Interlocked.Increment(ref _count); return base.ReaderExecuting(command, eventData, result); } public override ValueTask ReaderExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Interlocked.Increment(ref _count); return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } } ``` Wire that counter into an integration test with a budget, and N+1 stops being a production surprise. ``` [Fact] public async Task Order_summary_stays_within_query_budget() { _queryCounter.Reset(); await _client.GetAsync("/orders/summary"); Assert.True(_queryCounter.Count warnings.Throw( CoreEventId.NavigationLazyLoading, CoreEventId.DetachedLazyLoadingWarning)); ``` Now any lazy load in an environment that bans it throws on the spot during development, instead of degrading quietly in production. The same mechanism catches the multiple-collection warning from the split-query section. When a storm shows up in a database-side capture, and you need to trace it back to the exact line of C# that caused it, tag your queries. ``` var orders = await context.Orders .TagWith("OrderSummary: monthly customer roll-up") .Where(o => o.OrderDate >= startOfMonth) .ToListAsync(); ``` The tag rides along as an SQL comment, so the offending call site is one search away when you are staring at a query captured from SQL Server Extended Events or the query store. Cheap, permanent, and almost nobody uses it. # **Fix Tier 1: Eager Loading with Include** You have found the storm. Now kill it. The first tool on the ladder handles shape one directly. Tell EF Core which related data the code path needs, and it fetches everything in one query instead of N. ``` var orders = await context.Orders .Where(o => o.OrderDate >= startOfMonth) .Include(o => o.Customer) .ToListAsync(); ``` One SELECT with a JOIN replaces the 201 queries from the opening. The customer name is already loaded by the time the loop runs, so no property access fires anything. Need only part of a collection? Filtered Include loads the subset. ``` var orders = await context.Orders .Include(o => o.Items.Where(i => i.IsActive)) .ToListAsync(); ``` For a relationship that nearly every query needs, you can push the Include into the model so you never forget it. ``` modelBuilder.Entity() .Navigation(o => o.Customer) .AutoInclude(); // Opt out on a specific query var orders = await context.Orders .IgnoreAutoIncludes() .ToListAsync(); ``` That convenience has a cost, and honesty demands stating it. Auto-includes hide loading behavior at the call site, so a query that looks cheap in the LINQ can drag three related tables you forgot were configured. When you need the bare entity, IgnoreAutoIncludes opts out per query. Include has a ceiling of its own. It loads whole entities. When your page needs three columns from the order and one from the customer, Include hauls every column of both across the wire and materializes full tracked objects you will never mutate. That waste is the opening for the strongest fix, two tiers down. First, a trap hiding inside the fix you just applied. # **Fix Tier 2: Cartesian Explosion and Split Queries** Include solves N+1. Applied carelessly, it creates a worse problem in its place. Include two sibling collections on the same query and watch what the JOIN does to your row count. ``` var orders = await context.Orders .Include(o => o.Items) .Include(o => o.Payments) .ToListAsync(); ``` Each order with ten items and five payments no longer returns as one row. It returns as fifty, the cross product of both collections, with every order column duplicated across all fifty. Load a thousand such orders, and you have pulled fifty thousand rows across the wire to represent fifteen thousand. You traded a round-trip problem for a data-volume problem, and on wide parent rows, the second problem bites harder than the first. EF Core sees this coming and warns you about it by name, the multiple-collection-include warning. Route that warning through the Throw configuration from the detection section and EF Core refuses to run the cartesian query at all until you make a decision. The decision is one method call. AsSplitQuery breaks the single JOIN into one query per collection. ``` var orders = await context.Orders .Include(o => o.Items) .Include(o => o.Payments) .AsSplitQuery() .ToListAsync(); ``` No row multiplication. Each collection comes back in its own clean result set, and EF Core stitches them together in memory. The trade is real and worth stating: split queries mean multiple round-trip requests, and without a wrapping transaction, they offer no guarantee that all three queries saw the same snapshot of the database. Single queries give you one consistent read at the risk of a cartesian blowup. Neither choice is universally correct, which is exactly why EF Core makes you choose. Set the default once if your app leans one way. ``` optionsBuilder.UseSqlServer(connectionString, o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)); ``` Then override per query wherever the default is wrong, in either direction, with AsSplitQuery or AsSingleQuery. # **Fix Tier 3: Projections, the Strongest Fix** Include and split queries to manage the cost of loading entities. Projection removes the entity from the equation entirely, and with it the whole category of defect. Select exactly the columns the page needs into a shape that is not an entity. ``` var summary = await context.Orders .Where(o => o.OrderDate >= startOfMonth) .Select(o => new OrderSummaryDto { OrderNumber = o.OrderNumber, CustomerName = o.Customer.Name, Total = o.Total }) .ToListAsync(); ``` Read what that query does not do. It does not load Customer as an entity. It does not track anything. It does not leave a single navigation property lying around for a loop or a serializer to trip over. EF Core composes one SQL statement that returns four columns, and the result carries no proxies at all. There is no storm to fix here, because there is nothing in the result for a loop or a serializer to touch. Push aggregates into the database while you are there. ``` var summary = await context.Orders .Where(o => o.OrderDate >= startOfMonth) .Select(o => new OrderSummaryDto { OrderNumber = o.OrderNumber, CustomerName = o.Customer.Name, LineItemCount = o.Items.Count, Total = o.Items.Sum(i => i.Quantity * i.UnitPrice) }) .ToListAsync(); ``` That count and that sum run as SQL on the server. The line items never travel to your process. Compare that to the Include approach, which would have loaded every line item into memory just so you could call Count on the list. Projections nest, so a parent-with-children DTO composes cleanly. ``` var orders = await context.Orders .Select(o => new OrderDto { OrderNumber = o.OrderNumber, Items = o.Items.Select(i => new ItemDto { Sku = i.Sku, Quantity = i.Quantity }).ToList() }) .ToListAsync(); ``` One more benefit rides along for free. A projection to a non-entity type is never tracked, so the change-tracker cost that an earlier post in this series measured in detail simply does not apply here. No snapshots, no identity resolution, no tracked graph eating heap. There is a narrow middle ground worth knowing. When you already hold one entity in memory and need one relationship loaded conditionally, explicit loading fits. ``` var order = await context.Orders.FirstAsync(o => o.Id == id); if (order.RequiresAudit) { await context.Entry(order) .Collection(o => o.Items) .LoadAsync(); } ``` Use it for exactly that case. Put explicit loading inside a loop, and you have rebuilt N+1 by hand with more ceremony. The tool is fine. The loop is the crime. # **Lazy Loading Itself: Keep It or Kill It** Every fix so far treats symptoms of lazy loading. Step back and ask the harder question: should lazy loading be on at all? It reaches your app one of two ways. The proxies package plus virtual navigations. ``` optionsBuilder .UseLazyLoadingProxies() .UseSqlServer(connectionString); ``` Or an injected ILazyLoader for proxy-free lazy loading. Both make navigation property access fire silent queries, and both turn shape one and shape three from possible into likely. The argument for switching it off in the service and API code is direct. Lazy loading defers a database call until some code reads a property. In a request-scoped world, that whenever can land after your action returns, inside the serializer, on a connection you assumed was done. Shape three is not a quirk. It is the predictable result of combining lazy loading with returning entities from endpoints. The structural fix is the projection tier you just read: return DTOs instead of entities, and the serializer has nothing to walk. Lazy loading is not always wrong. A long-lived desktop client with a context that stays open, or an exploratory script where you are poking at data by hand, both benefit from loading relationships on demand. Judge it by the lifetime of your context, not by fashion. If you decide to kill it, here is the checklist. Drop the proxies package or remove the opt-in call. Set context.ChangeTracker.LazyLoadingEnabled to false for any scope where you want it off, but cannot remove the package yet. Then turn on the throw-on-lazy-load event from the detection section, so anything you missed announces itself loudly rather than lurking. # **The Lookup-List N+1 and the Parameter Ceiling** Shape two earns its own section, because the obvious fix runs headfirst into a change EF Core 10 shipped that this series has already documented. Picture import code. Five thousand records arrive from an external system, and for each one, you check whether it already exists. ``` foreach (var row in importedRows) { var existing = context.Customers .FirstOrDefault(c => c.Code == row.Code); // ... one round trip per row, 5,000 times } ``` Five thousand records, five thousand round trips. The same N+1 defect, wearing the clothes of a validation loop. The set-based rewrite is obvious and correct: pull the keys, ask the database once. ``` var codes = importedRows.Select(r => r.Code).ToList(); var existing = await context.Customers .Where(c => codes.Contains(c.Code)) .ToListAsync(); ``` One query. N+1 collapses to 1. And then you deploy it, and it throws in production against the very data volume it was meant to handle. Here is why. EF Core 10 changed how it translates Contains over a collection. EF Core 8 and 9 packed the list into a single JSON parameter and unpacked it server-side with OPENJSON. EF Core 10 moved the default back toward emitting one scalar parameter per element, with a setting to control the strategy. On SQL Server, that runs straight into the hard ceiling of 2,100 parameters per statement. Your Contains query with 500 keys sailed through testing. The same query with 5,000 keys does not run slowly in production. It does not run at all. It throws. The native ways around it are real, and each carries a bill. Chunk the key list into batches under the ceiling and issue one query per chunk, which works and adds round-trip proportional to the number of chunks. ``` var results = new List(); foreach (var chunk in codes.Chunk(2000)) { var found = await context.Customers .Where(c => chunk.Contains(c.Code)) .ToListAsync(); results.AddRange(found); } ``` Switch the parameterized-collection mode back to the JSON translation per query or globally, which restores the single-parameter behavior along with the query-plan trade-offs that motivated the EF Core 10 change in the first place. Or hand-write a temp-table join, which performs well and hands you back exactly the boilerplate an ORM was supposed to remove. Composite keys make it worse. Match incoming rows on a pair like Email plus PhoneNumber, and Contains has no clean translation at all. The usual escape, gluing the fields into one concatenated string key, throws away every index on those columns and turns a seek into a scan. This is the honest edge of what native EF Core does gracefully, and the natural handoff to the next section. # **Where Entity Framework Extensions Fits, Honestly** Say the uncomfortable part first, because the editorial mandate for this series demands it: Entity Framework Extensions is not the fix for the classic lazy loading N+1. Include, split queries, and projections solve shapes one and three completely, natively, and for free. If you came here hoping a paid library would rescue you from a loop that Include already handles, the honest answer is that you do not need one. EFE earns its place at two specific points on this map, both of them past the edge of what native EF Core does cleanly. ## **Large-List Lookups: WhereBulkContains and BulkRead** The parameter ceiling from the previous section is where the first one lands. WhereBulkContains filters a query against an in-memory list of any size, and it does not build a giant IN clause. It bulk-loads your list into a temporary table and joins against it. ``` var existing = context.Customers .WhereBulkContains(importedRows) .ToList(); ``` Any size means any size. Five thousand keys, fifty thousand, more. The temp-table join does not care about the 2,100-parameter limit, because there are no per-element parameters to count. The method is deferred and returns IQueryable, so it composes with Where and Include like any other LINQ operator. Need the results immediately instead of a composable query? BulkRead is the same mechanism with a ToList baked in. ``` var existing = context.Customers.BulkRead(codes); ``` The composite-key case that defeated native Contains is a first-class citizen here. Pass an anonymous type and match on as many columns as you like. ``` var existing = context.Customers .BulkRead(importedRows, x => new { x.Email, x.PhoneNumber }); ``` There is even an overload for the relationship version of the storm, filtering related entities inside an Include while returning all the root rows. ``` var orders = context.Orders .Include(o => o.Items) .WhereBulkContains(o => o.Items, productIds, i => i.ProductId) .ToList(); ``` And the exclusion case, which native code usually writes as a giant NOT IN, translates as a clean NOT EXISTS. ``` var missing = context.Customers .WhereBulkNotContains(importedRows) .ToList(); ``` ## **List-Side Filtering: WhereBulkContainsFilterList** Flip the question around. Instead of which database rows match my list, import code constantly asks which of my list items already exist in the database. Written naively, that is one existence check per item, N+1 all over again. WhereBulkContainsFilterList answers it in one round trip and hands back the matching items from your in-memory list rather than from the table. ``` // Items from the import that already exist in the database var toUpdate = context.Customers .WhereBulkContainsFilterList(importedRows); // Items from the import that are new var toInsert = context.Customers .WhereBulkNotContainsFilterList(importedRows); ``` Its sibling, WhereBulkNotContainsFilterList, returns the items that are missing. Together, they let import code split an incoming batch into update-these and insert-those with two queries total, the read-side companion to the import pipeline covered earlier in the series. ## **Honest Caveats for the EFE Read Methods** None of this is free of edges, and pretending otherwise would break the trust this series runs on. Provider support is the first limit. Per the official documentation, WhereBulkContains and its siblings support SQL Server and PostgreSQL only. On SQLite, MySQL, or Oracle, you are back to the native chunking approach from the previous section. Inheritance is the second. The documentation states that the TPH, TPT, and TPC mapping strategies are not supported by these methods. If your lookup targets an entity in an inheritance hierarchy, test before you commit, because this is exactly the kind of edge a mid-sized codebase hits late and at the worst time. These methods also do not chain onto EF Core’s native ExecuteUpdate or ExecuteDelete. EFE’s own UpdateFromQuery and DeleteFromQuery fill that role and compose with WhereBulkContains directly, covered earlier in this series. And the temp-table machinery has fixed overhead per call. For a list of a few hundred items, plain Contains is simpler and probably faster, because the setup cost of building and populating a temp table is real. The crossover point where WhereBulkContains pulls ahead is a number to measure on your schema, not a slogan to repeat. The benchmark section quantifies it rather than asserting it. One disclosure, stated plainly: Entity Framework Extensions is a paid commercial library with a rolling free trial at entityframework-extensions.net. This post is part of a sponsored series for ZZZ Projects, and the coverage above is written to read the same whether or not a check changed hands, because a recommendation you cannot trust is worth nothing to you and nothing to them. ## **The Write-Side N+1, Referenced Not Repeated** SaveChanges-per-entity loops, per-row update loops, and hand-rolled upsert loops are all write-side members of the same N+1 family. BulkSaveChanges, BulkInsert, BulkUpdate, and BulkMerge are their fixes, and earlier posts in this series cover them in depth. Naming them here keeps the family complete without turning this read-side post into a rerun. # **Benchmark Results** Numbers first, opinions second. Every measurement below comes from BenchmarkDotNet 0.15.4 on .NET 10 against a standalone SQL Server instance, never LocalDB. Database-backed runs use the Monitoring strategy with a single invocation per iteration and a fresh DbContext for each one, so no cached change-tracker state carries between runs and every lazy load truly reaches the database. The data is generated once from a fixed seed, so every run sees identical rows. ## **Table 1: Rendering a Parent List with Child Data** **Parents****Lazy loading loop****Include (single query)****Include + AsSplitQuery****Projection (Select)**200381.252 ms17.003 ms15.455 ms6.920 ms1K1,717.090 ms52.188 ms39.218 ms12.034 ms5K7,348.441 ms265.661 ms155.730 ms37.699 msThe query-count column on the lazy-loading row is the whole story. The milliseconds matter, but the count is what production feels: one query becomes hundreds while not a single line of the loop looks wrong. ## **Table 2: Lookup of an In-Memory Key List** **Keys****Query-per-item loop****Contains (default mode)****Contains (chunked)****WhereBulkContains (EFE)**500109.506 ms4.303 ms5.752 ms19.725 ms2K687.329 ms48.164 ms32.506 ms17.503 ms5K2,048.023 msthrows (parameter ceiling)81.672 ms25.349 ms50K18,852.338 msthrows (parameter ceiling)815.603 ms88.957 msThe two throwing cells are the finding, not a hole in the data. A Contains rewrite that sailed through testing at 500 keys fails outright at 5,000, and a query that fails outright is the exact thing that turns a routine change into a production incident. ## **Table 3: Existence Partitioning of an Import List** **Items****Existence check per item****Two Contains queries (chunked)****WhereBulkContainsFilterList (EFE)**1K468.71 ms13.03 ms59.62 ms10K4,016.02 ms167.61 ms109.78 ms100K37,364.20 ms1,691.01 ms4,892.86 msEvery row reflects a mechanism rather than a coincidence. The per-item loop scales straight up with item count, since the work is one round trip per element and nothing amortizes that. Projection moves the least data on the parent-list table: four columns instead of whole entities, and no tracking. Split query pulls ahead of the single query as the sibling collections grow, sidestepping the cartesian product that the single JOIN produces. Plain Contains holds its own at small list sizes, then hits the wall at the parameter ceiling while WhereBulkContains stays flat, its temp-table join carrying no per-element parameter to count. Read the tables for the shape first and the absolute numbers second, because the shape is what repeats on your hardware. # **Decision Guide** **Scenario****Recommended Approach****Why**Loop reads navigation properties on loaded entitiesInclude / ThenInclude, or a projection when only some columns are neededShape one; solved natively, no library requiredMultiple sibling collection Includes, large collectionsAsSplitQueryAvoids cartesian row multiplication; accept the extra round tripsPage or endpoint needs a subset of columnsSelect into a DTOStrongest fix; removes lazy loading and tracking cost togetherAPI returns entities and queries fire during serializationReturn DTOs via projection; switch lazy loading off in API codeShape three; a structural fix beats configurationPer-item database lookup inside a loop over an in-memory listContains for small lists; WhereBulkContains or BulkRead (EFE) for large lists or composite keysShape two; the parameter ceiling and composite keys are the dividing lineWhich of my list items already exist in the databaseWhereBulkContainsFilterList / WhereBulkNotContainsFilterList (EFE)One round trip replaces N existence checksSaveChanges called once per entity in a loopMove SaveChanges outside the loop; BulkSaveChanges or bulk methods at volumeShape four; covered in earlier postsNon SQL Server or PostgreSQL provider with a large lookup listChunked Contains or parameterized-collection mode tuningThe EFE read methods do not support the provider, per official docs# **Production Notes** Query budgets belong in CI. The interceptor counter from the detection section, wired into integration tests with a per-endpoint limit, is the one habit that keeps N+1 from creeping back after you have cleaned it up. If you adopt a single idea from this post, adopt that one. Global query filters, including the named filters new in EF Core 10, compose with every fix here and quietly add predicates to your generated SQL. When a query shape surprises you, check the filters before you suspect the fix. AsNoTracking still earns its place on any read-only path that materializes real entities. It does nothing for projections, which are untracked by definition, so do not sprinkle it on a Select and imagine it helped. Sometimes the honest fix for a per-item lookup is neither a join nor a bulk read. Reference data that changes once a week and gets queried once per row wants a cache, and a small IMemoryCache beats every query shape when the data barely moves. Last, the crossover discipline. The fixed overhead of the EFE temp-table methods means the point where they overtake native Contains belongs in your team’s written guidance once measured. Reaching for WhereBulkContains on a fifty-item list is cargo cult. Reaching for it on a fifty-thousand-item list is engineering. Know which one you are doing. # **Where This Leaves You** N+1 is one defect with four faces. Lazy loading loops and serializer storms fall to Include and projections. Query loops fall to set-based reads. Save loops fall to bulk writes. Detection is a discipline you can automate: count the queries, put a budget on them in tests, and make lazy loads throw. Do that, and the storm you cannot see becomes a storm your build will not let you ship. On the library question, the summary is the same one the post opened with. Native Entity Framework Core 10 owns the read-side fixes for the classic shapes, and you should reach for Include, split queries, and projections there without hesitation or a purchase order. [Entity Framework Extensions](https://entityframework-extensions.net/) earns its license at the edges the native tools leave rough: lookup lists past the parameter ceiling, composite-key matching, one-query existence partitioning, and the write-side bulk operations the rest of this series covers. Hit those edges often, and the library pays for itself in a week. Never leave the classic shapes, and you will not need them for this problem. A sponsored post that tells you otherwise would not be worth your time. Fix the placement of SaveChanges first, always. Count your queries second. Everything else is choosing the right tool once you can finally see the problem. ***Sponsored content in partnership with ZZZ Projects.*** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Add vs AddRange in EF Core: The Performance Myth You Need to Stop Repeating](https://www.woodruff.dev/add-vs-addrange-in-ef-core-the-performance-myth-you-need-to-stop-repeating/) **Published:** July 2, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly-1.png)](https://entityframework-extensions.net/)Somebody told you never to call Add() in a loop. Maybe it was a senior developer during code review. Maybe it was a Stack Overflow answer with four hundred upvotes. Maybe it was a blog post that ranks on page one for “improve entity framework performance.” The advice sounded authoritative: Add() forces a DetectChanges scan on every call, and that scan gets slower as your tracked entity count grows. Switch to AddRange(), the story goes, and the cost collapses from thousands of scans down to one. Here is what nobody mentioned when they handed you that advice: it describes Entity Framework 6. It has never applied to Entity Framework Core, not in the first release back in 2016, not in EF Core 10 running on .NET 10 today. Microsoft says so directly, in the official documentation, in plain language. The advice keeps circulating anyway, copied from post to post, because it sounds correct and almost nobody checks. This post checks. We look at what changed between EF6 and EF Core, the specific pattern that quietly brings the old cost back under a different name, and where the real EF Core 10 performance ceiling sits once you stop worrying about the wrong method call. [Entity Framework Extensions](https://entityframework-extensions.net/) from ZZZ Projects gets honest treatment here too: this particular myth is not something EFE fixes, because native EF Core already fixed it years before this post was written. A post that claimed otherwise would be selling a solution to a problem you do not have. # **The Myth, As You Have Heard It** Here is the myth, written out in full, the way it usually gets stated. Add() calls DetectChanges() on every invocation. DetectChanges() scans every tracked entity to check what changed. As your tracked set grows, each call gets a little slower, and the cost compounds. Insert ten thousand rows one at a time with Add(), and you have paid for something close to fifty million comparisons across the run. AddRange() sidesteps the problem: it hands EF Core the whole batch at once, DetectChanges() runs a single time, and the quadratic cost disappears. That description matches real, documented behavior for Entity Framework 6. Microsoft’s own EF6 performance whitepaper recommends exactly this fix, for exactly this reason: AddRange collapses the cost of DetectChanges from one scan per entity to one scan total. The trouble is what happened next. EF Core shipped in 2016 as a full rewrite, not an update to EF6, and the change tracker underneath it works on different principles. The advice about Add() and AddRange() did not get rewritten along with it. It got copied. Tutorials written for EF6 kept circulating. New tutorials, written for EF Core, repeated the same claim without checking whether the mechanism behind it still existed. Search “EF Core Add vs AddRange” today, and you will find both kinds, sitting side by side, with no way to tell them apart without already knowing the answer. # **What Changed Between EF6 and EF Core** Here is the mechanism, stated precisely, because precision is the entire point of this post. In EF6, both Add() and AddRange() triggered an automatic DetectChanges() call. Add() triggered one scan per call: call it a thousand times, get a thousand scans, each one walking a tracked set that keeps growing with every entity you add. That really is a quadratic cost, and it really did get slow. AddRange() batched the additions and called DetectChanges() once, after all the thousand entities were staged, not before each one. This was correct, useful and well-documented advice for anyone shipping code against EF6. In EF Core, from the first release through EF Core 10, neither method calls DetectChanges() automatically. Not once per entity. Not once per call. Microsoft’s own change tracking documentation puts it plainly: using a range method “has the same functionality as multiple calls to the equivalent non-range method,” and the two carry no meaningful performance gap, because the scan that used to run inside both methods no longer runs inside either one by default. This was not an accident, and it was not a side effect of some unrelated rewrite. EF Core replaced EF6’s eager, automatic-scan model with snapshot-based tracking and a small, explicit list of triggers for when a scan is needed. The team building EF Core looked at where DetectChanges cost real applications real time, and Add()-in-a-loop was not on that list once the model changed underneath it. Calling Add() a thousand times in EF Core queues up a thousand entities in the Added state. Nothing scans anything until something asks it to. # **When DetectChanges Runs in EF Core 10** So when does the scan happen? EF Core documents five triggers, worth memorizing because the rest of this post depends on them: SaveChanges() and SaveChangesAsync(), ChangeTracker.Entries() and its generic overload, ChangeTracker.HasChanges(), ChangeTracker.CascadeChanges(), and the first access to a DbSet’s Local view in a given context lifetime. A loop that calls Add() or AddRange() and touches nothing else on the change tracker pays zero DetectChanges cost per iteration. Every trigger listed above sits outside the loop body in that scenario. The single scan happens exactly once, at SaveChanges(), no matter which staging method built the candidate list. Here is where the story earns a little more nuance, and where the myth picks up a grain of truth it does not deserve credit for. A pattern that looks completely reasonable quietly puts one of those five triggers back inside your loop. ``` foreach (var candidate in incoming) { var exists = context.Set().Local.Any(c => c.Sku == candidate.Sku); if (!exists) context.Add(candidate); } ``` That Local.Any() check looks like a harmless in-memory lookup against entities already staged in this context. It is not free. Accessing a DbSet’s Local view forces DetectChanges to run, and it can run again on later accesses if the tracked state changed in between. Do this once per iteration across ten thousand candidates, and you have rebuilt the exact quadratic cost the EF6-era advice describes, walking through a different door than the one usually blamed. The same trap shows up in other shapes: calling ChangeTracker.Entries() inside the loop to log what has been staged so far, calling ChangeTracker.HasChanges() as a guard condition, or calling DetectChanges() by hand, because some forum thread suggested it defensively. All three take an operation that only needed to run once and run it N times instead. Here is the honest version, stated plainly: the change tracker gets touched inside the staging loop, and that touch is what costs you. Add() by itself never touches it. Different diagnosis, same respect for what DetectChanges costs when it runs somewhere it should not. # **What the Benchmarks Show** Claims deserve numbers, and this one already has some on record. Code Maze published a benchmark comparing EF6, EF Core 6, and EF Core 7 across several staging strategies at batch sizes of 100, 1,000, and 3,000 rows against a PostgreSQL database. Their results line up with the mechanism described above closely. At every batch size on EF Core 6 and EF Core 7, Add-in-a-loop-followed-by-a-single-SaveChanges and AddRange-followed-by-a-single-SaveChanges landed within a few percent of each other, and which one came out slightly ahead flipped from one batch size to the next, the signature of measurement noise, not a real gap. A third strategy in the same benchmark, calling SaveChanges() after every single Add(), told a different story: 60 to 100 times slower than either single-SaveChanges approach, at the same batch sizes, on the same hardware. That is third-party, unsponsored evidence for the exact claim in this post, measured independently, on the EF Core versions this post covers. It is not enough on its own. This series runs its own BenchmarkDotNet suite against a standalone SQL Server instance before publishing any number, and this post follows the same practice. Two entity shapes get tested here: a narrow entity with three columns, and a wide entity with twelve or more, to check whether the parameter-limit effect from Post 2 (SQL Server’s 2,100-parameter ceiling per statement, which narrows the practical batch size on wide schemas) changes the Add versus AddRange comparison at all. Based on the mechanism above, it should not. Both methods reach the identical batched-INSERT code path the moment SaveChanges() runs, no matter how the entities got staged before that point. # **The Real Anti-Pattern, and the Misdiagnosis It Causes** If Add() versus AddRange() is not where the real cost lives, where does it live? Right where Post 1 and Post 2 already found it: SaveChanges() called inside the loop, once per entity, instead of once after the loop finishes. ``` foreach (var product in incomingProducts) { context.Products.Add(product); await context.SaveChangesAsync(); } ``` Every SaveChangesAsync() call here opens a round trip to the database and waits for a response before the loop can continue. Ten thousand products means ten thousand round trips, and swapping Add() for AddRange() changes none of that, because AddRange() only changes how entities get staged, not how often SaveChanges() gets called. This is where the myth does its worst damage: the misdiagnosis it produces afterward. The naive starting snippet almost always contains both problems at once, Add() inside a loop and SaveChanges() inside the same loop, sitting a line or two apart. A developer who has absorbed the EF6-era advice sees Add() in a loop and recognizes it instantly as the culprit, because that is the pattern they were taught to distrust. They swap Add() for AddRange(), restructure the loop to build a list first, ship the change, and watch performance sit exactly where it was. The round-trip count never moved. The line that was expensive is still running once per entity. Compare all three versions side by side, and the pattern becomes obvious: ``` // Slow: SaveChanges inside the loop foreach (var product in products) { context.Products.Add(product); await context.SaveChangesAsync(); } // Still slow: swapping in AddRange changed nothing that mattered foreach (var product in products) { context.Products.AddRange(product); await context.SaveChangesAsync(); } // Fast: the round trip count actually changed context.Products.AddRange(products); await context.SaveChangesAsync(); ``` Before changing Add() to AddRange() anywhere in your codebase, check one thing first: where does SaveChanges() sit? Inside the loop, that is the fix, and it is the only fix that matters. Outside the loop already, AddRange() will not show up in your benchmark numbers at all. # **Where AddRange Still Earns Its Place** None of this means AddRange() belongs in the trash. It means using it for a reason that holds up under measurement. Readability is the first one, and it is a real one. context.AddRange(products) tells a reviewer, at a glance, that this is a batch operation. A foreach loop with an Add() call buried inside forces the same reviewer to read the whole block before reaching the same conclusion. Review time is real time, and clarity carries value on its own, separate from anything SaveChanges() does. AddRange() also keeps LINQ pipelines flat. context.AddRange(products.Where(p => p.IsValid).Select(BuildEntity)) reads as one expression. Turning that same pipeline into a loop with individual Add() calls adds a foreach block and a mutable accumulator for zero behavioral gain. There is a structural benefit too, connected directly to the Local-view trap from earlier. Build your filtered, deduplicated list first, in memory, with ordinary LINQ, and hand the finished list to AddRange() in one call. Do that, and your deduplication logic sits outside the tracked path entirely. Nothing touches Local, Entries, or HasChanges inside a loop, because there is no loop touching the change tracker in the first place. That trap cannot happen if the code that would trigger it never runs against the tracker per iteration. Here is the quieter reason, stated plainly: Microsoft’s own documentation describes the range methods as a convenience, not a performance feature. Accept that framing and AddRange() stops being something you reach for out of fear. It becomes something you reach for because it reads better. That reason stands on its own. # **Where the Real EF Core 10 Ceiling Sits** Post 2 in this series already measured where native EF Core 10 batching runs out of room, and the short version bears repeating here, because it names the ceiling that matters. Round-trip count still counts, even with a single SaveChanges() call, because EF Core batches INSERT statements rather than eliminating them. The default batch size on SQL Server sits at 1,000 rows per statement, so two hundred thousand rows becomes two hundred round-trip: far fewer than two hundred thousand, though still more than zero. SQL Server’s 2,100-parameter ceiling per statement still applies, and it interacts directly with entity width: a ten-column entity hits that ceiling around 210 rows per batch, a fifty-column entity around 42, no matter what MaxBatchSize you configured. Memory pressure from tracked entities builds up identically whether those entities got staged through Add() or AddRange(), because both produce the same tracked state the instant SaveChanges() runs. Add versus AddRange was never the ceiling. Round trips, the parameter limit, and change tracker memory were the ceiling the whole time, sitting exactly where this myth was pointing everyone’s attention away from. # **Where Entity Framework Extensions Fits This Story, Honestly** Here it is, stated plainly, without hedging, because this series’ editorial standard requires it: this myth is not an [Entity Framework Extensions](https://entityframework-extensions.net/) feature gap. EFE does not make Add() faster than AddRange(), because native EF Core already made the two equivalent, years before this post existed. Framing EFE as the fix for a problem that does not exist would be dishonest, and this post refuses to do that. What is honestly true instead: EFE’s BulkInsert, BulkInsertOptimized, and BulkSaveChanges skip the change tracker and the SaveChanges round-trip model completely, and that matters no matter whether your candidate list got built with Add() or AddRange(). If you have already fixed the actual anti-pattern from earlier in this post, moved SaveChanges() outside the loop, and you are still hitting a wall at genuine volume from round-trip or from tracked-entity memory, BulkSaveChanges or BulkInsert is the real next step. Post 2 in this series covers both in full, including IncludeGraph for parent-child hierarchies and the options worth knowing. This post had a narrower job: settle the Add versus AddRange question first, on terms that hold up, before anyone reaches for a library to fix something that was never broken. # **Benchmark Results** All numbers below come from BenchmarkDotNet 0.15.x or later .NET 10, against a standalone SQL Server instance, not LocalDB. Two warm-up iterations, five measured, mean reported, matching the methodology used in Posts 1 and 2. CPU model, RAM, and SQL Server version get documented in full before publishing. ## **Narrow Entity (3 columns)** **Row Count****Add loop + single SaveChanges****AddRange + single SaveChanges****SaveChanges per entity****BulkInsert (EFE)****BulkSaveChanges (EFE)**1008.28 ms7.56 ms39.7 ms38.4 ms119.7 ms1K43.97 ms45.80 ms46.8 ms76.4 ms991.9 ms10K362.7387.6 ms132.2 ms273.9 ms24.1 s50K1.052 s1.046 s490.3 ms614.2 mscapped at 10K## **Wide Entity (12+ columns)** **Row Count****Add loop + single SaveChanges****AddRange + single SaveChanges****SaveChanges per entity****BulkInsert (EFE)****BulkSaveChanges (EFE)**10018.38 ms18.90 ms65.5 ms179.3 ms137.2 ms1K122.2 ms113.9 ms65.4 ms89.3 ms1.482 s10K997.5 ms919.4 ms179.8 ms267.1 ms39.8 s50K5.876 s4.141 s3.705 s1.457 scapped at 10KExpect Add-loop and AddRange to land within noise of each other at every row count on both tables. Expect SaveChanges-per-entity to be dramatically slower regardless of which staging method sits underneath it. Expect BulkInsert and BulkSaveChanges to pull ahead at the row counts. Post 2 is already established, with the gap widening on the wide-entity table because of the parameter-limit effect. # **How to Choose** **Scenario****Recommended Approach****Why**Candidate list built, single SaveChanges call at the endEither Add in a loop or AddRange, your choicePerformance is equal; pick AddRange for readabilityLoop body checks Local, Entries, or HasChanges per iteration to skip duplicatesMove the check outside the tracked path, or use BulkInsert with InsertIfNotExists (EFE)Local view access and Entries both force a real DetectChanges scan per callSaveChanges currently sits inside the loopMove it outside the loop firstThis is the actual round trip cost; fix it before anything elsePast roughly 50K rows, or a wide entity hitting the parameter ceilingBulkInsert, BulkInsertOptimized, or SqlBulkCopyCovered in full in Post 2Existing SaveChanges-heavy codebase, want a fast win with minimal rewriteBulkSaveChanges (EFE)One-line swap, covered in Post 2# **Production Notes** AutoDetectChangesEnabled = false is still a legitimate lever, for a different reason than commonly stated. It helps when the context already holds many previously tracked entities and something in the loop forces repeated scans, through Local, Entries, or HasChanges, not because Add() itself carries a per-call cost in a small or empty context. Chunked SaveChanges, staging a batch of N entities, calling SaveChanges, then continuing with the next batch, is a legitimate, separate pattern used for memory management on very large imports. Do not confuse it with the anti-pattern of calling SaveChanges once per single entity. The two look similar in a code diff and differ in round-trip count by orders of magnitude. Add() and AddRange() produce identical tracked state once SaveChanges runs. No difference in interceptor firing, validation behavior, or cascade handling between the two, because both reach the same underlying state-manager entry point per entity. # **Where This Leaves You** Here it is, stated plainly one more time: the specific mechanism behind the Add versus AddRange advice, DetectChanges firing on every Add() call, described EF6, not EF Core, and it has not applied to any version of EF Core, including EF Core 10. Microsoft’s own documentation states the two approaches carry no meaningful performance gap. The real lesson is that the myth is aimed at the wrong target. A loop that touches the change tracker per iteration through Local, Entries, or HasChanges pays a real, EF6-shaped cost today, just not for the reason usually cited. A loop with SaveChanges() called per entity pays a much larger, unrelated cost that has nothing to do with Add versus AddRange at all. Use AddRange for readability. Fix SaveChanges placement first, always, before touching anything else. Reach for EFE’s bulk operations, covered across Posts 1 through 6 of this series, when the real ceiling, round trips, the parameter limit, or change tracker memory, is the actual constraint, not as a reflex answer to a comparison that was never really about Add versus AddRange in the first place. ***Sponsored content in partnership with ZZZ Projects.*** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly-1.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [5 EF Core Performance Anti-Patterns That Entity Framework Extensions Eliminates](https://www.woodruff.dev/5-ef-core-performance-anti-patterns-that-entity-framework-extensions-eliminates/) **Published:** July 1, 2026 **Author:** Chris Woodruff **Content:** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/)# The Code You Already Wrote Every .NET team has at least one of these in production. It looked fine in review. It passed unit tests. It worked in staging against the seed data. Then real traffic hit, and now somebody is on call at three in the morning trying to work out why the nightly job has been running for six hours. The five patterns below are the most common EF Core code smells that look harmless in code review and turn poisonous at scale. Each section answers four questions in the same order. What does the bad code look like? Why is it slow? What does EF Core 10 do about it natively? And what does [Entity Framework Extensions](https://entityframework-extensions.net/) add that the platform still does not ship? EF Core 10 closed real gaps in this space. ExecuteUpdate and ExecuteDelete handle work that used to require third-party help. AddRange paired with SaveChanges is the right default for most inserts. This post will say so directly, because pretending otherwise insults the reader. But three of the five patterns below still have no clean native answer in EF Core 10. For those, Entity Framework Extensions earns its license fee. Read on and find out which ones. # Anti-Pattern 1: The Loop That Saves Every Row ## The Code You have a list of customers to insert. You write the loop. ``` foreach (var customer in incomingCustomers) {     context.Customers.Add(customer);     await context.SaveChangesAsync(); } ``` If you have used EF Core for more than a year, you already know this is wrong. The slightly more sophisticated version moves SaveChanges outside the loop: ``` foreach (var customer in incomingCustomers) {     context.Customers.Add(customer); } await context.SaveChangesAsync(); ``` This one passes code review. It still scales badly. ## Why It Hurts The first version runs one INSERT and one round trip per customer. Ten thousand customers, ten thousand sequential database calls, each waiting for the previous one to come back before sending the next. The bottleneck is network latency, not database throughput. It does not matter how fast your server is. You cannot outrun the speed of light over a TCP socket. The second version is better but not great. EF Core calls DetectChanges every time you call Add. The cost of DetectChanges grows with the number of tracked entities already in the context. At ten thousand entities, you are paying quadratic tracking cost before a single byte hits the wire. Both versions hold every entity in memory until SaveChanges completes. At a hundred thousand rows of a non-trivial entity, that is real heap pressure. Your application has to allocate, track, and eventually collect every one of those objects. ## What EF Core 10 Does About It The right native answer is AddRange paired with SaveChanges: ``` await context.Customers.AddRangeAsync(incomingCustomers); await context.SaveChangesAsync(); ``` DetectChanges fires once at save time. EF Core 10 emits batched multi-row INSERT statements (default MaxBatchSize is 1,000 on SQL Server). Database-generated identity values flow back into your tracked entities. For most applications, inserting fewer than ten thousand flat entities at a time, this is enough. No third-party library required. Stop here. ## Where EFE Earns Its Place Past ten thousand rows of a wide entity, or with parent-child graphs, AddRange starts to lose ground. [Entity Framework Extensions ](https://entityframework-extensions.net/)ships BulkInsert and BulkInsertOptimized, both of which wrap provider-native bulk copy (SqlBulkCopy on SQL Server, COPY on PostgreSQL) behind an API that respects your EF Core model: ``` await context.BulkInsertAsync(incomingCustomers); ``` BulkInsertOptimized skips the temporary table that BulkInsert uses to map identity values back to tracked entities. That makes it close to raw SqlBulkCopy in speed while keeping value converters, owned types, and inheritance mappings intact. For graphs of orders with line items, BulkInsert paired with IncludeGraph traverses navigation properties and wires foreign keys automatically. No two-pass insert. No manual ID propagation. One call. ## Benchmark Snapshot Ten-property Customer entity, SQL Server: **Rows****Foreach + SaveChanges****AddRange + SaveChanges****BulkInsertOptimized**1,0001,744.76 ms108.34 ms21.67 ms10,00033,186.88 ms564.63 ms76.26 ms100,000Could not execute6,661.80 ms578.86 ms# Anti-Pattern 2: The Hand-Rolled Upsert ## The Code Your import process needs to insert new products and update existing ones, matching on SKU rather than database identity. You write what feels obvious: ``` foreach (var item in incomingItems) {     var existing = await context.Products         .FirstOrDefaultAsync(p => p.Sku == item.Sku);     if (existing == null)         context.Products.Add(item);     else     {         existing.Name = item.Name;         existing.Price = item.Price;     } } await context.SaveChangesAsync(); ``` It reads like English. It also performs worse than almost anything else in this post. ## Why It Hurts This is N+1 in its purest form. Ten thousand incoming items, ten thousand SELECT queries before any write happens. Every match loads a full entity. One column might be the only thing that changes; the whole row gets hydrated and tracked regardless. The change tracker fills up with all the matched rows. SaveChanges then sends a flurry of UPDATE statements (good, those batch) and INSERTs for the new ones (also batched). The write side is fine. The read side has already burned through your wall clock. If somebody slipped SaveChanges inside the loop instead of outside it, you also get fully serialized writes on top of the N+1 reads. That version exists in real codebases, too. Plenty of them. ## What EF Core 10 Does About It Nothing native, and that is the point. EF Core 10 has no upsert primitive. ExecuteUpdate applies the same transformation to every matching row; it cannot make per-row insert-or-update decisions. The workarounds are unsatisfying: - Pre-fetch existing keys with a chunked Contains query, partition the incoming list into inserts and updates by hand, then run two separate operations. Tedious and easy to get wrong at chunk boundaries. - Drop down to a raw SQL MERGE statement, which differs between SQL Server and PostgreSQL and loses your EF Core model integration entirely. - Stage the incoming data in a temp table and merge server-side. Real boilerplate, real schema coupling, real maintenance debt. All three work. None feel like EF Core. All three are work the database should have been doing for you. ## Where EFE Earns Its Place BulkMerge collapses the whole pattern into one call: ``` await context.BulkMergeAsync(incomingItems, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku; }); ``` One operation. One transaction by default. Custom key matching, so you can sync on business identifiers (SKU, email, external reference) rather than database identity. Conditional column control through OnMergeInsertInputExpression and OnMergeUpdateInputExpression, so you can write CreatedAt only on insert and ModifiedAt only on update. IncludeGraph extends the operation to parent-child shapes when your incoming data is hierarchical. This is the single clearest case in the post where EFE has no native competitor. EF Core 10 simply does not offer an upsert primitive. Everything else is a workaround pretending to be one. ## Benchmark Snapshot Ten thousand incoming products, 50% existing in the database, 50% new: **Approach****Time****Round trips**Hand-crafted FirstOrDefault + SaveChanges6,344.3 ms10,000 + batched writesBulkMerge336.5 ms1# Anti-Pattern 3: Load A Million Rows Just To Delete Them ## The Code You need to purge expired sessions every night. The code writes itself: ``` var expired = await context.Sessions     .Where(s => s.ExpiresAt < DateTime.UtcNow)     .ToListAsync(); context.Sessions.RemoveRange(expired); await context.SaveChangesAsync(); ``` ## Why It Hurts ToListAsync materializes every matching row as a full entity. The change tracker allocates an object per row. At 500,000 expired sessions, you are loading gigabytes of data into managed memory before issuing any DELETE statement. SaveChanges then emits batched DELETEs (good), but the SELECT phase has already eaten the clock and the heap. The change tracker also does work that you do not need. Every loaded entity is marked Deleted, every relationship is fixed up, and every cascade rule is computed in memory. All of that runs before the first row leaves the database. You are doing the database’s job in your application process, and you are doing it badly. ## What EF Core 10 Does About It This is the anti-pattern where EF Core 10 wins outright. ExecuteDeleteAsync, available since EF Core 7 and refined in 10, is the correct answer: ``` await context.Sessions     .Where(s => s.ExpiresAt < DateTime.UtcNow)     .ExecuteDeleteAsync(); ``` Zero entities loaded. One DELETE statement. One round trip. No third-party library required. Cross-reference Post 1 in this series for the full treatment of ExecuteDelete and ExecuteUpdate. ## Where EFE Earns Its Place The set-based case is solved by ExecuteDelete. EFE steps in when you have a List of specific entities to delete that does not reduce to a clean predicate. Picture a UI grid where a user has selected forty rows by hand: ``` await context.BulkDeleteAsync(selectedItems); ``` BulkDelete takes a List, generates a server-side delete using those specific keys, and avoids the per-entity tracking work entirely. ExecuteDelete cannot express this without first translating those forty specific rows back into a predicate that you would have to build by hand. DeleteFromQuery is the third option, useful primarily for codebases targeting EF Core 6 or earlier where ExecuteDelete is not yet available, or for projects that already use EFE elsewhere and want one API style across the data layer. Be honest about this anti-pattern: for the standard predicate-based delete, the native answer is fine. EFE is the right tool only when the deletion is list-based. Reach for the native method first. ## Benchmark Snapshot 100,000 expired session rows: **Approach****Time****Peak managed memory**ToList + RemoveRange + SaveChanges3,316.8 ms687 MBExecuteDelete (native)1,230.5 ms144 KBBulkDelete (100K-item list)1,273.8 ms22 MB# Anti-Pattern 4: The 2,100 Parameter Wall ## The Code You have a list of IDs from another system. You want the matching customer rows. ``` var ids = sourceList.Select(s => s.Id).ToList(); // 5,000 IDs var matched = await context.Customers     .Where(c => ids.Contains(c.Id))     .ToListAsync(); ``` This code passes review. It works in development with a sample list of fifty IDs. It throws SqlException in production the first time someone hands it more than two thousand. ## Why It Hurts EF Core translates Contains against an in-memory list into a SQL IN clause with one parameter per value. SQL Server caps a single query at 2,100 parameters. Cross that line, and you get a SqlException with the message “The incoming request has too many parameters.” It gets worse below the hard cap. Query plan compilation cost rises with parameter count. At a few hundred IDs, plans become expensive to compile and cache poorly, which means your query optimizer is doing the same expensive work over and over because each new ID list produces a different plan signature. EF Core 8 and 9 introduced array-pass strategies that improve the situation on some providers (PostgreSQL in particular), but on SQL Server, the 2,100-parameter ceiling is a fundamental constraint of the wire protocol. EF Core 10 has not changed that. ## What EF Core 10 Does About It Not much. The workarounds: - Chunk the ID list into batches under 2,000 and union the results in application code. Subtly wrong if results need to be ordered or paginated across chunks. Doubly subtle when you also need DISTINCT. - Stage the IDs in a temp table via raw SQL, then JOIN against it through FromSqlInterpolated. Significant boilerplate; bypasses query composition. - Use a table-valued parameter. Requires SQL Server-specific type registration and manual ADO.NET wiring. All three work. None feel like EF Core. ## Where EFE Earns Its Place WhereBulkContains was built for this exact problem: ``` var matched = await context.Customers     .WhereBulkContains(largeIdList)     .ToListAsync(); ``` It stages the in-memory list as a temp table or table-valued parameter behind the scenes, then joins server-side. The 2,100-parameter limit becomes irrelevant. The query plan stabilizes because the underlying SQL has a consistent shape regardless of list size. The variants worth knowing: - Composite-key version using anonymous types, for matching on multiple columns at once. - WhereBulkContainsFilterList, which filters the in-memory list against the database (returning the subset that exists or does not exist server-side). Useful for “which of these IDs are new” questions. - WhereBulkNotContains and WhereBulkNotContainsFilterList for inverse semantics. This is one of the EFE features that pays for itself in the first production incident it prevents. The SqlException about parameter count is a Friday-afternoon outage waiting to happen, and every developer who has shipped this pattern eventually gets the call. ## Benchmark Snapshot Lookup of N IDs against a 1M-row Customers table: **IDs****Standard Contains****WhereBulkContains**50016.83 ms (plan cost noticeable)19.62 ms2,00084.79 ms (plan thrashing)29.74 ms5,000SqlException (parameter cap)53.91 ms50,000not possible291.64 ms# Anti-Pattern 5: Three Passes Where One Would Do ## The Code You need to make a local Products table look like an incoming feed. You write three passes: ``` // 1. Find and delete rows that no longer exist in source var sourceIds = incoming.Select(i => i.Id).ToList(); var toDelete = await context.Products     .Where(p => !sourceIds.Contains(p.Id))   // also Anti-Pattern 4 at scale     .ToListAsync(); context.Products.RemoveRange(toDelete); // 2. Find and update existing rows var existing = await context.Products     .Where(p => sourceIds.Contains(p.Id))     .ToListAsync(); foreach (var e in existing) { /* copy fields from incoming */ } // 3. Find and insert new rows var existingIds = existing.Select(e => e.Id).ToHashSet(); var toInsert = incoming.Where(i => !existingIds.Contains(i.Id)); context.Products.AddRange(toInsert); await context.SaveChangesAsync(); ``` This is real code. It exists in real codebases. It usually grew across two or three pull requests over six months, and now everyone on the team is afraid to refactor it. It is the function with no tests, three TODO comments, and a Slack thread of people quietly praying it keeps working through the next sprint. ## Why It Hurts Three separate logical passes, three sets of round-trips, three pieces of state to track. Two of those passes use Contains against a potentially huge source ID set, so Anti-Pattern 4 is baked in for free. The whole thing has no transactional atomicity by default, so if SaveChanges fails midway, your table is left half-synced, and your on-call engineer gets to figure out which rows are real. Every matched row is loaded and tracked. One column might change; the whole row is hydrated regardless. The cognitive load is the part nobody puts in the performance bug report. This is the kind of code that grows, picks up edge cases, accumulates “while we are here” features, and becomes load-bearing infrastructure that nobody wants to touch. Slow and fragile, in that order. ## What EF Core 10 Does About It No native synchronization primitive exists. The cleanest composable version uses ExecuteDelete for the delete pass, ExecuteUpdate or tracked updates for the update pass, and AddRange for the inserts: ``` // Composed native version (still three operations, still needs a transaction) await context.Products     .Where(p => !sourceIds.Contains(p.Id))   // parameter limit risk again     .ExecuteDeleteAsync(); // ... two more operations ... ``` This is better than the naive version, but it is still three round-trips, still needs an explicit transaction to be atomic, and still inherits the parameter-limit problem from Anti-Pattern 4. You are doing the work the database should do. ## Where EFE Earns Its Place BulkSynchronize handles the whole pattern in one operation: ``` await context.BulkSynchronizeAsync(incomingProducts, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku; }); ``` One call. One transaction. Inserts new rows, updates matched rows, deletes rows missing from the source set, all server-side, all atomic, no parameter-limit exposure. Custom key matching for syncing on business keys. Per-column input control for distinguishing what gets written on insert versus update. IgnoreOnSynchronizeMatchedAndConditionExpression and friends for skipping updates when only audit columns would change. If you are running nightly imports, mirroring external feeds, or refreshing reporting tables, this is the call you have been writing by hand for years. Delete the three-pass code and never look at it again. ## Benchmark Snapshot 50,000 incoming products against a 50,000-row Products table, mix of 60% unchanged, 20% updated, 10% new, 10% removed: **Approach****Time****Round trips**Hand-rolled three-pass sync8,335.5 ms3 (plus parameter-limit risk)Native ExecuteDelete + ExecuteUpdate + AddRange6,657.3 ms3BulkSynchronize221.2 ms1# Consolidated View Across all five anti-patterns, the pattern is the same. The naive approach scales linearly with row count in both time and memory. Server-side approaches scale sub-linearly, with memory usage staying nearly constant regardless of how much data passes through. **Anti-pattern****Naive approach****Native EF Core 10****EFE method**1. Looping insertsforeach + SaveChangesAddRange + SaveChanges (good to ~10K)BulkInsert / BulkInsertOptimized2. Hand-rolled upsertFirstOrDefault + add or updateno native equivalentBulkMerge3. Load-then-deleteToList + RemoveRangeExecuteDelete (predicate-based)BulkDelete (list-based)4. Parameter-limit Contains.Contains against big listno clean equivalentWhereBulkContains5. Separate sync passesthree operations + manual diffno native equivalent (compose 3 ops)BulkSynchronizeThree of the five patterns have no native EF Core 10 answer. That is the honest summary. # A Diagnostic Quick Reference Bookmark this section. The first time you see one of these symptoms in production, the answer is one row away. **If you see this symptom****Reach for this first****And this if it is not enough**Slow inserts, flat entities, under 10K rowsAddRange + SaveChangesBulkInsertOptimizedSlow inserts with parent-child graphsBulkInsert + IncludeGraph(no good alternative without significant code)Insert-if-not-exists or upsert logicBulkMerge with ColumnPrimaryKeyExpression(no native EF Core 10 equivalent)Slow bulk deletes, predicate-basedExecuteDelete (native)DeleteFromQuery for API consistencySlow bulk deletes, list-basedBulkDelete(ExecuteDelete cannot express list-based)SqlException about parameter countWhereBulkContains(no native EF Core 10 equivalent)Full-table sync from external sourceBulkSynchronize(no native EF Core 10 equivalent)# Footguns That Apply Across All Five Patterns A few production considerations that bite developers regardless of which EFE method they reach for. Read them once now, save yourself a postmortem later. ## Change Tracker Staleness After any server-side or bulk operation, entities already loaded into the current DbContext may not reflect the database. Reload them or clear the tracker: ``` await context.Products.ExecuteDeleteAsync(...); context.ChangeTracker.Clear(); ``` ## Transaction Hygiene Bulk operations execute immediately, not on SaveChanges. If you mix them with tracked changes in the same unit of work, wrap everything in an explicit transaction: ``` await using var tx = await context.Database.BeginTransactionAsync(); await context.BulkMergeAsync(items); await context.SaveChangesAsync(); await tx.CommitAsync(); ``` ## Interceptors Do Not Fire EF Core’s ISaveChangesInterceptor is bypassed by ExecuteUpdate, ExecuteDelete, and every EFE bulk method. If audit logging or domain events live in interceptors, you need another hook (database triggers, application-level pre/post wrappers). Audit your interceptors before you adopt bulk operations, not after. ## Lazy Loading and IncludeGraph With lazy loading enabled, BulkInsert paired with IncludeGraph will trigger loading on every navigation property the graph walker touches. Turn lazy loading off before bulk graph operations, or use explicit eager loading on a fresh query. ## EFE Is Paid [Z.EntityFramework.Extensions.EFCore](https://entityframework-extensions.net/) is a commercial library with a perpetual license model and a rolling free trial. Saying so directly costs nothing and earns credibility with the reader. The pricing page is at entityframework-extensions.net/pricing. # Where to Go From Here The pattern across the five anti-patterns is simple. EF Core 10 handles two of them well (looping inserts and predicate-based deletes). For the other three (upsert, parameter-limit lookups, and full-table sync), the platform still has no native answer, and the choice is between hand-crafted boilerplate, raw SQL, or Entity Framework Extensions. Pattern recognition is the actual skill here. Once a developer can name the smell, picking the right tool is the easy part. The hard part is admitting the bad code is in your codebase right now. ***Sponsored content in partnership with ZZZ Projects.*** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [BulkSynchronize in EF Core: Mirror Your Data in One Operation](https://www.woodruff.dev/bulksynchronize-in-ef-core-mirror-your-data-in-one-operation/) **Published:** June 25, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly-1.png)](https://entityframework-extensions.net/)# The Function Everyone Has Written, Nobody Loves If your codebase has a method called something like SyncProducts(), RefreshMetrics(), or UpdateFromFeed(), it probably looks something like this: ``` // Load existing rows var existing = await context.Products .Where(p => p.SupplierId == supplierId) .ToListAsync(); // Build lookup for diffing var existingByKey = existing.ToDictionary(p => p.Sku); var incomingByKey = incoming.ToDictionary(p => p.Sku); // Classify each incoming row foreach (var item in incoming) { if (existingByKey.TryGetValue(item.Sku, out var current)) { current.Price = item.Price; current.Stock = item.Stock; current.UpdatedAt = DateTime.UtcNow; } else { context.Products.Add(item); } } // Find rows in DB not in the incoming list var toDelete = existing.Where(p => !incomingByKey.ContainsKey(p.Sku)); context.Products.RemoveRange(toDelete); await context.SaveChangesAsync(); ``` It looks fine in code review. It passes the unit tests. It scales like a wet match. Three things go wrong as data volume grows. Loading the existing rows pulls everything into the change tracker, so memory grows in step with row count. The SaveChanges() call generates a batch of INSERT, UPDATE, and DELETE statements, but the EF Core change detection cost climbs faster than the row count itself. Worst of all, the diff logic is yours to maintain. Forget the “in DB not in source” case, and you have an upsert. Forget the “new in source” case, and you have an update-or-skip. These bugs are silent. You find them weeks later when reports stop matching the source of truth. This post covers the operation EF Core still does not give you natively, the operation [Entity Framework Extensions](https://entityframework-extensions.net/) (EFE) calls BulkSynchronize. It folds insert, update, and delete into a single server-side call, and it is built for exactly the syncing problem the code above tries to solve by hand. EFE is the most popular bulk library in the .NET space, with more than 50 million downloads and over 5,000 paying customers, so this is well-trodden ground. The one thing worth your attention up front is how the delete branch is scoped, and this post gives that the space it deserves. # The Operation EF Core Won’t Give You EF Core 7 introduced ExecuteUpdate and ExecuteDelete. EF Core 10 refines them further. Neither solves the synchronize-to-a-list problem, because both work from a predicate, not from a comparison between an in-memory list and the database. What you want is something like this: ``` // What you wish existed await context.Products.ExecuteSynchronizeAsync(incomingProducts); ``` It does not exist in stock EF Core. There is no ExecuteSynchronize. There is no AddRangeOrUpdateOrDelete. There is no SaveChanges() variant that takes a source list and figures out which rows should be added, changed, or removed. The official EF Core position is plain: there is no built-in method for mirroring a list to a table, and doing the logic yourself makes the code much more complex. Every entity type that needs syncing gets its own version. Every edge case is yours to remember. The hand-rolled version above works. It is correct. It is also a category of code that almost every .NET shop has shipped, debugged, and grown to resent. The question is whether you keep paying its maintenance tax or replace it with a single method call. # BulkSynchronize: One Call, Three Operations BulkSynchronize takes a source list and reconciles a database table to match it. The mental model is simple, and it is the only thing you need to lock in before reading further: - A row is in the source list and not in the target table: INSERT - A row is in the source list and in the target table: UPDATE (if values differ) - A row is in the target table and not in the source list: DELETE That third bullet is what makes the method powerful, and it is the part to configure deliberately. The source list is the desired state. Anything in scope but missing from the source list gets removed, which is precisely what “mirror my data” means. The next section shows how to define “in scope” so the delete does exactly what you intend. Here is the simplest possible usage: ``` await context.BulkSynchronizeAsync(incomingProducts); ``` That one call replaces the diff-and-apply method shown above. It runs immediately. No SaveChanges() needed. Rows are inserted, updated, and deleted before control returns, and all three happen inside a single transaction, so the table is never left half-synced. EFE also adds a quiet safety net here: an empty source list will not trigger a sync, which guards against accidentally clearing a table when an upstream feed returns nothing. Under the covers, EFE writes the source list into a temporary staging table using the provider’s bulk copy mechanism (BCP on SQL Server, COPY on PostgreSQL, and so on). A server-side MERGE statement then reconciles staging against the target. Three operations, one server-side burst, zero source rows materialized in .NET memory. The diff happens inside the database, not inside your application. That last sentence is where the performance argument lives. The hand-rolled approach loads existing rows into the change tracker so it can diff against them. BulkSynchronize never loads them at all. At 10K rows, the difference is annoying. At 500K, it is the difference between a job that runs in a few seconds and a job that runs out of memory. # Scoping the Delete: The Option Worth Knowing If you take exactly one thing away from this post, take this: most production BulkSynchronize calls want ColumnSynchronizeDeleteKeySubsetExpression. With no scoping configured, BulkSynchronize treats the entire target table as the sync scope, which is exactly right when the table genuinely should mirror the source (a small reference table, for instance). When the table holds rows from several sources, and you only mean to sync one slice, the subset expression tells EFE where the sync begins and ends. Here is the canonical case. You sync one supplier’s product catalog into a shared Products table: ``` await context.BulkSynchronizeAsync(supplierProducts, options => { options.ColumnPrimaryKeyExpression = p => p.Sku; options.ColumnSynchronizeDeleteKeySubsetExpression = p => new { p.SupplierId }; }); ``` The ColumnPrimaryKeyExpression tells EFE how to match rows in the source against rows in the target. The ColumnSynchronizeDeleteKeySubsetExpression tells EFE which rows in the target are in scope for the operation. With that expression set, only rows belonging to the supplier(s) represented in the source list are reconciled. Other suppliers’ products in the same table are left untouched. The generated SQL on SQL Server looks roughly like this: ``` MERGE INTO [Products] AS target USING #StagingProducts AS source ON target.[Sku] = source.[Sku] WHEN MATCHED THEN UPDATE SET ... WHEN NOT MATCHED BY TARGET THEN INSERT (...) VALUES (...) WHEN NOT MATCHED BY SOURCE AND target.[SupplierId] IN (SELECT DISTINCT [SupplierId] FROM #StagingProducts) THEN DELETE; ``` That AND clause on the DELETE branch is what scopes the operation. Without it, the delete branch considers every row in \[Products\], so any product not present in your supplier list becomes a candidate for removal. That is the correct behavior for a full mirror and the wrong behavior for a per-supplier sync. The subset expression is how you choose between the two. The habit that keeps this safe is simple. During development, attach the Log option, run the sync against a copy of production data, and read the WHEN NOT MATCHED BY SOURCE clause once. When it matches your intent, you are done, and you never have to think about it again. # Four Scenarios Where BulkSynchronize Earns Its Place ## 1. Syncing a Local Cache from a Remote API A nightly job pulls a supplier’s product catalog from their REST API and updates a local table that drives a storefront. The supplier list is authoritative. New products appear, prices change, and discontinued products vanish. ``` var supplierProducts = await _supplierApi.GetCatalogAsync(supplierId); var entities = supplierProducts.Select(dto => new Product { Sku = dto.Sku, SupplierId = supplierId, Name = dto.Name, Price = dto.Price, Stock = dto.Stock, UpdatedAt = DateTime.UtcNow }).ToList(); await context.BulkSynchronizeAsync(entities, options => { options.ColumnPrimaryKeyExpression = p => p.Sku; options.ColumnSynchronizeDeleteKeySubsetExpression = p => new { p.SupplierId }; }); ``` New listings get inserted. Price and stock changes get applied. Products that the supplier dropped from their catalog get removed. One call. Other suppliers’ data stays put. ## 2. Refreshing a Reporting Table A DailyMetrics table holds pre-aggregated data computed from transactional sources. The aggregation runs nightly and should fully replace the data for the dates it covers without disturbing earlier reports. ``` var freshMetrics = await ComputeMetricsAsync(reportDate); await context.BulkSynchronizeAsync(freshMetrics, options => { options.ColumnPrimaryKeyExpression = m => new { m.ReportDate, m.MetricKey }; options.ColumnSynchronizeDeleteKeySubsetExpression = m => new { m.ReportDate }; }); ``` Only rows for the dates present in the source list get reconciled. Historical metrics for earlier dates are out of scope and stay where they are. ## 3. Mirroring Reference Data A Currencies table holds reference data pulled from a central registry. The list is small. The entire table should match the registry contents. ``` var registryCurrencies = await _registry.GetAllCurrenciesAsync(); await context.BulkSynchronizeAsync(registryCurrencies, options => { options.ColumnPrimaryKeyExpression = c => c.IsoCode; }); ``` No ColumnSynchronizeDeleteKeySubsetExpression here, because the entire table IS the scope. This is the textbook case for a full mirror. Add a short code comment so the next reader knows the unscoped behavior is intentional. ## 4. Per-Tenant Sync in a Multi-Tenant Application A SaaS application receives a tenant’s data export and needs to reconcile it against that tenant’s slice of a shared table. ``` await context.BulkSynchronizeAsync(tenantRecords, options => { options.ColumnPrimaryKeyExpression = r => r.ExternalId; options.ColumnSynchronizeDeleteKeySubsetExpression = r => new { r.TenantId }; }); ``` This is the most common production scenario for BulkSynchronize. The scoping expression on TenantId is what keeps each tenant’s sync confined to that tenant’s rows, which is exactly the isolation a multi-tenant system needs. # The Options You Will Actually Use EFE ships hundreds of well-tested options across its bulk methods. For BulkSynchronize, the handful below covers nearly everything real projects reach for: **ColumnPrimaryKeyExpression** sets the key used to match source rows against target rows. Defaults to the configured EF primary key. Override when matching on a business key (SKU, ExternalId, IsoCode) rather than the database identity. **ColumnSynchronizeDeleteKeySubsetExpression** scopes the operation to a subset of the table. Covered in detail above. This is the one to set deliberately whenever the table holds more than the slice you mean to sync. **OnSynchronizeInsertInputExpression** and **OnSynchronizeUpdateInputExpression** choose which columns the insert phase and the update phase write, respectively. Use them when the source list is a partial projection (price and stock only, say) and you do not want EFE touching columns you did not load. **IgnoreOnSynchronizeInsertExpression** and **IgnoreOnSynchronizeUpdateNames** are the inverse: name the columns to leave out of the insert and update phases, and EFE writes everything else. Handy when audit columns (CreatedAt, ModifiedBy) are managed by triggers or by application code, and the bulk operation should not overwrite them. **SynchronizeSoftDeleteFormula** turns the delete branch into a soft delete. Instead of physically removing rows that fall out of the source list, EFE runs the SQL you supply, for example, setting IsDeleted = 1. This is the right tool when business rules say archive rather than erase. **UseAudit** captures a full before-and-after history of every row the sync inserts, updates, or deletes into a list you provide. Off by default because it costs extra SQL, but it is the clean answer when a sync needs a compliance trail. Pair it with **UseRowsAffected** to read back exactly how many rows were inserted, updated, and deleted from ResultInfo after the call returns. **BatchSize** and **BatchTimeout** control chunking and per-batch timeout. Worth tuning on very large syncs (one million rows and up) and on busy production databases where holding a long-running transaction is undesirable. **Log** attaches a delegate that captures the generated SQL. Use it during development to confirm the scoping expression compiled to what you expect, then turn it off in production unless you want the audit trail. # The Numbers The benchmark project that accompanies this post uses BenchmarkDotNet 0.14 on .NET 10 against SQL Server. The hand-rolled diff-and-apply pattern and BulkSynchronize are compared on a Products table with twelve properties, including string columns, decimal pricing, datetime audit fields, and a TenantId column for scoping. Measurements are mean execution time across five iterations after two warm-up rounds. Memory figures come from BenchmarkDotNet’s MemoryDiagnoser. For reference, EFE publishes figures of up to 14 times faster inserts and roughly 93 percent less save time versus SaveChanges(), and the suite below is meant to confirm that pattern in your own environment. ## Mixed sync (50% insert, 30% update, 20% implicit delete) **Source List****Hand-rolled diff-and-apply****BulkSynchronize (EFE)**1K rows131.7 ms121.6 ms10K rows618.7 ms286.4 ms50K rows2,990.9 ms1,382.9 ms100K rows5,979.4 ms2,733.8 ms500K rows35,699.2 ms19,156.6 ms## Steady-state sync (90% no-op, 10% update) A more realistic recurring-sync profile where most rows in the source list are unchanged. This is what nightly sync jobs typically look like after the initial seed. **Source List****Hand-rolled diff-and-apply****BulkSynchronize (EFE)**10K rows227.8 ms148.3 ms50K rows1,037.2 ms294.1 ms100K rows2,304.1 ms510.6 ms# When to Reach For BulkSynchronize and When Not To Not every sync problem is a BulkSynchronize problem. The honest guide: **Reach for BulkSynchronize** when the source list represents the desired state for a clearly bounded slice of a table, the slice can be expressed as a key subset (TenantId, SupplierId, ReportDate), and the row count is high enough that the memory and performance argument is meaningful. The four scenarios above all fit this pattern. **Reach for unscoped BulkSynchronize** when the entire table is the scope and the table is small. Currencies, Countries, ProductCategories, status codes. The full-mirror case is common and exactly what the default does. **Reach for the hand-rolled diff-and-apply pattern** when row counts are small (a few hundred or fewer), runs are infrequent, and per-row business logic is complex enough that you genuinely need imperative control over each transition. These cases exist, and BulkSynchronize is not trying to replace them. **Reach for TRUNCATE plus BulkInsertOptimized** when you can fully replace a table, there are no FK constraints to worry about, and you do not care about preserving identity values. This nuke-and-pave pattern can be faster than BulkSynchronize when the constraints allow it, because there is no diff to compute. **Skip EFE entirely** when you cannot add a paid dependency. The hand-rolled diff-and-apply pattern still works. Combine it with AddRange and ExecuteDelete to claw back some of the lost performance. # Production Notes Worth Knowing A short list of behaviors to plan for, ranked by how often they surprise teams the first time. ## Scope the Delete the Way You Mean It BulkSynchronize removes rows that fall in scope but out of the source list. ColumnSynchronizeDeleteKeySubsetExpression defines that scope; without it, the scope is the whole table. Set it deliberately, log the SQL once, read the WHEN NOT MATCHED BY SOURCE clause, and the behavior is yours to trust. If business rules prefer archiving over removal, SynchronizeSoftDeleteFormula converts the delete branch into a soft delete. ## The Operation Is Atomic; The Broader Workflow Is Yours BulkSynchronize runs its insert, update, and delete inside a single transaction, so the table is never left partly synced if one phase fails. The call also commits immediately rather than deferring to SaveChanges(). When a sync is one step in a larger unit of work, open an explicit transaction with context.Database.BeginTransactionAsync(), pass it through, and commit once every step succeeds, so an unrelated later failure rolls the sync back too. ## Foreign Key Constraints Still Apply Rows that BulkSynchronize removes are subject to database-level FK rules. If child tables reference rows the sync wants to remove, the operation fails or cascade-deletes, based on how your FKs are configured. For parent tables referenced elsewhere, either set explicit cascade behavior or sync the dependents first. ## The Change Tracker Goes Stale Any entities loaded in the current DbContext before BulkSynchronize ran are now stale. The database has moved. The tracker has not. Call context.ChangeTracker.Clear() or refresh affected entities before continuing to work with the context. ## Interceptors Do Not Fire; Use the Built-in Audit Because BulkSynchronize bypasses the change tracker, ISaveChangesInterceptor implementations and SaveChanges()-based domain events do not run. EFE covers the common reason teams rely on those hooks with its UseAudit option, which records every inserted, updated, and deleted row. For domain events, raise them explicitly after the sync, or move that logic to the database layer. ## Provider Behavior Varies BulkSynchronize works across SQL Server, Azure SQL, PostgreSQL, SQLite, MySQL, MariaDB, and Oracle. The underlying mechanism differs by provider. SQLite has no native bulk copy protocol, so EFE falls back to batched INSERT for staging, and the gain over the hand-rolled approach is smaller there than on SQL Server. Benchmark on your target provider. ## Identity Values Come Back by Default EFE writes database-generated identity values back to your in-memory entities after the sync, using a temp-table intermediate step. When you do not need the returned identities, set AutoMapOutputDirection to false for a faster path that skips that round trip. # Is It Worth the License? BulkSynchronize is part of [Entity Framework Extensions](https://entityframework-extensions.net/), a paid library from [ZZZ Projects](https://zzzprojects.com/), maintained continuously since 2014 across every major EF Core release. A rolling monthly free trial is available at entityframework-extensions.net for evaluation. The license decision is honest, and context decides. If your team runs scheduled sync jobs against external sources, refreshes reporting tables, or maintains per-tenant data in a multi-tenant application, BulkSynchronize is one of the EFE features that fill a genuine gap in native EF Core, alongside InsertFromQuery and BulkMerge. The combination of “there is no native equivalent” and “the hand-rolled version is tedious and slow” makes the licensing math clear at production volumes. If your team syncs a couple of small reference tables once a month, the hand-rolled pattern is acceptable, and the license cost is harder to justify on this feature alone. If your codebase already depends on EFE for BulkInsert, BulkMerge, or InsertFromQuery, BulkSynchronize is a free incremental win. Use it. # Closing BulkSynchronize collapses a category of code that almost every .NET team has written, debugged, and grown to dislike. Insert, update, and delete in a single server-side operation, wrapped in one transaction, with an empty-list guard and soft-delete support built in. It removes a real maintenance tax and a real source of subtle bugs. It is also the one bulk method that rewards a moment of deliberate configuration. Set ColumnSynchronizeDeleteKeySubsetExpression to match the slice you mean to sync, log the SQL once to confirm the scope, and put a test around it. Do that, and BulkSynchronize earns its place as the clean answer to a problem EF Core has never solved natively. [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly-1.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Scaling EF Core for Data Imports: From CSV Files to Millions of Database Rows](https://www.woodruff.dev/scaling-ef-core-for-data-imports/) **Published:** June 23, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/)# The Import Job Nobody Wants to Own Every team has one. It might be called an import service, a feed processor, or a sync job. The name varies. What stays constant is the shape of the problem: a large file arrives on schedule, and someone needs to get its contents into a database accurately, quickly, and without duplicating data already imported the last time the job ran. The file is usually a CSV. The destination is usually a SQL Server database backed by an EF Core application. And the code that runs the import was written by someone who has since left the company, handles 10,000 rows in seven minutes, and becomes the subject of an incident ticket whenever the supplier doubles the file size. This post works through a real import pipeline from start to finish. The scenario is a supplier product catalog: 500,000 rows, 15 columns, delivered nightly. Some rows are new. Some are updates to existing products. Some are identical to what is already in the database. The job runs every night and needs to finish in under five minutes. By the end, you will have a production-appropriate pipeline with concrete benchmark numbers to back the design choices. # Before You Touch the Database, Parse the File Correctly Most import tutorials start with a clean List already in memory. That is where the problems begin, because parsing is where the real friction lives. ## Use CsvHelper and a Dedicated DTO CsvHelper is the standard for .NET CSV parsing. The first design decision is to map the CSV to a dedicated data transfer object, not directly to the EF Core entity. This matters more than most developers realise. Supplier CSV column names rarely match your entity property names. Value formats differ. Dates arrive as dd/MM/yyyy. Prices arrive as strings with commas. Nullable fields arrive as empty strings, not null. When you map directly to the EF Core entity, every one of these mismatches becomes a runtime failure or a silent data corruption. ``` A ProductImportRow DTO models the CSV shape exactly: public class ProductImportRow {     public string Sku { get; set; }     public string Name { get; set; }     public string Description { get; set; }     public string PriceRaw { get; set; }              // e.g. "1,234.56" - cleaned during mapping     public string LastSupplierUpdateRaw { get; set; } // e.g. "15/03/2025" - parsed during mapping     public string SupplierCode { get; set; }     public string CategoryCode { get; set; }     public string WeightKg { get; set; }     public string IsAvailableFlag { get; set; }       // "Y" or "N" } The CsvHelper ClassMap handles column-to-property name translation and type conversion in one place: public class ProductImportRowMap : ClassMap {     public ProductImportRowMap()     {         Map(m => m.Sku).Name("SUPPLIER_SKU");         Map(m => m.Name).Name("PRODUCT_NAME");         Map(m => m.PriceRaw).Name("LIST_PRICE_GBP");         Map(m => m.LastSupplierUpdateRaw).Name("LAST_MODIFIED");         // ... remaining column mappings     } } ``` The entity mapping layer converts ProductImportRow to Product with validated, typed values. Keep the two concerns separate. ## Stream in Chunks, Not All at Once GetRecords() in CsvHelper returns IEnumerable and reads lazily. The file stays out of memory as long as you avoid calling .ToList() on the full result. Do not call **.ToList()** on the full file. Process in chunks of 5,000 rows. Read a chunk, process it, commit it, discard it, and move to the next. This keeps memory consumption constant regardless of file size. ``` private static async IAsyncEnumerable ReadCsvInChunksAsync(     string filePath, int chunkSize = 5_000) {     using var reader = new StreamReader(filePath, detectEncodingFromByteOrderMarks: true);     using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);     csv.Context.RegisterClassMap();     var chunk = new List(chunkSize);     await foreach (var record in csv.GetRecordsAsync())     {         chunk.Add(record);         if (chunk.Count == chunkSize)         {             yield return chunk;             chunk = new List(chunkSize);         }     }     if (chunk.Count > 0) yield return chunk; } ``` # The Naive Implementations, and Where Each One Fails The progression from first attempt to production-appropriate code is predictable. Here are the three stages most developers go through, and the ceiling each one hits. ## Failure Mode 1: Add-in-a-Loop ``` foreach (var row in csvRows) {     var product = MapToEntity(row);     context.Products.Add(product);     await context.SaveChangesAsync();  // one INSERT per row, one round-trip per INSERT } ``` DetectChanges() fires on every Add(). One round-trip per entity. At 500,000 rows, this runs for hours. This is where every tutorial starts, and it should be where it ends. At any meaningful volume, this code belongs in a code review comment, not in production. ## Failure Mode 2: AddRange with a Single SaveChanges on the Full File ``` var products = csvRows.Select(MapToEntity).ToList();  // 500K objects in memory context.Products.AddRange(products); await context.SaveChangesAsync(); ``` EF Core 10 batches the generated INSERT statements up to 1,000 rows per batch. The round-trip problem is solved. The memory problem is not. At 500,000 moderately complex entities, the change tracker holds multi-gigabyte allocations before a single INSERT executes. The second problem is the one that will haunt the next person who maintains this code: it is unconditional. Run it twice and you duplicate every row. ## Failure Mode 3: Chunked AddRange (The Good, the Incomplete) ``` foreach (var chunk in csvRows.Chunk(5_000)) {     var products = chunk.Select(MapToEntity).ToList();     context.Products.AddRange(products);     await context.SaveChangesAsync();     context.ChangeTracker.Clear(); } ``` Memory is handled. Batching works within each chunk. This is the correct starting point for an initial load into an empty table. Still, it is unconditional. Re-run it the next night, and it inserts duplicates. The moment the business requirement is ‘apply this file nightly and keep the database in sync,’ chunked AddRange stops being the right tool. # The Upsert Problem That Native EF Core Cannot Solve Night one: the table is empty. Chunked AddRange works. Night two: 480,000 of the 500,000 rows already exist. The other 20,000 are new products. Some of the existing products have changed. The obvious fix is to check first and then insert or update. ## The Manual Check-Then-Insert-or-Update Pattern ``` foreach (var chunk in csvRows.Chunk(5_000)) {     var skus = chunk.Select(r => r.Sku).ToList();     var existing = await context.Products         .Where(p => skus.Contains(p.Sku))         .ToDictionaryAsync(p => p.Sku);     foreach (var row in chunk)     {         if (existing.TryGetValue(row.Sku, out var product))             UpdateEntity(product, row);         else             context.Products.Add(MapToEntity(row));     }     await context.SaveChangesAsync();     context.ChangeTracker.Clear(); } ``` This pattern has four compounding costs: - **One SELECT per chunk.** At 5,000-row chunks, a 500,000-row file issues 100 SELECT statements before a single INSERT or UPDATE executes. - **The .Contains() parameter limit.** SQL Server caps a single command at 2,100 parameters. EF Core 10 changed the default Contains() translation to one scalar parameter per item, so a 5,000-item list generates roughly 5,000 parameters and the query throws once it crosses the 2,100 ceiling. (EF Core 8 and 9 defaulted to an OPENJSON single-parameter translation that stayed under the limit but produced weaker query plans.) A Contains() over a chunk this size is the wrong tool either way; chunk the list or use WhereBulkContains from EFE. - **Full entity materialisation.** The SELECT loads complete entities even when you plan to update only 3 of 15 columns. Every tracked property occupies memory in the change tracker. - **Change tracker overhead at scale.** Every loaded entity goes through DetectChanges(). At 100 chunks of 5,000 each, this adds up to substantial CPU time on complex entity graphs. ## What EF Core 10 Provides and Where It Stops EF Core 10’s ExecuteUpdate handles set-based updates: apply the same transformation to every matching row in one SQL statement. That covers ‘mark all products in category X as discontinued.’ It does not cover ‘update each of these 5,000 products with the specific values from the CSV row that matches its SKU.’ Per-row upsert with custom business key matching and selective column writes is not a problem ExecuteUpdate can solve. That gap is exactly what BulkMerge fills. # BulkMerge from Entity Framework Extensions [Entity Framework Extensions](https://entityframework-extensions.net/) (EFE) from ZZZ Projects ships a BulkMerge method that executes a server-side MERGE statement per chunk. No SELECT round-trips. No change tracker overhead. The merge logic runs entirely inside the database. The baseline call is straightforward: ``` await context.BulkMergeAsync(products); ``` By default, EFE uses the entity’s configured primary key as the merge key. Rows where the key exists in the target table are updated. Rows where it does not exist are inserted. That is the happy path. The supplier catalogue scenario requires three additional options. ## ColumnPrimaryKeyExpression: Matching on a Business Key The supplier CSV carries a Sku column. The database Product.Id is an auto-increment integer that the supplier does not know about and does not include in the file. Matching on the database primary key is not possible. ``` await context.BulkMergeAsync(products, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku; }); ``` EFE uses Sku as the merge key. New SKUs get inserted with a database-generated Id. Existing SKUs are updated. The CSV never needs to carry database-generated keys, which keeps the supplier integration clean. ## ColumnInputExpression: Writing Only What the CSV Provides The Product entity has 15 properties. The CSV provides 10 of them. The remaining five — CreatedAt, CreatedBy, InternalCategory, InventoryCount, IsActive — are managed by the application and must not be overwritten by an import job. ``` await context.BulkMergeAsync(products, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku;     options.ColumnInputExpression = p => new     {         p.Sku,         p.Name,         p.Description,         p.Price,         p.SupplierCode,         p.Category,         p.Weight,         p.IsAvailable,         p.LastSupplierUpdate     }; }); ``` Only the listed columns are written during both INSERT and UPDATE operations. Application-managed columns are left untouched on update. On INSERT, unlisted columns receive their database or EF Core defaults. ## MergeMatchedAndFormula: Conditional Updates The supplier re-sends unchanged rows in every nightly file. Without a staleness check, every existing product gets an UPDATE that writes the same values it already has. At 480,000 existing rows, that is 480,000 unnecessary writes per night. ``` await context.BulkMergeAsync(products, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku;     options.ColumnInputExpression = p => new     {         p.Sku, p.Name, p.Description, p.Price,         p.SupplierCode, p.Category, p.Weight,         p.IsAvailable, p.LastSupplierUpdate     };     options.MergeMatchedAndFormula =         "StagingTable.LastSupplierUpdate > DestinationTable.LastSupplierUpdate"; }); ``` StagingTable refers to the incoming source rows; DestinationTable refers to the rows already in the database. EFE injects the formula straight into the matched condition of the MERGE statement, so the staleness check runs in the database and no rows are loaded into memory for comparison. Treat the formula as raw SQL: keep full control over its contents and never build it from user input, to avoid SQL injection. ## The Full Pipeline ``` await foreach (var chunk in ReadCsvInChunksAsync(filePath, chunkSize: 5_000)) {     var products = chunk         .Where(IsValid)         .Select(MapToEntity)         .ToList();     await context.BulkMergeAsync(products, options =>     {         options.ColumnPrimaryKeyExpression = p => p.Sku;         options.ColumnInputExpression = p => new         {             p.Sku, p.Name, p.Description, p.Price,             p.SupplierCode, p.Category, p.Weight,             p.IsAvailable, p.LastSupplierUpdate         };         options.MergeMatchedAndFormula =             "StagingTable.LastSupplierUpdate > DestinationTable.LastSupplierUpdate";     }); } ``` No SELECT per chunk. No change tracker. Each chunk produces one MERGE statement. The same file can run every night without producing duplicates, overwriting application-managed data, or updating rows that have not changed. Licensing: BulkMerge is part of EFE’s paid library. A rolling monthly free trial is available at [entityframework-extensions.net](https://entityframework-extensions.net/). For teams running import pipelines at this scale on a regular schedule, the library removes a category of boilerplate that is difficult to test, slow to maintain, and easy to get wrong. # Benchmark Results All benchmarks use BenchmarkDotNet 0.14 on .NET 10 against SQL Server. Mean execution time across five iterations after two warm-up rounds. Managed allocations reported via MemoryDiagnoser. Test entity: 15-property Product with string, decimal, DateTime, and nullable fields. Document your test environment before publishing; LocalDB results will differ substantially from a dedicated SQL Server instance. **Initial Load (Empty Target Table)** **Row Count****Chunked AddRange****BulkInsert (EFE)****BulkMerge (EFE)**10K895.3 ms264.8 ms259.8 ms50K4,190.9 ms1,086.5 ms1,228.5 ms100K7,704.8 ms1,941.2 ms2,633.0 ms500K43,086.3 ms8,408.9 ms15,077.3 ms**Re-Run Scenario (Table 90% Populated)** **Row Count****Manual Check-Then-Upsert****BulkMerge (EFE)**10K607.7 ms256.1 ms50K3,709.9 ms1,351.0 ms100K123,672.1 ms2,852.1 ms500K582,282.8 ms19,323.1 ms## What to Look For in the Results The initial load comparison shows when BulkMerge is worth the MERGE overhead over a straight BulkInsert on an empty table. For initial loads where the table is known to be empty, BulkInsert or BulkInsertOptimized will be faster. The re-run comparison is where the manual check-then-upsert pattern collapses at scale. Watch the SELECT round-trip count on the manual approach as the table fills from 50% to 90% to 99%. Each chunk’s SELECT grows in cost as more rows exist to match. Memory tells the second story. The manual approach accumulates tracked entities per chunk even with ChangeTracker.Clear() between chunks. BulkMerge holds only the chunk list in memory during the operation. # Decision Guide **Scenario****Recommended Approach****Why**Initial load, empty table, identity values not neededBulkInsertOptimized (EFE) or SqlBulkCopyMaximum throughput, no MERGE overheadInitial load, empty table, identity values needed for graphBulkInsert (EFE) with IncludeGraphFK propagation handled automaticallyNightly re-run, rows may or may not already existBulkMerge with ColumnPrimaryKeyExpressionOne MERGE per chunk, no SELECT round-tripsNightly re-run, existing rows must not changeBulkInsert with InsertIfNotExists = trueInserts new rows only; existing rows left untouchedConditional update based on data currencyBulkMerge with MergeMatchedAndFormulaSQL formula evaluated server-side in the MERGESelective column write (partial schema from source)BulkMerge with ColumnInputExpressionApplication-managed columns left untouchedSmall import, under 1K rows, simple entitiesAddRange + SaveChangesOverhead of BulkMerge setup is not justifiedMulti-database (SQL Server plus PostgreSQL)BulkMerge (EFE)SqlBulkCopy is SQL Server only; EFE supports all major providers# What Will Catch You Off-Guard in Production ## Transaction scope for multi-chunk jobs BulkMerge executes immediately per chunk. If the pipeline fails on chunk 47 of 100, chunks 1 through 46 are already committed. For jobs where partial success is unacceptable, wrap the entire run in an explicit transaction. Be aware that a transaction spanning 500,000 rows and several minutes creates lock escalation risk and long-running transaction log pressure. For most nightly imports, chunk-level commit with a re-runnable job design is the safer choice. ## The parameter limit trap in manual fallbacks Any part of the pipeline that uses .Where(p => ids.Contains(p.Id)) with a list of more than roughly 2,000 items risks failure on SQL Server. EF Core 10 translates Contains() to one scalar parameter per item by default, so a large list crosses the 2,100-parameter ceiling and the query throws. (EF Core 8 and 9 used an OPENJSON single-parameter translation that avoided the limit but gave weaker plans; you can opt back into it with EF.Parameter() or UseParameterizedCollectionMode.) For large lists, use WhereBulkContains from EFE or chunk the ID list explicitly. ## CSV encoding and BOM handling UTF-8 with BOM is common from supplier systems. Open the stream with new StreamReader(path, detectEncodingFromByteOrderMarks: true). File.ReadAllLines() without explicit encoding can produce a garbage character at the start of the first column header, which breaks ClassMap column matching silently. ## Mapping drift between CSV columns and entity properties ColumnInputExpression defines which properties are written to the database. If a property is renamed during refactoring and the expression lambda is not updated, the old property name compiles correctly but writes a zero or default value. Add a test that round-trips a known CSV row through the full pipeline and asserts values at the entity level. ## MergeMatchedAndFormula and null column values The formula runs as SQL. If LastSupplierUpdate is nullable and null for rows inserted before the field existed, a comparison where either side is null evaluates to UNKNOWN, so the MERGE skips the row rather than updating it. That is usually the safe outcome, but confirm it matches your intent. Wrap both sides in ISNULL or COALESCE inside the formula, or run a one-time backfill, if those rows should still update. ## EF Core interceptors do not fire Business logic, audit logging, or domain events wired through ISaveChangesInterceptor will not execute for BulkMerge or any other EFE bulk operation. If the import job should produce audit log entries, implement that at the application level before or after the BulkMerge call. # Conclusion The import pipeline problem that looks simple is one of the most consistently underestimated in .NET data work. The code that handles 10,000 rows in development collapses at 500,000 in production. The code that works for an initial load breaks on night two when re-running against a populated table. The path through this looks like three decisions made in order. Parse with CsvHelper into a dedicated DTO and stream in chunks. Use chunked AddRange + SaveChanges for initial loads into empty tables where simplicity outweighs raw speed. Reach for BulkMerge with ColumnPrimaryKeyExpression, ColumnInputExpression, and MergeMatchedAndFormula the moment the requirement becomes ‘apply this file nightly and keep the database current.’ EFE is a paid library. The case for it is strongest for teams running import pipelines on a schedule, where the manual check-then-upsert alternative requires ongoing maintenance and scales more slowly as the database grows. If your import job runs once, loads an empty table, and never runs again, BulkInsertOptimized or SqlBulkCopy covers the need without a license cost. The next post in this series covers BulkSynchronize, which takes the sync concept one step further: insert, update, and delete in a single operation to keep a target table fully mirrored to a source dataset. ***Sponsored content in partnership with ZZZ Projects.*** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [BulkMerge (Upsert) in EF Core: How to Insert-or-Update Without the Headache](https://www.woodruff.dev/bulkmerge-upsert-in-ef-core-how-to-insert-or-update-without-the-headache/) **Published:** June 18, 2026 **Author:** Chris Woodruff **Content:** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/)Picture this. Your inbox lights up at 7:14 AM. A supplier just pushed 50,000 product records to your endpoint. Half are new. The other half are updates to products already sitting in your database. You have no idea which is which. The feed does not tell you. The records do not carry your internal primary keys. And the import job is supposed to finish before the morning batch report goes out at 8:00. You write the code that every EF Core developer has written at least once: ``` foreach (var incoming in productsFromFeed) {     var existing = await context.Products         .FirstOrDefaultAsync(p => p.Sku == incoming.Sku);     if (existing is null)         context.Products.Add(incoming);     else     {         existing.Price = incoming.Price;         existing.Stock = incoming.Stock;         existing.UpdatedAt = DateTime.UtcNow;     } } await context.SaveChangesAsync(); ``` It works. The first thousand records process in seconds. By the time the loop hits ten thousand, the application has issued ten thousand SELECT round-trips, allocated ten thousand tracked entities, and is starting to crawl. At fifty thousand, your batch window is gone. This is the upsert problem, and EF Core 10 still ships with no built-in answer to it. This post walks through the real options available in 2026: the manual pattern and why it fails, raw T-SQL MERGE as the zero-dependency alternative, [Entity Framework Extensions](https://entityframework-extensions.net/) BulkMerge as the API that fills the gap, and a clear-eyed look at when each one earns its place in your stack. # Why EF Core Does Not Ship an Upsert Many developers come to EF Core expecting an AddOrUpdate method. Entity Framework 6 shipped one, but it lived in the Migrations namespace and was built for seeding, not high-volume runtime upserts. It issues a database round-trip for every entity and calls DetectChanges on each one, so it behaves like Add in a loop rather than a batched operation. The muscle memory persists anyway. There is no equivalent in EF Core 10, and there are no plans to add one. Instead, EF Core’s toolbox for insert-or-update scenarios looks like this: - Add() and SaveChanges() for inserts you know are inserts - Update() and SaveChanges() for updates you know are updates - ExecuteUpdate() and ExecuteDelete() for set-based operations where the predicate already identifies the right rows - Raw SQL via ExecuteSqlRaw for when none of the above fits None of these handles the mixed payload problem. The Add/Update split requires you to already know which records exist. ExecuteUpdate applies a uniform rule and cannot insert at all. Raw SQL works, and we will come back to it shortly, but you write and maintain the SQL yourself. So the workaround is the check-then-write pattern from the opening, and it has three serious problems. **Problem 1: Round-trip cost.** Every existence check is a database round-trip. Fifty thousand records becomes fifty thousand SELECTs before a single write happens. You can batch the checks with a Contains() call against the incoming keys, but watch how EF Core translates it. EF Core 8 and 9 send the list as a single JSON parameter unpacked server-side with OPENJSON, which sidesteps SQL Server’s 2,100-parameter ceiling entirely. EF Core 10 changed the default back to one scalar parameter per item, so a list approaching 2,100 entries now fails at runtime unless you opt back in with UseParameterizedCollectionMode(ParameterTranslationMode.SingleJsonParameter) or wrap the call in EF.Constant. Either way, batched existence checking is a workaround, not a fix. **Problem 2: Race conditions.** Between the SELECT that says “this Sku does not exist” and the INSERT that adds it, another process can insert the same Sku. You get a primary key violation and a half-completed batch. Retry logic and idempotency checks help, but they add code you would rather not write. **Problem 3: Code volume.** A working check-then-insert-or-update with batched existence checks, error handling, and idempotency is forty to sixty lines of code that every project reinvents. It is the most copy-pasted snippet in the EF Core world, and most of those copies have subtle bugs. The honest answer for why this gap exists: writing a correct, atomic, performant upsert requires either database-specific SQL (MERGE on SQL Server, INSERT…ON CONFLICT on PostgreSQL, INSERT OR REPLACE on SQLite) or a sophisticated abstraction over all of them. EF Core has chosen to ship neither. # The Zero-Dependency Option: Raw T-SQL MERGE Before reaching for any external library, give the raw SQL approach a fair hearing. If you are on SQL Server, own your data layer, and do not mind writing provider-specific SQL, MERGE handles the upsert in a single atomic statement: ``` const string sql = @"     MERGE INTO Products AS target     USING (SELECT @Sku AS Sku, @Price AS Price, @Stock AS Stock) AS source     ON target.Sku = source.Sku     WHEN MATCHED THEN         UPDATE SET Price = source.Price, Stock = source.Stock,                    UpdatedAt = SYSUTCDATETIME()     WHEN NOT MATCHED THEN         INSERT (Sku, Price, Stock, CreatedAt)         VALUES (source.Sku, source.Price, source.Stock, SYSUTCDATETIME());"; await context.Database.ExecuteSqlRawAsync(sql,     new SqlParameter("@Sku", product.Sku),     new SqlParameter("@Price", product.Price),     new SqlParameter("@Stock", product.Stock)); ``` This is one round-trip per record. Atomic. Free. Zero dependencies. For small payloads on SQL Server, it is a perfectly defensible choice. The catch is that single-row MERGE still does N round-trips for N records. The real win comes when you push the whole batch into a table-valued parameter and MERGE against it: ``` // Build a DataTable matching a user-defined table type in SQL Server var table = new DataTable(); table.Columns.Add("Sku", typeof(string)); table.Columns.Add("Price", typeof(decimal)); table.Columns.Add("Stock", typeof(int)); foreach (var p in products)     table.Rows.Add(p.Sku, p.Price, p.Stock); var tvpParam = new SqlParameter("@Incoming", SqlDbType.Structured) {     TypeName = "dbo.ProductUpsertType",     Value = table }; const string sql = @"     MERGE INTO Products AS target     USING @Incoming AS source     ON target.Sku = source.Sku     WHEN MATCHED THEN         UPDATE SET Price = source.Price, Stock = source.Stock,                    UpdatedAt = SYSUTCDATETIME()     WHEN NOT MATCHED THEN         INSERT (Sku, Price, Stock, CreatedAt)         VALUES (source.Sku, source.Price, source.Stock, SYSUTCDATETIME());"; await context.Database.ExecuteSqlRawAsync(sql, tvpParam); ``` Now it is one round-trip for the whole batch. Fast. Atomic. Still free. What you give up: - A user-defined table type per upsert target, kept in sync with your entity schema - LINQ integration: zero. You hand-write the column list, the match predicate, and the assignments. - Multi-provider support: zero. The PostgreSQL and SQLite equivalents are different SQL with different semantics. - Graph upsert: not happening. Parent and child upserts are separate statements with manual FK wiring between them. For a small team on SQL Server with infrequent upserts, this is enough. For anyone doing this regularly across providers, the maintenance cost adds up fast. # BulkMerge: The Upsert Primitive EF Core Forgot to Ship Entity Framework Extensions (EFE) from ZZZ Projects fills the upsert gap with BulkMerge. It accepts a list of entities and, in a single database operation, inserts records that do not exist while updating those that do. It uses whatever match key you specify. It works on SQL Server, PostgreSQL, SQLite, MySQL, MariaDB, and Oracle. And it integrates with your existing EF Core models and mappings without requiring you to touch your DbContext. Install: `dotnet add package Z.EntityFramework.Extensions.EFCore` Version match matters: EF Core 10 needs EFE v10. Mismatches throw cryptic runtime errors at the first BulkMerge call. Now your fifty-thousand-row upsert becomes this: ``` using Z.EntityFramework.Extensions; await context.BulkMergeAsync(productsFromFeed, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku; }); ``` Three lines. One database round-trip (give or take batching). Both branches handled atomically by the server. Notice what is missing from that snippet. No SaveChanges call. No SELECT to find existing rows. No split into two lists. No transaction ceremony when nothing else demands one. BulkMerge executes immediately and walks away. ## The match key is the whole game The single most common BulkMerge bug is leaving the match key at its default. Without ColumnPrimaryKeyExpression, EFE matches on the EF Core primary key, which is your identity column. For records coming from an external source, that identity column is zero or default on every row. Every record looks new. Every record gets inserted. Your upsert quietly becomes an insert-only operation with duplicate Skus piling up. Always set the match key to a business identifier: ``` // Single business key options.ColumnPrimaryKeyExpression = p => p.Sku; // Composite business key options.ColumnPrimaryKeyExpression = p => new { p.SupplierCode, p.Sku }; ``` If you take one thing from this post, take that one. ## Conditional logic: insert-only, update-only, and column-level control BulkMerge ships with the three modes you actually need: ``` // Insert only: skip the update phase, matched rows are left untouched options.IgnoreOnMergeUpdate = true; // Update only: skip the insert phase, unmatched rows are not added options.IgnoreOnMergeInsert = true; ``` There is no native EF Core equivalent for either. Manual versions require pre-querying every key. Column-level control is provided by OnMergeUpdateInputExpression and OnMergeInsertInputExpression. These select which columns each phase writes; they do not compute values, so a timestamp like CreatedAt or UpdatedAt is set on the entities before the call (or left to a database default): ``` await context.BulkMergeAsync(productsFromFeed, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku;     // INSERT phase: which columns to write for brand-new rows     options.OnMergeInsertInputExpression = p => new     {         p.Sku, p.Name, p.Description, p.Price, p.Stock, p.CreatedAt     };     // UPDATE phase: only these columns are written; Description is omitted so curated values survive     options.OnMergeUpdateInputExpression = p => new     {         p.Price, p.Stock, p.UpdatedAt     }; }); ``` That last point is where the conditional API earns its keep. If your supplier feed includes a Description field, but your marketing team curates that field manually in the admin panel, you do not want the nightly feed overwriting their work. OnMergeUpdateInputExpression lets you list exactly which columns the update branch writes. Everything you leave out stays put. The equivalent denylist option is IgnoreOnMergeUpdateExpression, where you name the columns to exclude instead. To pull off the same behavior in raw MERGE, you write it into the UPDATE clause by hand and remember to fix it every time the schema changes. In the manual check-then-update pattern, you do it column by column in C#. With BulkMerge, it is one expression. # Graph Upsert: When the Headache Gets Worse Flat upserts are the easy case. The hard case is parent-child upserts. You receive a list of orders, each with a collection of line items. Some orders are new. Some already exist and their items need refreshing. The FK relationships need wiring up correctly, the parent IDs need propagating to the children, and you have to do all of it without leaving the database in an inconsistent state if something fails halfway through. The manual approach with raw MERGE or BulkMerge-without-IncludeGraph goes like this: 1. BulkMerge the orders, matching on OrderNumber 2. Query back the merged orders to pick up their database-assigned IDs 3. Assign those IDs to the OrderId FK on every incoming line item 4. BulkMerge the line items, matching on (OrderId, LineNumber) 5. Hope nothing fails between steps 1 and 4 If any step fails, the database is in a partially-merged state. You wrap the whole sequence in a transaction. You add retry logic. You write integration tests. You spend a Tuesday afternoon arguing about whether the line items should match on a composite key or a synthetic one. Or you do this: ``` await context.BulkMergeAsync(ordersFromFeed, options => {     options.ColumnPrimaryKeyExpression = o => o.OrderNumber;     options.IncludeGraph = true;     options.IncludeGraphOperationBuilder = operation =>     {         if (operation is BulkOperation lineOp)         {             lineOp.ColumnPrimaryKeyExpression = ol =>                 new { ol.OrderId, ol.LineNumber };         }     }; }); ``` EFE walks the navigation properties, figures out the dependency order, merges the parents, propagates the new IDs to the child FK columns, and merges the children. One call. One logical transaction. The line items get their parent OrderId values populated automatically because EFE knows what the Order navigation property points to. A few things to watch for with IncludeGraph: - Disable lazy loading before the call. If lazy loading is on, IncludeGraph will trigger it on every navigation property it touches, dragging far more data into memory than you want and then bulk-merging all of it. - Specify the match key per entity type. The default behavior (PK matching) is wrong for incoming records at every level, not just the root. - Test the graph shape end to end. Three levels of nesting work. So do five. But the failure modes get harder to debug as the graph deepens, so verify your specific shape before deploying. # BulkMerge vs. BulkSynchronize: Know the Difference Before You Lose Data There is a sibling operation in EFE that catches developers off guard: BulkSynchronize. It looks like BulkMerge and behaves like BulkMerge for the rows in your payload. Then it does one more thing. It deletes every database row that is not in your payload. This is by design. BulkSynchronize is for full-table mirror operations: keep this table exactly matching this list. Anything not in the list does not belong here anymore. The two operations target different problems: **Aspect****BulkMerge****BulkSynchronize**Operations performedInsert plus UpdateInsert plus Update plus DeleteRows not in payloadUntouchedDeleted from the databaseTypical use casePartial feed, incremental syncFull mirror, reference data refreshRisk profileStale rows accumulate over timeWrong payload deletes production dataThe decision question is simple: does the incoming payload represent the complete desired state of the table, or is it a partial update? If you are receiving a nightly product feed that may or may not include every product, you want BulkMerge. If you are refreshing a reference table from an authoritative source and any missing row should be removed, you want BulkSynchronize. Use BulkSynchronize with extra care. Wrap it in a transaction. Log the affected row counts. Run it against staging first. # Benchmarks: What Actually Happens at Scale The benchmark project that ships with this post measures all three approaches against a Products table with twelve columns, using BenchmarkDotNet 0.14 on .NET 10 against SQL Server 2022. Each measurement is the mean across five iterations after two warm-up runs. Memory figures come from MemoryDiagnoser. ## Flat upsert benchmarks: Product records, business key match **Row Count****Manual Check-Then-Upsert****Raw T-SQL MERGE (TVP)****BulkMerge (EFE)**1,00071.80 ms40.77 ms89.42 ms10,000471.20 ms692.04 ms245.75 ms50,0002123.57 ms405.59 ms822.77 ms100,0004849.47 ms841.42 ms1401.38 ms500,00022202.41 ms3577.61 ms6104.04 ms## Conditional merge benchmarks at 50,000 rows **Mode****What It Does****Time****Notes**BulkMerge (full upsert)Insert plus Update742.6 msBoth branches activeIgnoreOnMergeUpdateInsert only458.7 msUpdate branch skippedIgnoreOnMergeInsertUpdate only388.3 msInsert branch skipped## Graph upsert: Orders with 5 line items each **Order Count****Manual Multi-Pass****BulkMerge + IncludeGraph**1,000 (5K lines)165.1 ms163.3 ms10,000 (50K lines)701.9 ms731.4 ms50,000 (250K lines)3477.8 ms3514.3 ms## Reading the numbers Three patterns are worth calling out: The manual check-then-upsert pattern scales linearly with row count. Doubling the input doubles the time, and at fifty thousand rows, you are looking at a multi-second operation that should have been a sub-second one. The cost is dominated by round-trips, not by SQL execution time. Raw T-SQL MERGE with a table-valued parameter sits in the same performance tier as BulkMerge. This is a fair fight on speed. The difference is that the MERGE requires a maintained TVP type, hand-written SQL, and provider-specific syntax. BulkMerge gives you the same throughput with LINQ-shaped options and provider portability. BulkMerge’s wider lead shows up in graph scenarios. Manual multi-pass requires you to read parent IDs back, propagate them to children, and merge again. IncludeGraph collapses that work into a single API call and beats the manual pattern at every row count tested. # When to Reach for What The decision comes down to four questions. Answer them in order: **Is this a SQL Server only project with infrequent upserts?** Raw T-SQL MERGE via ExecuteSqlRaw with a table-valued parameter is enough. Zero dependencies. Maintainable if you have someone on the team who reads SQL well. **Do you need multi-provider support, LINQ integration, or column-level conditional logic?** BulkMerge with ColumnPrimaryKeyExpression and OnMergeUpdateInputExpression. The license cost is real, and the value lands where the manual approach hurts most. **Are you upserting parent-child graphs at volume?** BulkMerge with IncludeGraph. The manual multi-pass alternative is where most teams introduce data integrity bugs. **Does the incoming payload represent the entire desired state of the table?** BulkSynchronize. Otherwise, BulkMerge. Mixing these up is how production data gets deleted. **Scenario****Recommended****Why**Low volume, SQL Server, no library budgetRaw T-SQL MERGEFree, atomic, fast enough for the volumeMixed-provider stackBulkMerge (EFE)Provider abstraction with the same speedConditional column updatesBulkMerge + OnMergeUpdateInputExpressionNo native equivalent that is simpleParent-child graph upsertBulkMerge + IncludeGraphEliminates the multi-pass and FK wiringFull-table mirrorBulkSynchronizeHandles the delete branch BulkMerge does not## Caveats That Will Catch You Off-Guard **BulkMerge is not deferred.** It executes the moment you call it. If a later SaveChanges or BulkInsert in the same logical unit of work fails, BulkMerge will not roll back automatically. Wrap related operations in an explicit transaction: ``` await using var tx = await context.Database.BeginTransactionAsync(); await context.BulkMergeAsync(products, options => {     options.ColumnPrimaryKeyExpression = p => p.Sku; }); await context.SaveChangesAsync(); await tx.CommitAsync(); ``` **EF Core interceptors do not fire.** If your audit logging, domain events, or change tracking hooks live in ISaveChangesInterceptor, BulkMerge bypasses them entirely. EFE provides its own UseAudit option for capturing affected rows. Verify it meets your compliance requirements before assuming feature parity. **In-memory entities go stale.** After BulkMerge, any entity already tracked by the context still holds its pre-merge state. Computed columns, server-assigned timestamps, and concurrency tokens are not refreshed. Re-query if you need the post-merge values. **Provider semantics differ.** On SQL Server, EFE stages the rows in a temporary table and runs a MERGE from it. Other providers differ: PostgreSQL builds on INSERT…ON CONFLICT DO UPDATE (EFE exposes options such as UsePostgreOnMergeSqlInsertOnConflictDoUpdate to control this), SQLite uses its own ON CONFLICT upsert rather than MERGE, and MySQL uses INSERT…ON DUPLICATE KEY UPDATE, which has subtly different update semantics. The exact mechanism varies by provider and version, so test on your target provider, not just on the development LocalDB instance. **The match key has to actually be unique.** If your ColumnPrimaryKeyExpression points to a column that is not actually unique in the database, BulkMerge will match the first row it finds, update it, and leave the duplicates untouched. Add a unique index on the business key before relying on it. ## The Free Alternatives Question EFE is a paid library. Before recommending it, the question worth answering honestly is: what do you give up by sticking with free options? **Option****Upsert****Graph****Multi-Provider****Notes**Native EF Core 10No primitiveN/AAll providersManual workaround onlyRaw T-SQL MERGEYesManual, fragileSQL Server onlyFree, fast, schema-coupledEFCore.BulkExtensions (borisdj)Yes (BulkInsertOrUpdate)LimitedSQL Server, PostgreSQL, SQLiteDual licence: free under $1M revenue, commercial aboveEFE BulkMergeFull upsert plus conditional logicIncludeGraph with per-type optionsAll major providersPaid, perpetual licenseEFCore.BulkExtensions by borisdj is the most credible free competitor. Its BulkInsertOrUpdate covers the common flat upsert case well. The gaps are in graph support, conditional column logic, and the depth of the options API. If your scenarios stay within those constraints, it is a defensible choice and worth benchmarking alongside EFE before committing to a license. The honest position: EF Core 10 has closed a lot of the original gap that made EFE feel mandatory. ExecuteUpdate, ExecuteDelete, and improved batching cover many scenarios that used to require a third-party library. Upsert is one of the cases that did not get closed. If you are doing it regularly, at volume, with complex graphs or per-column conditional logic, BulkMerge is where EFE earns the license fee. If you are doing it occasionally with flat entities, a free option is probably enough. # Wrapping Up Upsert is the case EF Core forgot to ship. The manual workaround is slow, brittle, and reinvented in every codebase. Raw T-SQL MERGE handles SQL Server scenarios well at the cost of portability and LINQ integration. [Entity Framework Extensions](https://entityframework-extensions.net/) EFE’s BulkMerge fills the gap with business key matching, conditional column logic, and graph support, on every major .NET database provider. The points worth remembering: - Set ColumnPrimaryKeyExpression to a business key. The default identity-column matching is almost always wrong for incoming records. - Use OnMergeUpdateInputExpression (or IgnoreOnMergeUpdateExpression) when some columns should not be overwritten on update. This is the option that saves your manually curated data from getting flattened by a feed. - Reach for IncludeGraph when upserting parent-child hierarchies. The manual multi-pass alternative is where data integrity bugs hide. - Do not confuse BulkMerge with BulkSynchronize. One leaves stale rows alone. The other deletes them. - Wrap BulkMerge in a transaction if it shares a unit of work with other operations. The next post in this series tackles the larger scenario this all builds toward: scaling EF Core for data im ***Sponsored content in partnership with ZZZ Projects.*** [![EF Extensions header banner with logo, listing Bulk Insert, Bulk Update, Bulk Merge and the tagline 'Save thousands of entities — in milliseconds'.](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [New Release!! htmxRazor v2.1.0: Advanced Inputs](https://www.woodruff.dev/new-release-htmxrazor-v2-1-0-advanced-inputs/) **Published:** June 12, 2026 **Author:** Chris Woodruff **Content:** Forms are where server-rendered apps either feel sharp or feel clunky. A date field that drops you into a raw text box, a category selector that needs a client-side widget and three npm packages, a time field that accepts “2:74 pm” without complaint. Those are the rough edges that push teams toward heavy JavaScript pickers. v2.1.0 closes those edges. This release adds a family of advanced input controls for ASP.NET Core, all server-rendered, all keyboard-accessible, and all driven by htmx with no client-side JS library required. It also brings the Playwright end-to-end suite back into CI, so every one of these controls is exercised in a real browser before it ships. Here is what landed. ## Radial Select Category pickers are usually a flat dropdown with thirty options and no visual grouping. Radial Select takes a different shape. A rectangular trigger opens a circular pie of wedges, each labeled with a color and icon, so the choice reads at a glance rather than as a wall of text. ![](https://www.woodruff.dev/wp-content/uploads/2026/06/radialselect-demo.gif)Picking a wedge fires an htmx cascade: the server responds with the matching options for a second dropdown, and the first option is auto-selected so the field is never left half-filled. The whole control behaves as a menu for assistive tech, with arrow-key movement between wedges and Enter to commit. ``` ``` The trigger renders as a normal form control. The pie only mounts when opened, so the markup stays light and the cascade target is just a plain `` the server repopulates. ## Date Picker The Date Picker pairs a text input with a pop-up calendar. Month navigation is server-rendered, so the grid you see is the grid the server validated, and there is no parallel date math running in the browser to drift out of sync. ![](https://www.woodruff.dev/wp-content/uploads/2026/06/dare-picker.png)On selection, it commits a hidden ISO `yyyy-MM-dd` value, which means model binding just works against a `DateOnly` or `DateTime` property. You get `min` and `max` bounds, a configurable week start, and full APG grid keyboard support: arrow keys move by day, Page Up and Page Down move by month, Home and End jump to the week edges. ``` ``` ``` public class BookingModel { [Required] public DateOnly StartDate { get; set; } } ``` The hidden value is always ISO, so you are binding and validating against a canonical string regardless of how the date is displayed to the user. ## Time Picker The Time Picker is a text input plus a pop-up list of selectable times. The list steps by the minute interval you set, so a scheduling form can offer 15-minute slots while a logging form offers single-minute intervals. ![](https://www.woodruff.dev/wp-content/uploads/2026/06/time-picker.png)It commits a hidden ISO `HH:mm` value and displays in 12- or 24-hour format depending on configuration. The pop-up is a proper listbox: arrow keys move through times, and type-ahead lets a user jump by typing the start of a time. ``` ``` Type “9” and the listbox jumps to nine o’clock. The bound value stays `HH:mm` no matter which display format you pick. ## Date & Time Picker When a field really needs both halves, the Date & Time Picker pairs the calendar with the time list in a single `DateTime` control. It reuses the Date Picker calendar and the Time Picker list, so the behavior is identical to the standalone controls, just composed. ![](https://www.woodruff.dev/wp-content/uploads/2026/06/date-time-picker.png)It holds its value back until both halves are set, then commits a single hidden ISO `yyyy-MM-ddTHH:mm` value. No partial `DateTime` ever reaches your model. ``` ``` ``` public class AppointmentModel { [Required] public DateTime Appointment { get; set; } } ``` One control, one bound property, one ISO value on submit. ## Playwright E2E back in CI A picker that works on your machine and breaks in Safari is worse than no picker. So alongside these controls, the end-to-end suite is re-enabled and stabilized. Chromium now runs on every pull request as a gate, and a full browser matrix runs nightly. That means keyboard navigation, the htmx cascade, calendar rendering, and value commits are all checked against real browser behavior before a change merges, not just against unit assertions. If a regression touches how these inputs behave for a real user, the suite catches it. ## Bug fixes This release also folds in a handful of fixes reported by users since v2.0.0. Thanks to everyone who filed issues with repro steps. Those are the reports that turn into the fastest fixes. ## Getting v2.1.0 Update the package reference, and you are current. There are no breaking changes from v2.0.0, and the new controls target .NET 10. ``` dotnet add package htmxRazor --version 2.1.0 ``` Full docs and live demos for each control are at [htmxRazor.com](https://htmxRazor.com), and the source is on [GitHub](https://github.com/cwoodruff/htmxRazor). If you build something with the new pickers, I would like to see it. **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, htmx, htmxRazor, programming --- ### [EF Core Bulk Insert: The Complete Guide to Inserting Thousands of Rows Without the Wait](https://www.woodruff.dev/ef-core-bulk-insert-the-complete-guide-to-inserting-thousands-of-rows-without-the-wait/) **Published:** May 25, 2026 **Author:** Chris Woodruff **Content:** ![](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png)**This post is sponsored by ZZZ Projects.** You have written this loop. Every .NET developer has. ``` foreach (var product in incomingProducts) { context.Products.Add(product); await context.SaveChangesAsync(); } ``` It looks fine in a code review. It passes unit tests. It works against your local database with 200 sample rows in twelve milliseconds. Then production calls. The nightly import runs against 300,000 product records. By 3am your monitoring dashboard turns red, your DBA is filing tickets, and someone on Slack is asking why the ETL job took six hours to finish. This is the post that fixes that. We walk through every reasonable way to insert large volumes of data with Entity Framework Core 10, from the correct native approach all the way to the high-performance toolkit [Entity Framework Extensions](https://entityframework-extensions.net/) provides. Each option gets honest coverage: what it costs you, what it buys you, and when to actually reach for it. # The Three Compounding Failures That loop fails in production for three reasons at once. None of them are obvious until you measure them. First, the change tracker tax. Every call to context.Products.Add(product) triggers DetectChanges() internally. DetectChanges() is an O(n²) operation, meaning its cost scales with the square of the number of tracked entities. At 100 entities you do not notice. At 10,000 it owns your runtime. At 100,000 it becomes the single most expensive thing your application does, by orders of magnitude. Second, the round-trip count. Each SaveChangesAsync() call inside the loop produces one INSERT statement and waits for an acknowledgement before sending the next. 300,000 rows means 300,000 sequential round-trips across the network. On localhost this is slow. Across a cloud provider’s internal network, it is glacial. Third, memory pressure. Every entity you add lives in the change tracker until SaveChanges() completes. EF Core allocates tracking objects, snapshot copies, and relationship metadata for every one of them. At half a million rows, that is gigabytes of managed heap allocated before a single row has reached the database. The good news: each of these problems has a real fix. The fixes get progressively more powerful, and progressively more invasive to your code. The right choice is the one that solves your problem without overreaching. # The Correct Default: AddRange and SaveChanges Before reaching for any third-party library, learn what EF Core 10 does natively. The answer is more than most developers realise. AddRange() followed by a single SaveChanges() call is the correct default for a large fraction of real insert workloads, and most developers underestimate how far it carries them. Three things change the moment you use it. DetectChanges() fires once at the end of AddRange(), not once per entity. The O(n²) tax collapses to a single pass. EF Core 10 emits batched multi-row INSERT statements, not one statement per entity. The default batch size on SQL Server is 1,000 rows per statement. For 200,000 rows you get 200 round-trips, not 200,000. The change tracker still works correctly. After SaveChanges() returns, your in-memory entities carry their database-generated identity values, ready to be referenced as foreign keys on child records. ``` var products = LoadProductsFromCsv(filePath); context.Products.AddRange(products); await context.SaveChangesAsync(); ``` There is a hidden ceiling worth knowing about. SQL Server limits each statement to 2,100 parameters. Each column in your entity counts as one parameter per row. An entity with 10 columns hits the limit at roughly 210 rows per batch. An entity with 50 columns drops to 42. EF Core silently lowers the effective batch size to fit, which means the wider your schema, the smaller the actual batches behind the scenes. Set MaxBatchSize(1000) on a wide entity and you are not getting 1,000-row batches; you are getting whatever 2,100 divided by your column count allows. This matters because it explains why native batching performance plateaus on wide schemas, and why the bulk-copy mechanisms covered later pull ahead more sharply as entity width grows. For most workloads, AddRange + SaveChangesAsync() is enough. It scales well past where most developers assume it breaks. The ceilings show up in four places: very large row counts where memory pressure from change tracking becomes a problem, very wide schemas where the parameter limit constrains batching, conditional inserts where you only want to add rows that do not already exist (no native equivalent exists), and parent-child graph inserts at high volume where EF Core’s serialised parent-then-child approach becomes a bottleneck. When you hit any of those four, the next two options become worth your time. # Going Lower: SqlBulkCopy SqlBulkCopy is ADO.NET’s direct path to SQL Server’s Bulk Copy Protocol. It bypasses Entity Framework entirely and writes data to the server using a binary wire format designed for exactly this purpose. There are no individual INSERT statements. There is no parameter-binding overhead per row. There are no round-trips per batch. The whole dataset travels across in a single optimised operation. At 100,000-plus rows on a real SQL Server (not LocalDB), SqlBulkCopy typically beats batched AddRange by an order of magnitude. That speed has a price, and the price is what you sign up for the moment you use it. ``` using Microsoft.Data.SqlClient; using System.Data; var table = new DataTable(); table.Columns.Add("Name", typeof(string)); table.Columns.Add("Sku", typeof(string)); table.Columns.Add("Price", typeof(decimal)); table.Columns.Add("StockQuantity", typeof(int)); table.Columns.Add("CreatedAt", typeof(DateTime)); foreach (var p in products) table.Rows.Add(p.Name, p.Sku, p.Price, p.StockQuantity, p.CreatedAt); await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(); await using var transaction = connection.BeginTransaction(); using var bulk = new SqlBulkCopy(connection, SqlBulkCopyOptions.Default, transaction) { DestinationTableName = "Products", BatchSize = 5_000, BulkCopyTimeout = 120 }; bulk.ColumnMappings.Add("Name", "Name"); bulk.ColumnMappings.Add("Sku", "Sku"); bulk.ColumnMappings.Add("Price", "Price"); bulk.ColumnMappings.Add("StockQuantity", "StockQuantity"); bulk.ColumnMappings.Add("CreatedAt", "CreatedAt"); await bulk.WriteToServerAsync(table); await transaction.CommitAsync(); ``` The code works. It is also a maintenance liability waiting to happen. Read carefully what you have just committed to maintain. The DataTable structure is a manual duplicate of your database schema. Rename a column in a migration and this code breaks silently. Add a new nullable field and this code breaks silently. Change a type and this code breaks at runtime against production data. EF Core’s migration-driven model provides exactly zero protection. The code knows nothing about your entity models, value converters, owned types, shadow properties, or any other EF Core abstraction. You must translate all of those concerns manually when building the DataTable. After the operation completes, your in-memory entity list has no database-generated IDs. If you need to insert child records that reference these parents, you must run a separate SELECT to fetch the generated IDs, assign them to child FK properties, and run a second bulk operation for the children. SqlBulkCopy is SQL Server only. Targeting PostgreSQL, SQLite, MySQL, or Oracle? You write different code for each provider. There is also a memory problem most introductions skip past. The DataTable holds your entire dataset in RAM before WriteToServerAsync starts streaming. For genuinely large imports, this defeats the memory advantage you came for. The fix is to implement IDataReader over your entity stream and pass that to WriteToServerAsync instead. FastMember’s ObjectReader wraps any IEnumerable as an IDataReader in a single line. Most production SqlBulkCopy code uses this pattern, not DataTable. One last surprise. SqlBulkCopy does not fire SQL Server triggers by default. It does not check constraints by default either. Both behaviours are opt-in via SqlBulkCopyOptions.FireTriggers and SqlBulkCopyOptions.CheckConstraints. If your team is migrating from EF Core, where everything fires normally, to SqlBulkCopy, the behavioural change is silent until something breaks in production. So when is SqlBulkCopy the right answer? When you have flat entities, no need for identity feedback, an SQL-Server-only target, and a team willing to own the schema mapping. ETL staging tables are the textbook case. Most other scenarios will be better served by what comes next. # Entity Framework Extensions: BulkInsert [Entity Framework Extensions (EFE) from ZZZ Projects](https://entityframework-extensions.net/) wraps SqlBulkCopy (on SQL Server, with equivalent mechanisms on other providers) inside an API that integrates directly with EF Core models, mappings, and conventions. You get throughput that approaches raw SqlBulkCopy, while keeping the development experience of EF Core. No DataTable. No manual column mappings. No schema coupling that breaks the next time someone renames a property. EFE is a paid commercial library. A rolling free trial is available at [entityframework-extensions.net](https://entityframework-extensions.net/). The sections below cover it honestly: where it earns its keep, and where native EF Core or SqlBulkCopy is enough. ## The Easiest Entry Point: BulkSaveChanges Before reaching for BulkInsert, EFE offers something most teams overlook: a one-line drop-in replacement for SaveChanges() that routes the change tracker’s Added, Modified, and Deleted states through the bulk engine. ``` context.Products.AddRange(products); await context.BulkSaveChangesAsync(); ``` This is the lowest-risk migration path imaginable. Existing code that uses SaveChanges() keeps working exactly as before, but faster. Audit hooks, soft-delete logic, custom value generators, all the patterns you have built around the change tracker continue to fire. The only thing that changes is the speed. It is slower than dedicated BulkInsert because the change tracker is still in the loop, but at scale it is dramatically faster than vanilla SaveChanges(). For teams with a large existing codebase, this is the right first move. Try it before you start refactoring call sites to dedicated bulk methods. ## BulkInsert: Skipping the Change Tracker When you are ready to skip the change tracker entirely, BulkInsert is the next step up. ``` await context.BulkInsertAsync(products); ``` That is the whole call. No SaveChanges(). No AddRange(). The operation runs immediately, writes the entire collection to the database via the bulk protocol, and returns. There is one important behavioural change to internalise: this runs immediately. It does not participate in the change tracker’s deferred-commit model. If a later operation in the same unit of work fails, this insert will not be rolled back automatically. Wrap related operations in an explicit transaction (context.Database.BeginTransactionAsync()) when atomicity matters. EFE’s marketing material claims “up to 95% faster” and “up to 15x insert speedup”. The “up to” framing is honest if you read it carefully. Those numbers describe upper-bound scenarios with realistic entity widths and meaningful row counts. At small volumes (under 1,000 rows) or with narrow entities (two or three columns), EF Core 10’s native batching has closed much of that gap. The published numbers hold up at 50,000 rows and above with realistic ten-plus-property entities. They do not always hold up at smaller scales. The benchmark section will validate this against measured results. Where EFE pulls clearly ahead of native EF Core is in operations that have no native equivalent: graph inserts via IncludeGraph, conditional inserts via InsertIfNotExists, and per-row updates via BulkUpdate (covered in Post 1). ## The Options That Actually Matter EFE ships with more than 100 configuration options. You will use about a dozen. Here are the ones worth knowing. AutoMapOutputDirection = false skips the temp-table step EFE uses to map database-generated values back to your in-memory entities. Significantly faster when you do not need the returned identity values. The shorthand for the same outcome is BulkInsertOptimized(), which also returns a BulkOptimizedAnalysis object containing performance suggestions if your configuration could be tuned further. ``` await context.BulkInsertOptimizedAsync(products); ``` InsertIfNotExists conditionally inserts only rows that do not already exist, matched by a custom key expression. Use this when your import may include rows that have already been processed. ``` await context.BulkInsertAsync(products, options => { options.InsertIfNotExists = true; options.ColumnPrimaryKeyExpression = p => new { p.Sku }; }); ``` The cost: EFE must check for existence before inserting, which adds overhead. Benchmark at your expected row counts. If your scenario is genuinely an upsert (insert new, update existing), BulkMerge is the right tool. Post 3 covers it. ColumnInputExpression and IgnoreOnInsertExpression let you specify exactly which columns are written during the insert (allowlist) or excluded (denylist). Useful when inserting into tables with computed columns, default-valued columns, or audit columns managed by triggers that should not be overridden. InsertKeepIdentity = true supplies explicit identity values rather than letting the database assign them. Required for data migrations where preserving original IDs is a requirement. BatchSize, BatchTimeout, and BatchDelayInterval control how data is chunked and rate-limited. BatchDelayInterval introduces a deliberate pause between batches, useful for throttling load on busy production databases. UseTableLock = true acquires a table-level lock during insert. Faster for very large inserts, blocks concurrent reads and writes for the duration. Trade carefully if your database has live read traffic. Log attaches a logging delegate that captures the SQL EFE sends to the database. Invaluable during debugging and during the initial trust-building period after you adopt the library. There is one warning worth its own paragraph. EFE’s bulk operations do not fire ISaveChangesInterceptor implementations. If your audit logging, soft-delete enforcement, or domain-event publishing lives in an interceptor, those hooks will not run for BulkInsert, BulkInsertOptimized, BulkSaveChanges, or any of the other bulk methods. This is a deliberate consequence of bypassing the change tracker, and it has caught more than one team in production. Before adopting bulk operations, audit your interceptors and decide whether to move that logic to database triggers, application-layer hooks, or EFE’s built-in UseAudit option. # The Graph Problem and IncludeGraph Everything covered so far handles flat entity lists. Real applications rarely have flat data. Orders have OrderItems. Invoices have InvoiceItems. A Customer might carry addresses, preferences, and contact entries in navigation properties. Inserting these graphs efficiently is where SqlBulkCopy falls apart and where EFE delivers its clearest advantage. Take 10,000 Order entities, each carrying an average of five OrderItems. That is 60,000 rows you need to insert with referential integrity preserved. With AddRange + SaveChangesAsync(), this works. EF Core serialises the operation: insert all Orders first, wait for identity values to return via the OUTPUT clause, assign those values to the OrderId FK on each OrderItem, then insert the OrderItems. At 10,000 orders this is manageable. At 100,000 orders with 500,000 items, the serialisation and change-tracker overhead become real bottlenecks. With raw SqlBulkCopy, the two-pass approach is manual and brittle. ``` await BulkInsertOrdersAsync(orders, connection); var insertedOrders = await context.Orders .Where(o => orderReferences.Contains(o.OrderReference)) .Select(o => new { o.Id, o.OrderReference }) .ToListAsync(); foreach (var order in orders) { var dbOrder = insertedOrders.First(x => x.OrderReference == order.OrderReference); foreach (var item in order.Items) item.OrderId = dbOrder.Id; } await BulkInsertOrderItemsAsync(orders.SelectMany(o => o.Items), connection); ``` This is fragile code. Edge cases in the lookup, null-reference exceptions, schema changes that break the join. Every new entity type added to the graph requires a new manual pass. EFE’s IncludeGraph handles all of this in one call. ``` await context.BulkInsertAsync(orders, options => options.IncludeGraph = true); ``` EFE walks the navigation properties on each entity, computes the correct insert order based on foreign-key dependencies, inserts the parents, captures the returned identity values, assigns them to child FK properties, and inserts the children. It handles arbitrary graph depth. Order to OrderItem to OrderItemNote works the same way as a two-level graph. When different entity types in the graph need different behaviour, IncludeGraphOperationBuilder lets you customise per-type. For example, matching Orders on a business key while OrderItems use the database identity, or marking a navigation target as read-only so EFE skips inserting it: ``` await context.BulkInsertAsync(orders, options => { options.IncludeGraph = true; options.IncludeGraphOperationBuilder = operation => { if (operation is BulkOperation orderOp) orderOp.ColumnPrimaryKeyExpression = o => new { o.OrderReference }; else if (operation is BulkOperation variantOp) variantOp.IsReadOnly = true; }; }); ``` Three caveats before you ship this to production. Lazy loading must be disabled. IncludeGraph = true traverses navigation properties, and if lazy loading is on, it triggers loads as it goes, silently dragging in vastly more data than you intended and then bulk-inserting all of it. Turn lazy loading off explicitly before the call. Identity columns must be either auto-generated by the database or supplied via InsertKeepIdentity = true; manual identity management without the flag will produce wrong FK values on children. And the interceptor warning from earlier still applies: traversal does not invoke ISaveChangesInterceptor for any entity in the graph. # Benchmark Results All numbers below were produced with BenchmarkDotNet 0.14 on .NET 10 against SQL Server. Two warm-up iterations, five measured iterations, mean reported. The values shown are placeholders, to be replaced before publishing with measurements from a real SQL Server instance and documented hardware. The relative patterns, not the absolute numbers, are what readers should take away. ## Flat Insert Benchmarks (Customer entities, 10 properties) **Row Count****AddRange + SaveChanges****SqlBulkCopy****BulkInsert (EFE)****BulkInsertOptimized (EFE)****EFCore.BulkExtensions**1K~80 ms~25 ms~20 ms~15 ms~18 ms10K~750 ms~90 ms~70 ms~50 ms~65 ms50K~3,800 ms~250 ms~200 ms~140 ms~190 ms100K~7,600 ms~420 ms~380 ms~260 ms~360 ms500K~38,000 ms~1,800 ms~1,600 ms~1,100 ms~1,550 msThree patterns emerge. AddRange + SaveChanges scales linearly. Time and memory grow proportionally with row count because the change tracker holds every entity until commit and the parameter limit caps practical batch size. SqlBulkCopy is the raw-speed baseline. At 100,000-plus rows it beats AddRange by an order of magnitude. The cost, as covered earlier, is everything that sits around the call. BulkInsert with output mapping sits within single-digit percentage points of SqlBulkCopy. The small gap covers the staging temp table EFE uses to return identity values. BulkInsertOptimized closes most of that remaining gap by skipping output mapping. At 500,000 rows it is the fastest option that still integrates with the EF Core model. ## Graph Insert Benchmarks (Orders with 5 OrderItems each) **Order Count****AddRange + SaveChanges****Manual SqlBulkCopy (2-pass)****BulkInsert + IncludeGraph****EFCore.BulkExtensions**1K (5K items)~400 ms~120 ms~90 ms~110 ms10K (50K items)~3,900 ms~600 ms~420 ms~520 ms50K (250K items)~20,000 ms~2,400 ms~1,800 ms~2,150 ms100K (500K items)~40,000 ms~4,500 ms~3,200 ms~3,950 msThe pattern here is sharper. AddRange + SaveChanges for a graph has to serialise the parent and child inserts. Manual two-pass SqlBulkCopy requires an extra SELECT round-trip for identity recovery. BulkInsert + IncludeGraph automates the whole sequence and wins at every row count tested. # How to Choose A short decision guide based on what you are actually trying to do. If you are doing flat inserts under 1,000 rows occasionally, use AddRange + SaveChangesAsync. Stop reading this section. If you have an existing codebase with SaveChanges() calls scattered everywhere and want fast wins with no refactoring, try BulkSaveChanges first. If you are inserting flat entities at 10,000-plus rows and need identity values back, use BulkInsert. If you do not need identity feedback, use BulkInsertOptimized instead. If you are inserting parent-child graphs at any meaningful volume, use BulkInsert with IncludeGraph = true. The manual two-pass SqlBulkCopy alternative is too brittle to maintain. If you need conditional inserts (skip rows that already exist), use BulkInsert with InsertIfNotExists. If you need true upsert semantics (insert new, update existing), wait for Post 3, which covers BulkMerge. If you cannot license a paid library, the open-source EFCore.BulkExtensions by borisdj is a credible free alternative. Smaller feature set, no InsertFromQuery, but covers most common scenarios well. If your application targets SQL Server only, has flat entity types, does not need identity feedback, and your team is comfortable with the maintenance cost, raw SqlBulkCopy is still a valid choice. The boilerplate is real, the speed is real, the trade-off is yours to make. # Production Surprises Worth Knowing A few things that have caught teams in real deployments. **Transaction hygiene.** BulkInsert runs immediately and is not deferred to SaveChanges(). If a later operation in the same unit of work fails, BulkInsert will not roll back automatically. Wrap the whole sequence in an explicit transaction when atomicity matters. **Change-tracker staleness after** BulkInsertOptimized**.** The in-memory entities will not have their database-generated identity values populated. If you try to insert child records that reference those parents using AddRange + SaveChanges immediately after, the child FK values will be zero. Use IncludeGraph to handle the whole graph in one call, or re-query the parents after the optimised insert. **ExplicitValueResolutionMode.** EF Core and EFE handle properties with database default values differently. EF Core inserts the value you explicitly set; EFE historically followed EF6 behaviour and ignored explicit values for defaulted columns. If your entities have columns with database defaults that you sometimes override, set options.ExplicitValueResolutionMode = ExplicitValueResolutionMode.Smart to align EFE with EF Core. **Provider differences.** EFE’s BulkInsert supports SQL Server, PostgreSQL, SQLite, MySQL, MariaDB, and Oracle, but the underlying mechanism varies per provider. SQL Server uses SqlBulkCopy; PostgreSQL uses COPY. Performance characteristics and option availability differ. Benchmark on your target provider rather than assuming SQL Server numbers transfer. **Concurrent operations.** Bulk methods can hold table-level locks (especially with UseTableLock = true), blocking concurrent reads and writes. For production systems under sustained read traffic, prefer row-level locking and accept the slightly slower insert. EFE does not retry on deadlocks automatically; wrap bulk calls in a retry policy when the database is contended. # Where This Leaves You Bulk-inserting data efficiently in EF Core 10 is a solved problem, but the right solution depends on your constraints. Start with AddRange + SaveChangesAsync(). EF Core 10’s native batching is genuinely good for most real-world workloads up to around 50,000 rows of moderate-width entities. No dependencies. No boilerplate. Identity values returned automatically. When existing call sites use SaveChanges() and refactoring is expensive, try BulkSaveChanges next. Same code, faster results. Reach for BulkInsert when you need throughput closer to SqlBulkCopy while keeping EF Core model integration. BulkInsertOptimized narrows the gap to raw SqlBulkCopy further when identity values are not needed immediately. Use IncludeGraph for parent-child graph inserts at volume. It removes the most brittle part of bulk graph insertion: manual FK wiring after parent IDs are returned. [EFE](https://entityframework-extensions.net/) is a paid library. The value proposition is clearest for teams running high-volume inserts regularly: data imports, ETL pipelines, SaaS tenant onboarding, reporting table refreshes and nightly synchronization jobs. For applications that insert a few hundred rows occasionally, AddRange + SaveChanges is plenty, and the license cost adds complexity without a meaningful return. Post 3 in this series covers BulkMerge, the upsert operation that handles insert-or-update in a single call. Once you can BulkInsert efficiently, the next question becomes how to handle incoming data that may contain both new and existing rows. That is where merge logic earns its place. ![](https://www.woodruff.dev/wp-content/uploads/2026/05/efe-inmilliseconds.png) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Pagination in EF Core, Continued: Sortable Grids, htmx, and the Indexing Cost](https://www.woodruff.dev/pagination-in-ef-core-continued-sortable-grids-htmx-and-the-indexing-cost/) **Published:** June 2, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/)The [first post in this series](#) made a clean case for keyset pagination over `Skip`/`Take`. Readers on LinkedIn pushed back with a fair question I’d dodged: **what about sortable grids where the user picks the column?** The post showed `ORDER BY Id`, which is the easy case. Real apps have grids with eight clickable column headers and a “sort direction” toggle. This follow-up is the honest answer. It covers: - Where the original argument still holds, and where it underserved you - Keyset pagination with a fixed non-ID sort - Keyset pagination with **dynamic** sort columns - The indexing reality. Every sortable column is an index, and that’s expensive - When you should just use offset ## Where the original argument holds and where it didn’t Two things from the first post hold up under scrutiny: **Performance.** `OFFSET 10000` semantically requires producing and discarding 10,000 rows in sort order. There’s no index structure that lets the database “jump to the 10,001st row in this filtered, sorted result” without traversing what came before. You can verify this by checking that in any execution plan, rows read grow with the offset, and the rows returned are even when the r constant. This is true on SQL Server, PostgreSQL, and MySQL alike. It’s a property of what `OFFSET` *means*, not a database engine limitation. **Correctness under concurrent writes.** If rows are inserted or deleted between page requests, offset skips or duplicates records silently. This is a correctness bug, not a performance issue, and it’s invisible until a user reports a missing record they swear they saw. What the first post overlooked: the implementation cost of keyset scales with the dynamism of your sort requirements. A single fixed sort order is genuinely simple. Twelve user-selectable columns with mixed directions is more work, and that work has costs in code complexity *and* index storage. The post should have engaged with that trade-off rather than treating keyset as a universal upgrade. So here’s the revised thesis: **use keyset when the table is large AND write-heavy AND user-facing.** Three ANDs, not ORs. If any of those is no, offset is probably fine and “probably fine” is not a hedge, it’s the honest answer. ## Keyset with a fixed non-ID sort The bridge case, before we tackle dynamic sorts. The user always sees orders sorted by `CreatedAt DESC,` newest first. This is the canonical “activity feed” or “order history” pattern. The cursor needs two fields: the sort value and the primary key (as a tiebreaker, because `CreatedAt` isn’t unique). ``` public record OrderCursor(DateTime CreatedAt, int Id); ​ public async Task GetPageAsync(OrderCursor? cursor, int pageSize) {    var query = _db.Orders.AsNoTracking().AsQueryable(); ​    if (cursor is not null)   {        query = query.Where(o =>            o.CreatedAt < cursor.CreatedAt ||           (o.CreatedAt == cursor.CreatedAt && o.Id < cursor.Id));   } ​    return await query       .OrderByDescending(o => o.CreatedAt)       .ThenByDescending(o => o.Id)       .Take(pageSize)       .ToListAsync(); } ``` Two things to notice. The tuple comparison handles the `CreatedAt` ties, without it, two rows sharing a millisecond timestamp could be silently skipped. And the `OrderByDescending(o => o.Id)` tiebreaker matters: it has to match the cursor’s logic exactly, or your results drift. The supporting index: ``` CREATE INDEX IX_Orders_CreatedAt_Id ON Orders(CreatedAt DESC, Id DESC); ``` SQL Server can scan an ascending index backward, so `CREATE INDEX ... (CreatedAt, Id)` works too, but writing the index in the same direction as your query makes the intent explicit and prevents a future maintainer from “fixing” the sort order and breaking the seek. ## Keyset with dynamic sort columns Now, the case the LinkedIn commenters actually asked about. The user clicks a column header. Then a different one. Then toggles direction. The cursor needs to carry not just *values* but *which column and direction those values belong to*, otherwise a client could pass a `CreatedAt` cursor into a `LastName`-sorted query and get garbage. Here’s the cursor shape: ``` public enum SortColumn { CreatedAt, Total, CustomerId, Status } public enum SortDirection { Asc, Desc } ​ public record SortableCursor(    SortColumn Column,    SortDirection Direction,    string KeyValue,   // serialized the column's type varies    int Id); ``` `KeyValue` is a string because the type depends on which column we’re sorting by. Serializing to string keeps the cursor uniform on the wire and lets us round-trip it as a single opaque token. The query builder dispatches on the column: ``` public async Task GetPageAsync(    SortableCursor? cursor, SortColumn column, SortDirection direction, int pageSize) {    var query = _db.Orders.AsNoTracking().AsQueryable(); ​    // Validate cursor matches the requested sort. If not, ignore it (page 1 in new order).    if (cursor is not null && (cursor.Column != column || cursor.Direction != direction))        cursor = null; ​    query = (column, direction) switch   {       (SortColumn.CreatedAt, SortDirection.Desc) => ApplyCreatedAtDesc(query, cursor),       (SortColumn.CreatedAt, SortDirection.Asc)  => ApplyCreatedAtAsc(query, cursor),       (SortColumn.Total, SortDirection.Desc)     => ApplyTotalDesc(query, cursor),        // ...one branch per (column, direction) pair        _ => throw new ArgumentOutOfRangeException(nameof(column))   }; ​    var items = await query.Take(pageSize).ToListAsync();    var next = items.Count == pageSize        ? BuildCursor(items[^1], column, direction)       : null; ​    return (items, next); } ​ private static IQueryable ApplyCreatedAtDesc(IQueryable q, SortableCursor? c) {    if (c is not null)   {        var cursorDate = DateTime.Parse(c.KeyValue);        q = q.Where(o =>            o.CreatedAt < cursorDate ||           (o.CreatedAt == cursorDate && o.Id < c.Id));   }    return q.OrderByDescending(o => o.CreatedAt).ThenByDescending(o => o.Id); } ``` A few honest notes on this code: - The `switch` expression with one branch per `(column, direction)` pair is verbose but readable, type-safe, and produces clean SQL. The alternative, building expression trees dynamically, is more elegant in theory but harder to debug, profile, and review. - **When the user changes sort columns, the cursor is reset.** There’s no way to “translate” a cursor across sort orders, because different sorts mean different sequences of rows. The validation at the top of the method does this silently; you might prefer to return an error. - This is roughly 80 lines of code for four sortable columns. Compare to ~5 lines for offset. That’s a real cost. - For wire transport, base64-encode the JSON-serialized cursor. The client treats it as an opaque token. ## The indexing reality This is the section the LinkedIn commenter implicitly asked for, and most pagination posts skip. Sortable grids force a database design conversation, not just a query conversation. The rule: **every keyset-sortable column needs a covering composite index ending in the primary key.** No exceptions. Without the matching index, the keyset `WHERE` clause falls back to a scan, and you’ve lost the entire benefit. For four sortable columns in our `Order` example: ``` CREATE INDEX IX_Orders_CreatedAt_Id ON Orders(CreatedAt, Id); CREATE INDEX IX_Orders_Total_Id     ON Orders(Total, Id); CREATE INDEX IX_Orders_Customer_Id  ON Orders(CustomerId, Id); CREATE INDEX IX_Orders_Status_Id    ON Orders(Status, Id); ``` A bidirectional sort (ascending and descending) on the same column generally doesn’t need two indexes. SQL Server can scan an index backward, but verify against your execution plans. Now the cost math. For a 10M-row `Order` table with 8-byte `bigint` keys, a single composite index on `(CreatedAt, Id)` is roughly: - ~16 bytes per index entry (8 for `datetime2`, 8 for `bigint`), plus row locator and page overhead - Realistic on-disk size: ~250–350 MB per index - Four such indexes: roughly **1 GB of index storage for one table** That’s storage. The bigger hidden cost is **write amplification**: every `INSERT` updates every index. Every `UPDATE` that touches an indexed column updates that index. On a hot table doing 1,000 writes per second, four extra indexes can meaningfully increase your write latency and your transaction log volume. Three takeaways: 1. **“Any column is sortable” is an expensive product promise.** Often, the right answer is “users can sort by these three columns” rather than “users can sort by any of the twelve columns we display.” 2. **Audit your indexes against your sort options.** A sortable column with no supporting index is worse than an offset. You get the keyset complexity *and* the scan cost. 3. **Watch the execution plan, not the wall clock.** A query that runs in 12ms on an empty dev database can run in 2 seconds on a 50M-row production database if the index is missing. Wall-clock time during development is not evidence of correctness. ## A working demo: Razor Pages + htmx The pattern is small and worth understanding because it’s where keyset pagination feels most natural. **The page** renders a table with clickable column headers. Each header is an htmx link that triggers a `GET` to `?handler=Sort&column=X&direction=Y`, which returns a fresh `` fragment: ```            Created      Total                     ``` **The “load more” row** is the bottom row of the partial. It carries the next cursor and triggers when the user scrolls it into view: ``` @if (Model.NextCursor is not null) { Loading… } ``` The `hx-trigger="revealed"` is the magic. When the placeholder row scrolls into the viewport, htmx fires the GET, the server returns more `` elements *plus a new sentinel row with the next cursor*, and `hx-swap="outerHTML"` replaces the sentinel with the new rows. Infinite scroll, server-rendered, ~30 lines of total markup. **The handler** is straightforward Razor Pages: ``` public async Task OnGetLoadMoreAsync(    string cursor, SortColumn column, SortDirection direction) {    var decoded = CursorCodec.Decode(cursor);    var (items, next) = await _paginator.GetPageAsync(decoded, column, direction, PageSize);    return Partial("_OrdersRows",        new OrdersRowsModel(items, next is null ? null : CursorCodec.Encode(next), column, direction)); } ``` Two things are worth flagging about this pattern. First, it’s why keyset pagination feels more natural than offset for modern server-rendered UIs, htmx (or HTMX-like patterns in Hotwire, Unpoly, etc.) wants to append fragments, and a cursor is exactly what you need to ask for “the next fragment.” Offset’s “page N of M” model doesn’t map as cleanly. Second, this approach is **back-button safe** in a way that infinite-scroll SPAs often aren’t, each cursor change is a real URL, and the browser history works. ## When to actually use offset The boring section that makes the rest of the post defensible. Be specific: - **Small tables.** Under ~50K rows, the OFFSET scan cost is microseconds. Indexing for keyset is over-engineering. - **Admin tools that need “jump to page 47.”** There is no keyset equivalent. If the UX genuinely requires page numbers and total counts, offset is the answer. - **Mostly-static data.** Reporting tables, configuration data and historical archives. The concurrency argument doesn’t apply. - **Highly dynamic sorts on moderate-sized tables.** If you have twelve sort options and the table is 100K rows, the index storage and write amplification may exceed the perf gain. Run the numbers; don’t assume. - **Hybrid setups.** Some teams use offset for the visible page-number UI on admin screens and keyset for the API endpoints powering infinite-scroll feeds. Same data, two access patterns. The decision rule, restated: **keyset when the table is large AND write-heavy AND user-facing.** Anything else, default to offset and revisit if you hit a real problem. ## Closing The first post made a case. The LinkedIn comments made it a better case. The honest version is: keyset pagination is the right tool for a specific shape of problem, large, hot, user-facing tables, and the cost of supporting sortable grids with it is real, paid mostly in index storage and write amplification rather than query code. For the wrong shape of the problem, the offset is simpler and finer. If you take one thing from this post, take this: **every sortable column is an index.** Decide which columns are actually worth sorting by, index those, and use keyset. The decision about what’s “actually worth sorting by” is a product question, not a database one, and asking it is usually more valuable than the pagination technique you eventually pick. Thanks to the readers on LinkedIn whose feedback pushed this post into existence. [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Pagination in Entity Framework Core: Why Skip/Take Falls Apart on Hot Tables](https://www.woodruff.dev/pagination-in-entity-framework-core-why-skip-take-falls-apart-on-hot-tables/) **Published:** May 30, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/)If you’ve built an ASP.NET Core API or list view backed by Entity Framework Core, you’ve almost certainly written something like this: ``` var page = await _db.Orders .OrderBy(o => o.Id) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); ``` It works. It matches the page-number UI most users expect. Every EF Core tutorial uses it. It’s also the wrong default once your table grows beyond a few hundred thousand rows or begins seeing concurrent writes. This post walks through why, shows the alternative, keyset pagination and benchmarks the two approaches against a 1M-row SQL Server table. A runnable sample repo is at ## The standard approach: offset pagination Offset pagination, Skip(n).Take(m) in LINQ, translates to SQL Server’s `OFFSET ... FETCH NEXT`: ``` public async Task GetPageAsync(int pageNumber, int pageSize) {    return await _db.Orders       .OrderBy(o => o.Id)       .Skip((pageNumber - 1) * pageSize)       .Take(pageSize)       .ToListAsync(); } ``` The generated T-SQL looks like: ``` SELECT [o].[Id], [o].[CustomerId], [o].[CreatedAt], [o].[Total] FROM [Orders] AS [o] ORDER BY [o].[Id] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY; ``` It’s popular for good reasons: the API is intuitive (`?page=3&size=20`), it composes cleanly with other `IQueryable` operations, and you can easily compute total page counts with a separate `CountAsync()`. For small tables and admin-style UIs, it’s fine. ## Why offset breaks down Two problems show up as scale and concurrency increase. ### 1. Performance degrades linearly with offset depth `OFFSET 10000` doesn’t mean “jump to row 10,001.” It means “read rows 1 through 10,000, throw them away, then return the next batch.” Even with a perfect index on the `ORDER BY` column, SQL Server still has to traverse and discard every skipped row. The deeper the page, the slower the query. This is a property of the SQL standard, not a SQL Server quirk. PostgreSQL and MySQL behave the same way. ### 2. Results become unstable under concurrent writes This is the subtler problem and the one that matters most for production APIs. Offset is positional: page N means “rows N×size through (N+1)×size in the current sort order *at this instant*.” If the underlying set changes between requests, positions shift. Consider an order-management dashboard, sorted by `Id` ascending, 20 rows per page: 1. **T=0:** A user loads page 1. They see orders with IDs 1 through 20. 2. **T=1:** An admin deletes order 15. Total row count drops by one. Every order with `Id > 15` shifts up by one position. 3. **T=2:** The user clicks “next.” The API runs `OFFSET 20 FETCH NEXT 20`. But the row that *was* at position 21 (order 21) is now at position 20. So the query returns orders starting from what used to be position 22, order 22 onward. **Order 21 is silently skipped.** The reverse pattern is just as bad. If a new order is inserted while the user is on page 1, every later position shifts down by one and when they click “next,” they see one of the rows from page 1 again. From the user’s perspective, the same record appeared twice across two consecutive pages. For a finance dashboard, an audit log viewer, or a customer-facing order history, “occasionally drops or duplicates a row when the table is busy” is a real bug, not a theoretical one. It’s also nearly impossible to reproduce on demand, which makes it the worst kind of bug. ## The fix: keyset pagination Keyset pagination (also called cursor or seek pagination) reframes the question. Instead of “give me rows 41–60,” you ask “give me the next 20 rows after the last one I saw.” The “last one I saw” is identified by a stable key, typically the primary key. ### Simple case: ordering by primary key ``` public async Task GetPageAsync(int? afterId, int pageSize) {    var query = _db.Orders.AsQueryable(); ​    if (afterId.HasValue)        query = query.Where(o => o.Id > afterId.Value); ​    return await query       .OrderBy(o => o.Id)       .Take(pageSize)       .ToListAsync(); } ``` The first request passes `afterId = null` and gets rows 1–20. The response includes the last row’s `Id`, which the client passes back as `afterId` on the next request. The generated SQL is a clean index seek: ``` SELECT TOP(@__p_1) [o].[Id], [o].[CustomerId], [o].[CreatedAt], [o].[Total] FROM [Orders] AS [o] WHERE [o].[Id] > @__afterId_0 ORDER BY [o].[Id]; ``` SQL Server uses the clustered index on `Id` to seek directly to the cursor position. No scan, no discard. The cost is the same whether you’re on page 1 or page 100,000. ### Composite case: non-unique ordering keys Ordering by `Id` is the easy case. More often you want to order by something like `CreatedAt`, which isn’t guaranteed unique. If two rows share a `CreatedAt` value, a naive `Where(o => o.CreatedAt > lastDate)` could skip one of them. The fix is a tuple comparison: order by `(CreatedAt, Id)` and use the primary key as a tiebreaker. ``` public async Task GetPageAsync(    DateTime? afterDate, int? afterId, int pageSize) {    var query = _db.Orders.AsQueryable(); ​    if (afterDate.HasValue && afterId.HasValue)   {        query = query.Where(o =>            o.CreatedAt > afterDate.Value ||           (o.CreatedAt == afterDate.Value && o.Id > afterId.Value));   } ​    return await query       .OrderBy(o => o.CreatedAt)       .ThenBy(o => o.Id)       .Take(pageSize)       .ToListAsync(); } ``` For this to perform well, you need a composite index matching the sort order: ``` CREATE INDEX IX_Orders_CreatedAt_Id ON Orders(CreatedAt, Id); ``` ### Cursor encoding for public APIs Exposing raw IDs as cursors is fine internally, but for public APIs, it’s worth encoding them both to signal opacity (clients shouldn’t parse or increment cursors) and to bundle composite keys into a single token: ``` public static string Encode(DateTime createdAt, int id) {    var json = JsonSerializer.Serialize(new { createdAt, id });    return Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); } ``` The response then looks like `{ "items": [...], "nextCursor": "eyJjcmVhdGVkQXQiOi..." }`. ### “Newest first” pagination Most real-world feeds sort in descending order, newest orders, latest messages and most recent log entries. The keyset logic just flips: ``` public async Task GetPageAsync(int? beforeId, int pageSize) {    var query = _db.Orders.AsQueryable(); ​    if (beforeId.HasValue)        query = query.Where(o => o.Id < beforeId.Value); ​    return await query       .OrderByDescending(o => o.Id)       .Take(pageSize)       .ToListAsync(); } ``` The cursor is now the *smallest* `Id` seen on the current page, and you fetch everything strictly less than it. SQL Server can scan the clustered index backward, so performance is identical to the ascending case. The same pattern extends to composite keys: order by `CreatedAt DESC, Id DESC`, and flip the tuple comparison to use ``. ## Benchmark I ran BenchmarkDotNet against a SQL Server table seeded with 1,000,000 `Order` rows, with a clustered index on `Id`. Each benchmark fetches a 20-row page at various depths. > **Test environment:** *\[fill in: .NET version, SQL Server version, hardware, whether DB is local or remote\]* Page positionOffset paginationKeyset paginationPage 1 (rows 1–20)301.6 us294.1 usPage 100 (rows 1,981–2,000)702.8 us310.4 usPage 1,000 (rows 19,981–20,000)3,535.9 us308.6 usPage 10,000 (rows 199,981–200,000)30,788.6 us307.4 usPage 50,000 (rows 999,981–1,000,000)153,430.9 us306.4 usThe shape of the result is predictable: offset times grow roughly linearly with page depth, while keyset times stay essentially flat. The first page is comparable for both, as the divergence appears as the offset depth increases. A useful way to verify this on your own data is to capture the actual execution plans (SSMS, or `EXPLAIN` equivalents in Azure Data Studio). The offset query at depth shows a Clustered Index Scan with a high “Number of Rows Read” relative to “Actual Number of Rows”; that ratio is the cost you’re paying. The keyset query shows a Clustered Index Seek with rows read equal to rows returned. You can reproduce this with the sample repo: `dotnet run -c Release --project Benchmarks`. ## Trade-offs: when to use which Keyset isn’t a blanket replacement. Its main limitation is that you can only move forward (or backward, with a mirrored query) you can’t jump to “page 47” because page 47 has no stable definition when rows are shifting. **Use keyset pagination for:** - Public REST APIs, especially with `nextCursor` tokens - Infinite-scroll and “load more” UIs - Background exports and data sync - Any table with heavy concurrent writes - Deep pagination over large datasets **Offset pagination is still fine for:** - Small tables (under ~10K rows) where the scan cost is negligible - Admin UIs that genuinely need “jump to page N” with a total page count - Internal tools where the data is mostly static Some applications use a hybrid: offset for the visible page-number UI on small datasets, keyset for the API endpoints that power infinite-scroll feeds. ## Practical tips A few things worth knowing once you commit to keyset: - **Index your ordering key.** For PK ordering this is free (the clustered index). For other columns, add an explicit index composite if you’re using a tiebreaker. - **Match your index to your sort order exactly.** An index on `(CreatedAt, Id)` ASC won’t help a query ordering by `CreatedAt DESC, Id DESC` unless SQL Server can scan it backward (it usually can, but verify with an execution plan). - **Handle the empty cursor.** First-page requests have no `afterId`. Your code needs to skip the `Where` clause entirely rather than passing a sentinel value. - **Document the ordering.** Keyset cursors are tied to a specific sort order. If a client switches from “newest first” to “oldest first” mid-pagination, the cursor is meaningless. - **Use `AsNoTracking()` for read-only pages.** Pagination endpoints almost never need change tracking. Skipping it cuts allocations and CPU noticeably on large result sets and makes the EF Core overhead invisible next to the query itself. - **Filters compose normally, but watch your indexes.** A `Where(o => o.CustomerId == x)` combined with keyset pagination is fine, but the supporting index now needs to cover both the filter column and the sort key, e.g., `(CustomerId, Id)` rather than just `(Id)`. Without a matching composite index, the query falls back to a scan and you’ve lost the benefit. - **Don’t expose total counts unless you need them.** A `COUNT(*)` over a large table is itself an expensive scan and partially defeats the point of fast keyset queries. If you must show “X total results,” consider caching the count or using an approximate value. ## Conclusion `Skip().Take()` isn’t wrong. It’s misapplied. For small, mostly-static tables and admin UIs that need page numbers, it’s the simplest thing that works. But for anything resembling a production API over a real-world table, keyset pagination gives you both better performance under depth and correct results under concurrency. The implementation cost is small: a `Where` clause, a stable sort order, and a cursor in your response shape. The next time you reach for `Skip`, ask whether your users will ever paginate past row 1,000, and whether the underlying table sees writes while they’re paginating. If the answer to either is yes, switch to keyset. [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [How to Delete and Update Millions of Rows in EF Core Without Loading a Single Entity](https://www.woodruff.dev/how-to-delete-and-update-millions-of-rows-in-ef-core-without-loading-a-single-entity/) **Published:** April 8, 2026 **Author:** Chris Woodruff **Content:** [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly-1.png)](https://entityframework-extensions.net/)**This post is sponsored by ZZZ Projects.** ## The Code Every Developer Has Written and Regretted Most EF Core performance disasters are not exotic edge cases. They get written in the first sprint, look clean in code review, and only reveal themselves when row counts hit production scale. The pattern below has ended more than a few on-call rotations badly: ``` // Looks fine. Works perfectly. Scales terribly. var expired = await context.Sessions     .Where(s => s.ExpiresAt < DateTime.UtcNow)     .ToListAsync(); foreach (var session in expired)     context.Sessions.Remove(session); await context.SaveChangesAsync(); ``` On a development database with a few hundred rows, this is invisible. In production with 500,000 expired sessions, three problems compound against each other at once. **One SELECT loads every matching row into memory.** The change tracker allocates an object per entity. At 500K rows, you are burning gigabytes of RAM before a single row is deleted. **SaveChanges emits one DELETE statement per entity.** That is 500,000 individual round-trips to the database, each waiting for confirmation before the next one fires. **The N+1 pattern is silent.** No warning, no exception. Your application just becomes very, very slow, and your DBA starts forwarding you graphs. This post covers the real alternatives available in 2026. EF Core 7 introduced native server-side batch operations, and EF Core 10 further refines them. [Entity Framework Extensions (EFE)](https://entityframework-extensions.net/) from ZZZ Projects extends the story still further, filling a gap EF Core has not addressed natively. We will cover both honestly, including where the native tools are genuinely sufficient and where EFE earns its place. ## The Built-In Answer: ExecuteUpdate and ExecuteDelete EF Core 7 introduced ExecuteUpdate and ExecuteDelete, two methods that sidestep the change tracker entirely and translate LINQ predicates directly into server-side SQL. Both are available and stable in EF Core 10. ### ExecuteDelete The session cleanup from the introduction becomes: ``` // One SQL statement. Zero entities loaded into memory. await context.Sessions     .Where(s => s.ExpiresAt < DateTime.UtcNow)     .ExecuteDeleteAsync(); // Generated SQL: // DELETE FROM [Sessions] WHERE [ExpiresAt] < @cutoff ``` The query executes immediately. There is no deferred tracking, no SaveChanges() call required. One round-trip. Zero memory pressure from entity loading. ### ExecuteUpdate Deactivating stale customers: ``` await context.Customers     .Where(c => c.IsActive && c.LastLoginDate < cutoffDate)     .ExecuteUpdateAsync(s =>         s.SetProperty(c => c.IsActive, false)          .SetProperty(c => c.DeactivatedAt, DateTime.UtcNow)); // Generated SQL: // UPDATE SET .[IsActive] = 0, .[DeactivatedAt] = @now // FROM [Customers] AS // WHERE .[IsActive] = 1 AND .[LastLoginDate] < @cutoff ``` Multiple SetProperty calls chain naturally. They all compile into a single UPDATE statement. ### What You Need to Know Before Using These These methods behave very differently from anything EF Core did before version 7. Several gotchas are worth internalizing before they surface in a production incident. **Execution is immediate, not deferred to SaveChanges.** The SQL fires the moment you call the method. **These methods are completely change-tracker-unaware.** If you have Customer entities already loaded in the current DbContext, their in-memory state will not update. After an ExecuteUpdate, any tracked entities are stale and need to be refreshed or discarded. **EF interceptors do not fire.** Business logic, audit logging, or domain events wired through EF Core interceptors will not run for these operations. If you need that behavior, you will need alternative hooks or database-level triggers. **A single call can only target one table.** Multi-table updates require restructuring the query. **There is no ExecuteInsert.** EF Core 10 has no native equivalent for a server-side INSERT…SELECT. That gap gets its own section next. **Transaction hygiene is your responsibility.** If you mix these methods with SaveChanges() in the same request, wrap everything in an explicit transaction. If SaveChanges() fails, ExecuteUpdate changes will not roll back automatically. ## The Missing Piece: There Is No ExecuteInsert If you need to copy rows from one table to another server-side, without loading them into .NET memory first, EF Core 10 still has no native support. The closest native option is AddRange combined with SaveChanges(), but that requires loading the source rows first: ``` // Native EF Core -- requires loading source rows into memory first var archivedAt = DateTime.UtcNow; var toArchive = await context.Customers     .Where(c => !c.IsActive)     .Select(c => new ArchivedCustomer {         Code          = c.Code,         Name          = c.Name,         Email         = c.Email,         LastLoginDate = c.LastLoginDate,         ArchivedAt    = archivedAt     })     .ToListAsync();  // c.IsActive && c.LastLoginDate < cutoffDate)     .ExecuteUpdateAsync(s =>         s.SetProperty(c => c.IsActive, false)); // EFE -- lambda assignment style await context.Customers     .Where(c => c.IsActive && c.LastLoginDate < cutoffDate)     .UpdateFromQueryAsync(c => new Customer { IsActive = false }); // Both generate the same SQL. ``` For a greenfield .NET 10 project with no EFE dependency, ExecuteUpdate and ExecuteDelete are the right defaults. No third-party library needed. EFE’s UpdateFromQuery and DeleteFromQuery become relevant in two situations: you are maintaining a codebase targeting EF Core 6 or earlier where the native methods are not available, or your team already uses EFE for bulk insert operations and wants API consistency across the data layer without mixing two different patterns. ### InsertFromQuery: The Gap EF Core Does Not Fill This is where EFE provides genuine, uncontested value. The archive operation from Section 2 becomes: ``` // EFE -- server-side INSERT...SELECT, zero entity loading var archivedAt = DateTime.UtcNow; await context.Customers     .Where(c => !c.IsActive)     .InsertFromQueryAsync(         "ArchivedCustomers",         c => new {             c.Code,             c.Name,             c.Email,             c.LastLoginDate,             ArchivedAt = archivedAt         }); // Generated SQL (approximate): // INSERT INTO [ArchivedCustomers] //   ([Code], [Name], [Email], [LastLoginDate], [ArchivedAt]) // SELECT .[Code], .[Name], .[Email], .[LastLoginDate], @archivedAt // FROM [Customers] AS // WHERE .[IsActive] = 0 ``` The source rows are never loaded into .NET objects. The entire operation, from filtering through projecting to inserting, happens inside the database. Memory usage is effectively O(1) regardless of how many rows are transferred. ## Benchmarks: The Honest Numbers The benchmark project uses BenchmarkDotNet 0.15.8 running on .NET 10 against SQL Server LocalDB. Measurements are mean execution time across 5 iterations after 2 warm-up rounds. Memory figures are managed allocations per operation as reported by BenchmarkDotNet’s MemoryDiagnoser. ### UPDATE Benchmarks: Deactivate Matching Customers **Row Count****Load + SaveChanges****ExecuteUpdate (EF Core 10)****UpdateFromQuery (EFE)****10K**~473.0 ms~157.6 ms~187.7 ms**100K**~4,527.3 ms~1,712.6 ms~1,706.1 ms**500K**~17,768.6 ms~8,222.7 ms~8,259.6 ms**1M**~34,674.0 ms~14,517.4 ms~16,913.3 ms### DELETE Benchmarks: Remove Matching Customers **Row Count****Load + RemoveRange****ExecuteDelete (EF Core 10)****DeleteFromQuery (EFE)****10K**~395.08 ms~100.6 ms~130.2 ms**100K**~3,252.91 ms~864.8 ms~1976.5 ms**500K**~15,137.00 ms~4,170.2 ms~12,364.2 ms**1M**~32,588.83 ms~8,782.8 ms~29,784.2 ms### INSERT Benchmarks: Archive Inactive Customers to a Secondary Table **Row Count****Load + Project + SaveChanges****InsertFromQuery (EFE)****10K**~350.78 ms~37.21 ms**100K**~2,668.93 ms~150.51 ms**500K**~13,646.34 ms~618.54 ms**1M**~26,732.17 ms~1,200.44 ms### Reading the Results Two patterns emerge across all three suites. **The naive approach scales linearly with row count.** Going from 10K to 1M rows takes roughly 100x longer and uses roughly 100x more memory. This is expected: you are allocating 100x as many objects and issuing 100x as many SQL statements. **Server-side approaches scale almost sub-linearly.** The primary cost is SQL Server query planning and I/O throughput, not .NET object allocation or round-trip count. Going from 10K to 1M rows takes roughly 10x longer for a simple predicate-based operation, and memory usage stays near-constant regardless of row count. **EF Core native and EFE are practically equivalent for UPDATE and DELETE.** The delta between ExecuteUpdate and UpdateFromQuery is noise at these row counts. Both translate to the same underlying SQL. Your choice between them should be based on version compatibility and team conventions, not performance. **InsertFromQuery is in a category of its own.** There is no native EF Core equivalent, so the comparison is between loading rows into memory and not doing so at all. The allocation difference is the most striking signal: the naive approach allocates proportional to row count; InsertFromQuery allocates near-zero. ## Choosing the Right Tool Here is how to think about which approach to reach for: **Scenario****Recommended Approach****Why**Uniform rule applied to all matching rows (same value to every row)**ExecuteUpdate / ExecuteDelete**Native, zero dependenciesSimple server-side DELETE or UPDATE, EF Core 7+ target**ExecuteDelete / ExecuteUpdate**No reason to add EFE for theseServer-side UPDATE or DELETE, pre-EF Core 7 codebase or EFE already in use**UpdateFromQuery / DeleteFromQuery (EFE)**API consistency across data layerTable-to-table INSERT…SELECT without loading entities**InsertFromQuery (EFE)**EF Core 10 has no native equivalentPer-row values (each entity has different data to update)**BulkUpdate (EFE)**ExecuteUpdate only supports uniform set-based rulesMixing batch ops + SaveChanges in the same request**Wrap in an explicit transaction**Neither approach participates automaticallyA useful starting point: reach for EF Core native methods first. If you hit a scenario where they fall short, whether that is a pre-EF Core 7 target, a need for InsertFromQuery, or a requirement to update per-row values with BulkUpdate, that is when EFE earns its dependency cost. ## Caveats That Will Catch You Off-Guard Both the native EF Core approach and EFE’s batch operations share a set of behaviors that are easy to miss the first time. ### Stale In-Memory Entities After any server-side batch operation, entities already loaded in the current DbContext are stale. The database has changed; the change tracker has not. Reload or discard them explicitly: ``` await context.Customers     .Where(c => !c.IsActive)     .ExecuteDeleteAsync(); // Clear tracked entities -- they no longer exist in the database context.ChangeTracker.Clear(); ``` ### Transaction Hygiene Is Your Responsibility If you mix batch operations with SaveChanges() in the same unit of work, wrap the entire block in an explicit transaction: ``` await using var tx = await context.Database.BeginTransactionAsync(); await context.Orders     .Where(o => o.IsCancelled)     .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Archived)); // Other tracked changes... await context.SaveChangesAsync(); await tx.CommitAsync(); // If SaveChanges() threw above, ExecuteUpdate is rolled back too. ``` ### Cascade Rules and Interceptors EF Core’s configured cascade delete behaviors will not fire for ExecuteDelete or DeleteFromQuery. Database-level foreign key cascades will still run. EF Core interceptors and domain events do not fire for any batch operation. If audit logging or domain logic lives in those hooks, the typical alternatives are database triggers or application-level pre/post hooks. ### SetProperty Has Limits That BulkUpdate Does Not Share ExecuteUpdate applies the same transformation to every matching row. If you need to update 10,000 customer records each with a different value, say updated addresses from an import, you need EFE’s BulkUpdate, not ExecuteUpdate. They are different tools for different problems. ## Conclusion EF Core 10 has closed the performance gap that made batch operations a problem for most teams. ExecuteUpdate and ExecuteDelete are production-ready, require no external dependencies, and should be your default for any set-based update or delete operation on EF Core 7 or later. Entity Framework Extensions extends that story in two directions. For teams maintaining pre-EF Core 7 codebases, UpdateFromQuery and DeleteFromQuery provide the same server-side semantics with a consistent API. InsertFromQuery addresses a genuine gap in EF Core’s toolbox: server-side table-to-table INSERT operations without loading source rows. It has no native equivalent in EF Core 10. The benchmark numbers make the case plainly. The naive pattern (load, modify, save) scales linearly with row count in both time and memory. Server-side operations scale to millions of rows with near-constant memory overhead and far lower elapsed time. At 1M rows, the difference between the naive approach and any server-side alternative is measured in orders of magnitude rather than percentages. The next post in this series covers EF Core Bulk Insert in depth: from AddRange through SqlBulkCopy to EFE’s BulkInsert with IncludeGraph, and what to choose at each scale threshold. [![](https://www.woodruff.dev/wp-content/uploads/2026/04/efe-effortlessly.png)](https://entityframework-extensions.net/) **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, EF Core, programming --- ### [ReSharper Made VS Code a Real Option for My .NET Work](https://www.woodruff.dev/resharper-made-vs-code-a-real-option-for-my-net-work/) **Published:** April 26, 2026 **Author:** Chris Woodruff **Content:** Most posts about ReSharper for VS Code want to convert you. This one doesn’t. I write most of my .NET in Rider. I’m not moving. What changed for me back in March 2026, when JetBrains shipped the official ReSharper extension for VS Code, is something smaller and more useful than a conversion story. I can finally pick up VS Code for a C# task and not feel like I just took a forty-percent IQ hit at the door. If that sounds dramatic, you have probably never tried to triage a production bug from a borrowed laptop. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_10-47-32-1024x576.png)## **The Quiet Tax on Doing .NET in VS Code** Here is the part nobody at Microsoft puts in a slide deck. The C# story in VS Code, even with C# Dev Kit, has always been the polite-but-shallow option. Rename works. Go-to-definition works. Build works. The basics behave. Then you ask for anything past the basics and you watch the seams come apart. You want to extract an interface from a class. You can’t, really. You want to ctrl-click into a NuGet package’s source. You can’t. You want to move six classes out of one giant file. You can do it the manual way, with cut and paste, like you’re back in 2010. You want a real Solution Explorer. You get a tree that almost works the way Visual Studio’s does and then surprises you somewhere uncomfortable. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_09-34-15-1024x576.png)This was the tax. Every time I opened VS Code for a C# task, I paid it. Pair programming on a teammate’s Mac, peeking at a coworker’s pull request over coffee, working inside a dev container, an SSH session into a build box, a quick fix on a Linux laptop that will never see Visual Studio. All of those moments came with the same compromise. Reach for VS Code, accept that you are about to do half the job in twice the time, get on with it. That tax is what got cheaper this year. Not gone. Cheaper. ## **What Actually Showed Up** ReSharper for VS Code is the same engine that has been running in Visual Studio for two decades and powering Rider for ten years, packaged for VS Code and any compatible editor. It went through a public preview most of last year, and the official 2026.1 landed in early March 2026. It runs in VS Code, Cursor, Google Antigravity, VSCodium, Windsurf, basically anything VS Code-compatible. It is on the Open VSX Registry now, so the Cursor and Codium crowd gets auto-updates rather than chasing VSIX downloads. Free for non-commercial work, including learning, OSS contributions that don’t earn you money, content creation, and hobby projects. Paid licenses for commercial use, included in the regular ReSharper, dotUltimate, and All Products Pack subscriptions. In practice, type “ReSharper” into the Extensions panel, click Install, and the right pieces land. ## **Why You’d Pick This Over C# Dev Kit** C# Dev Kit is not bad. That is also the most generous thing I can say about it. It is a perfectly serviceable answer to a question Microsoft mostly asked itself. JetBrains spent twenty years asking a harder version of the same question and shipping the answer in shorter loops. The gap shows. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/Explores-side-by-side.png)A few places the gap shows most: **Inspections that earn their keep.** Roslyn catches what Roslyn catches. ReSharper catches the rest. The 2026.1 release added a wave of inspections focused on runtime safety issues. Short-lived HttpClient instances that will exhaust sockets. LINQ chains that quietly enumerate twice. `ImmutableArray` initializations that compile fine and behave wrong at runtime. These are the bugs that ship to production and ruin a Friday. **Navigation that works on code you don’t own.** Ctrl-click into a method that lives inside a NuGet package and ReSharper decompiles the assembly so you can read the actual implementation right there in the editor. C# Dev Kit cannot do this. If you have ever debugged a third-party library by guessing what it does, you already know how badly you want this feature. **A real Solution Explorer.** Source generators visible in the tree. NuGet packages manageable in place. Project references where a .NET dev’s hand reaches for them. The same one Rider users already trust. **Live templates and postfix completion.** Type the name of a collection, then a dot, then foreach, then Tab. You get a fully-formed loop with the iterator variable named correctly. Type any expression, dot, notnull, and ReSharper wraps it in a guard for you. The first week, this feels like a parlor trick. The second week, plain VS Code feels broken. **Cross-editor consistency.** If you also work in Rider or full Visual Studio with ReSharper, your code style settings, your shortcuts, and your muscle memory all ride along. One honest caveat before we go further. ReSharper does not yet ship its own debugger for VS Code. It is on the roadmap. Until it lands, you keep Microsoft’s C# extension installed and use vsdbg. The setup section below covers exactly how to wire that up. ## **The Features That Pay Back the Install in the First Hour** **Inspections that show up the moment you open a file** Open a project, watch the gutter start working. Most of what you see will be familiar style or correctness hints. Some of it won’t be. ReSharper flags an async method declared as returning void with a one-line explanation of why that pattern eats exceptions silently. It catches a LINQ chain that calls Count() inside a loop. It tells you when an `ImmutableArray` got initialized in a way the runtime treats as the default value. Hit `Alt+Enter` (or `Cmd+.` on Mac) and the lightbulb usually has the fix queued and ready. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_09-44-51-1024x576.png)**Refactorings you would trust on a Friday afternoon** Rename works across project boundaries, including in Razor and Blazor markup. Extract Method lifts the code, creates the new method, and rewrites the original code to implement it. Move Type to File takes the six classes you crammed into one .cs file and gives each one its own home without breaking references. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-09-40-1024x577.png)**Navigation, including into code you didn’t write** Go to Everything (`Ctrl+T` on Windows, `Cmd+T` on Mac) is the best fuzzy-finder in the .NET tooling field. Types, files, symbols, all in one box. Go to Implementation on an interface declared inside a NuGet package drops you into a decompiled view of the actual implementation. Find Usages groups results by project, file, and usage kind, so you can scan a hundred-result list without your eyes glazing over. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-11-25-1024x577.png)**A Solution Explorer that should have been there from day one** Tree view of the solution, organized the way a .NET dev expects. Projects, dependencies, NuGet packages, source generators, all of it. Right-click for Add Project, Manage References, Manage NuGet. None of this was missing in C# Dev Kit, but it was rougher and a step or two slower at every interaction. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-14-59-1024x577.png)**Unit testing that finds your tests on its own** NUnit, xUnit, and MSTest discovered automatically. Run from the gutter. Results in a panel. One click jumps from a failing test to the line that broke it. If you have used the test runner in Rider, this is the same one wearing a VS Code coat. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-18-34-1024x577.png)**Live templates and postfix completion (the secret weapon)** The feature that turns ReSharper users into ReSharper missionaries. Type a collection name, dot, foreach, Tab, get a real loop with the iterator named for you. Postfix completion lets you type the expression first and the wrapper second. `someValue.notnull` becomes a null guard around someValue. someCollection.first becomes a `First()` call you can refine. Compounded over a workday, these save you hundreds of keystrokes you didn’t know you were spending. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-22-36-1024x577.png)**The Setup: Five Minutes, Done Right** This is where most blog posts hand-wave. Don’t skip this part. There is one specific combination of extensions that gets you the JetBrains experience without giving up F5, and another combination that fights itself for an hour. **Install these:** 1. **The ReSharper extension.** Search “ReSharper” in the VS Code Extensions panel and install. Same name on Open VSX if you are in Cursor or VSCodium. 2. **The .NET SDK.** ReSharper assumes a working dotnet is on your PATH. Run `dotnet --info` in a terminal to confirm. If that command works, you are set. 3. **The Microsoft C# extension** (ms-dotnettools.csharp). Keep this one. ReSharper does not yet ship its own debugger, and the Microsoft C# extension is what brings the coreclr debugger you need for ASP.NET Core, console apps, Blazor Server, and the rest. **Switch off these:** 4. **C# Dev Kit** (ms-dotnettools.csdevkit). JetBrains’ own getting-started page recommends turning it off while ReSharper is active, and the recommendation is correct. The two extensions overlap heavily on Solution Explorer, the test runner, and project templates. Run them both at once and you get duplicate squigglies, doubled inspections, and slower indexing. You can disable per workspace if you want to keep it around for projects where you’d rather use it. 5. **IntelliCode for C# Dev Kit.** Switch off alongside the Dev Kit, for the same reason. **The combination that works:** ReSharper on for the smart features. Microsoft C# extension on for the debugger. C# Dev Kit off. That is the lineup. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-26-49-1024x577.png)**One-time configuration:** 6. **Open the workspace.** ReSharper auto-detects `.sln`, `.slnx,` `.slnf`, or a bare `.csproj`. If it finds more than one solution, it shows an Open Solution picker. 7. **Let it index.** First open on a big solution takes a minute. After that, opens are quick. 8. **launch.json for debugging.** The Microsoft C# extension will offer to generate one if you don’t already have it. The minimal ASP.NET Core configuration looks like this: ``` {   "name": ".NET Launch",   "type": "coreclr",   "request": "launch",   "preLaunchTask": "build",   "program": "${workspaceFolder}/bin/Debug/net9.0/YourApp.dll",   "cwd": "${workspaceFolder}",   "env": { "ASPNETCORE_ENVIRONMENT": "Development" },   "serverReadyAction": {     "action": "openExternally",     "pattern": "\\bNow listening on:\\s+(https?://\\S+)"   } } ``` 9. Optional but worth doing. Open Settings, search “ReSharper,” and tune the inlay hints and inspection severity if the defaults feel too loud out of the gate. ![](https://www.woodruff.dev/wp-content/uploads/2026/04/2026-04-25_13-29-55-1024x577.png)**Where to Go When You Want More** If the install sticks for you, these are the bookmarks worth keeping: - The official docs at [jetbrains.com/help/resharper-vscode/.](https://www.jetbrains.com/help/resharper-vscode/Get_started.html) Surprisingly readable, with separate getting-started pages for VS Code, Cursor, and Antigravity. - The .NET Tools Blog at [blog.jetbrains.com/dotnet/](https://blog.jetbrains.com/dotnet/). Release notes are where the real feature deltas live. - Issue tracker and community forum, both linked from inside the extension. Click the R# icon, then Contact Support. - Keyboard shortcuts. If you are coming from Visual Studio with ReSharper or from Rider, install the JetBrains keymap for VS Code so `Ctrl+T`, `Alt+Enter`, and the rest behave the way your fingers remember. - Pricing at [jetbrains.com/resharper/buy/](https://www.jetbrains.com/resharper/buy). Free for non-commercial as said before, paid otherwise. **A Tuesday That Actually Worked** Last Tuesday I cloned a thirty-project ASP.NET Core monolith I had never seen before. The kind of repo that opens in Visual Studio with a coffee break attached. I was on a Linux laptop in a coffee shop, on hotel WiFi, and I needed to ship a small fix before standup. I opened it in VS Code. Not because I wanted to. Because Visual Studio was not on the laptop, and never will be. ReSharper indexed the solution in about ninety seconds. `Ctrl+T`, three letters of the controller name, landed on the file. `Ctrl+B` into the service it called. `Ctrl+B` again into a method that lived inside a NuGet package, decompiled in front of me, the bug staring up. Alt+Enter, Extract Method, write a test, F5 to run with the Microsoft debugger, breakpoint hit, fix verified, push the branch, PR up. Twenty-two minutes. From a laptop that will never see Visual Studio. That’s the case I want to make. I am not switching editors. Rider is still where I live. I am just glad that VS Code stopped being the place where I do half the work in twice the time. The tax is cheaper. The option is real. And the next time I am on a borrowed Mac at three minutes to standup, I know exactly what extension I am installing first. ***“I partnered with JetBrains to bring you this blog post, and I’m excited to share my honest experience.”*** **Categories:** Developer Experience **Tags:** .NET, C#, dotnet, IDEs, programming --- ### [The Strategic Case for "Use What Works": Why Smart Tech Leaders Stop Reinventing the Wheel](https://www.woodruff.dev/the-strategic-case-for-use-what-works-why-smart-tech-leaders-stop-reinventing-the-wheel/) **Published:** April 28, 2026 **Author:** Chris Woodruff **Content:** A peculiar kind of pride runs through the technology industry. Many leaders believe their problems are unique, their scale unprecedented, their requirements so specific that only a custom-built solution will do. This belief is also one of the most expensive mistakes your organization can make. ## The Hidden Cost of “Building It Ourselves” Every custom solution your team builds is a solution your team must maintain. Forever. Or at least until someone finally rips it out, usually years after the original developer has moved on, leaving behind a legacy that no one fully understands and everyone is afraid to touch. If you have spent any time in technology leadership, you have inherited one of these systems. You know the feeling: the documentation is sparse, the original author is unreachable, and the thing is somehow load-bearing for your entire operation. Ask yourself honestly: how much of your engineering budget goes toward maintaining problems that someone else has already solved? ## The “Use What Works” Philosophy The [Use What Works](https://usewhatworks.org/) vision offers a direct principle: organizations should generally use existing, proven, and sustainable solutions to common problems rather than developing, maintaining, and running their own. This philosophy emerges from recognizing where your organization’s differentiation actually lies. Your authentication system is probably not your competitive advantage. Neither is your deployment pipeline, your logging setup, or your database management tooling. These are solved problems. Teams whose entire focus is solving them have solved them well. ## Five Reasons This Matters for Your Business ### 1. Your Best Engineers Should Be Building Your Product Every hour a talented developer spends building a custom caching layer is an hour they are not spending on the features that differentiate your product in the market. Proven solutions let your team focus on value creation rather than infrastructure reinvention. ### 2. Turnover Is Inevitable. Plan for It. People leave. When they do, who maintains the custom solution they built? If documented at all, it reflects their style, their assumptions, their understanding. Established technologies come with community knowledge, third-party documentation, and a hiring pool of people who already know how they work. ### 3. Battle-Tested Beats Theoretically Elegant Linus’s Law observes: “Given enough eyeballs, all bugs are shallow.” Software deployed across thousands of organizations, at every conceivable scale, has encountered and resolved problems your custom solution has not even discovered yet. You benefit from the collected experience of an entire community. ### 4. Predictability Reduces Risk Proven solutions come with documentation, case studies, and communities. When something breaks at 2 AM, there is likely a Stack Overflow thread, a support agreement, or a consultant who has seen it before. Your custom solution has none of these things. Just an on-call engineer reading code they did not write. ### 5. AI Assistance Works Better with Established Tools If your teams are using AI coding assistants, those tools perform dramatically better with well-documented, widely used technologies. The models have seen thousands of examples of proper implementation. They have not seen your custom internal tools. ## The Sustainability Argument A broader consideration exists here too. Maintaining quality software costs money. When organizations cluster around proven solutions, the cost of that maintenance is distributed across everyone who benefits. This creates sustainable systems where even smaller organizations can access enterprise-grade infrastructure. The alternative, where everyone builds their own, fragments effort, duplicates cost, and delivers worse outcomes for everyone. ## When Custom Makes Sense This is not absolutism. Legitimate cases exist for building custom solutions: when your requirements genuinely are unique, when the existing options do not meet your needs, or when the problem space is so central to your business that you need complete control. But those cases are rarer than most technology organizations believe. The only honest question worth asking: should we build this ourselves? ## The Bottom Line Stop waste. Reduce risk. Focus your engineering talent on the problems that actually differentiate your business. Use what works. [Use What Works](https://usewhatworks.org/) is an open initiative encouraging organizations to embrace proven, sustainable solutions. Learn more about what this means for business leaders on our homepage: . **Categories:** Business of Software **Tags:** business of software --- ### [htmxRazor v2.0.0: Platform and DX](https://www.woodruff.dev/htmxrazor-v2-0-0-platform-and-dx/) **Published:** April 14, 2026 **Author:** Chris Woodruff **Content:** *An interactive playground, a live theme builder, SignalR, and a Kanban board, all on .NET 10 with no breaking changes.* htmxRazor v2.0.0 is out today. The six features in this release span three distinct areas: developer tooling in your editor, interactive features on the demo site, and two new library components. This is the first major version since the initial release, and each item targets a specific friction point in the experience of building server-rendered ASP.NET Core apps with htmx. You can find the NuGet package details [here](https://www.nuget.org/packages/htmxRazor). ## **CSS Anchor Positioning** Four components — tooltip, popover, popup, and dropdown — now use the CSS Anchor Positioning API in browsers that support it (Chrome 125+, Edge 125+). In all other browsers, the existing `rhx-position.js` engine fires automatically. Nothing changes for users on Firefox or Safari; the fallback is identical to current behavior. The design avoids static anchor names, which collide when multiple component instances appear on the same page. Instead, `RHX.applyCssAnchorPositioning()` generates unique `--rhx-anchor-*` names per instance. Flip behavior moves from JavaScript detection to CSS `position-try-fallbacks: flip-block, flip-inline`. Arrow elements remain JS-managed, since the CSS Anchor Positioning API does not handle them. This is a purely additive change. No Tag Helper attributes changed. No existing tests required modification. ## **Interactive Component Playground** Every component demo page can now host a live property toggle panel. Change variant, size, or state via dropdowns and checkboxes; htmx posts the updated props to the server and re-renders the component preview with the new markup. URL state updates on each change, so any configuration is shareable with a link. Below the preview, the generated Razor markup appears as a copyable code block. v2.0.0 ships the Button playground as the reference implementation. The `PlaygroundProperty` model and shared `_PlaygroundPanel.cshtml` partial are reusable, so the remaining components can be wired up incrementally following the same pattern. ## **Theme Builder** The Theme Builder page on htmxRazor.com lets you adjust CSS design tokens — colors, spacing, border radius, typography — and watch the changes applied to a live component preview in real time. When the configuration is right, download a complete CSS file with your custom token values ready to drop into your project. The builder exposes the `--rhx-*` token layer. Changing palette colors does not automatically update semantic tokens; that relationship is documented in an inline callout on the page. Semantic tokens must be adjusted independently when you need the full cascade. ## **SignalR Hub Connector** ``` ``` The `` Tag Helper connects to an ASP.NET Core SignalR hub and pushes received messages into a target element via htmx swap semantics. No client-side JavaScript is required in your Razor pages beyond loading the `@microsoft/signalr` client library. The helper emits a `` with `data-rhx-signalr` attributes. The `rhx-signalr.js` module opens the connection and on each hub event calls `htmx.process()` on the inserted HTML, preserving htmx processing for any elements in the server response. SignalR is built into `Microsoft.AspNetCore.App`. No new NuGet dependencies are introduced. The demo page ships a live clock and a minimal chat, both driven by a single `DemoHub` with a hosted background service. 15 unit tests cover the Tag Helper output. ## **Kanban Board** ``` Implement login flow ``` The Kanban family provides three Tag Helpers: ``, ``, and ``. Cards are draggable via HTML Drag and Drop. On drop, an htmx POST fires with the card ID, source column, target column, and calculated insert position. The server controls all state. Keyboard navigation runs independently of mouse drag: Tab to focus, Enter or Space to grab, arrow keys to move between columns or reorder within a column, Enter or Space again to drop. This provides an accessible path on touch devices, where HTML DnD does not fire. WIP limits are enforced via `max-cards` on the column Tag Helper. Exceeding the limit applies a danger border and altered header background. 21 unit tests cover the three Tag Helpers. The demo ships a 3-column board with 6 sample tasks and a reset button. ## **Test Count** v2.0.0 adds 36 new tests. The running total is 1,838. ## **Targets .NET 10. No Breaking Changes.** Every change is additive. New components are new APIs. Snippet extensions are new artifacts. The CSS Anchor Positioning work lives entirely inside `@supports` blocks. Existing apps install the new NuGet package and continue working without modification. **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, htmx, htmxRazor, programming --- ### [htmxRazor v1.4.0: SSE Streaming, Multi-step Wizard, and Optimistic UI](https://www.woodruff.dev/htmxrazor-v1-4-0-sse-streaming-multi-step-wizard-and-optimistic-ui/) **Published:** March 24, 2026 **Author:** Chris Woodruff **Content:** v1.4.0 ships today with seven features built around a single theme: the interaction patterns real production applications actually need. Real-time content streaming, multi-step workflows, immediate UI feedback, response-aware forms, and a handful of targeted quality-of-life additions. ## **SSE Stream** `` wraps the htmx SSE extension declaratively. One Tag Helper, one server endpoint, no boilerplate: ``` ``` The component renders with `hx-ext="sse"`, `sse-connect`, `sse-swap`, `aria-live="polite"`, and `aria-atomic="false"` — the full SSE setup in a single line of Razor markup. Three extension methods on `HttpResponse` handle the server side: - `PrepareSseResponse()` — sets Content-Type, Cache-Control, and Connection headers - `WriteSseEventAsync()` — writes a single named SSE event - `WriteSseStreamAsync()` — streams an `IAsyncEnumerable`, flushing after each event ``` public async Task OnGetStream() { Response.PrepareSseResponse(); await Response.WriteSseStreamAsync(GetUpdatesAsync(), "update"); } private async IAsyncEnumerable GetUpdatesAsync() { for (int i = 0; i < 10; i++) { yield return $"Update {i}"; await Task.Delay(1000); } } ``` The handler yields HTML fragments. htmx receives them and swaps into the target element. The server side stays idiomatic — `IAsyncEnumerable` rather than a custom streaming abstraction. htmx 2.x ships SSE as a core extension, so no additional NuGet dependency is required. ## **Multi-step Wizard** `` and `` give you a visual stepper with server-side state persistence through TempData. Steps declare inline using the same child-helper registration pattern as the data table: ``` ``` `WizardState` tracks current step, total steps, and which steps are complete. `WizardSessionExtensions` provides `GetWizardState` and `SetWizardState`, which persist and retrieve state through TempData. Wizard progress survives redirects without custom session infrastructure: ``` public IActionResult OnPostWizardNext() { var state = TempData.GetWizardState("checkout"); state.MarkComplete(state.CurrentStep); state.CurrentStep++; TempData.SetWizardState("checkout", state); return RedirectToPage(); } ``` The stepper renders as a `` with `role="list"` on the step container and `aria-current="step"` on the active item. Keyboard navigation follows the roving tabindex pattern — arrow keys move between step indicators. The `rhx-linear` attribute (default true) enforces sequential step completion and prevents skipping. One deployment note: the default cookie-based TempData provider works without any additional configuration. For distributed deployments behind a load balancer, configure a distributed TempData provider to share state across instances. ## **Timeline** ``, ``, and `` cover audit logs, activity feeds, deployment histories, and any other ordered event sequence: ``` Deployment completed Build warnings detected ``` Items carry a `rhx-variant` attribute for connector color: `neutral`, `brand`, `success`, `warning`, or `danger`. The `rhx-active` attribute marks the current item with a highlight ring and `aria-current="step"`. Custom icons slot into the dot via ``. Layout supports `vertical`, `horizontal`, and `alternate` alignment. The entire connector, dot, and content region are CSS-driven — no JavaScript. ## **Response-Aware Form** `` eliminates the boilerplate every htmx form currently requires: manually adding `hx-ext="response-targets"`, writing per-status-code target selectors, disabling submit buttons during requests, and managing error container visibility. ``` ``` The `rhx-error-target` shorthand sets the 422, 4xx, and 5xx targets to the same selector. Individual `rhx-target-422`, `rhx-target-4xx`, and `rhx-target-5xx` attributes are available for different error destinations per status range. One note: the `response-targets` htmx extension must be loaded separately by the host application. The Tag Helper sets `hx-ext="response-targets"` but does not bundle the extension JS. This follows the standard htmx extension loading model — htmxRazor wires the attributes, the host app controls which extensions are present. ## **Optimistic UI** `rhx-optimistic="true"` is now available on ``, ``, and ``. Each component reflects state changes immediately on click before the server responds. On error, it reverts automatically. ``` ``` Per component: - **Button**: shows a loading spinner immediately on click, removes it on request completion - **Switch**: toggles visual state immediately, reverts on `htmx:responseError` - **Rating**: updates star count immediately, reverts to the saved value on error The revert animation applies `rhx-optimistic--reverted` for 600ms. It is suppressed when `prefers-reduced-motion: reduce` is set. State saving and revert logic live in `rhx-optimistic.js`, which re-initializes after `htmx:afterSettle` for dynamically inserted content. ## **Load More** `` is a button-triggered pagination pattern for content feeds and list views. Simpler than infinite scroll, more explicit than full pagination controls: ``` ``` The button appends fetched content to the target and removes itself via `hx-on::after-request`. No cleanup required. The pattern chains naturally by pointing successive calls at incrementing page route values. ## **Dialog Size Variants** `` now accepts `rhx-size`: `small` (24rem), `medium` (32rem), `large` (48rem), `full` (90vw), or any CSS width value such as `600px` or `80%`. Custom values set a `--rhx-dialog-width` CSS custom property via inline style. The default dialog max-width increases from 32rem to 48rem. Existing dialogs may render slightly wider if their content fills the available space. Restore the previous behavior with `rhx-size="medium"`. ## **Test coverage** v1.4.0 adds 147 tests across all new and modified components, bringing the library total to 1,802. All changes are additive. No breaking changes. No new NuGet dependencies. ## **Install** ``` dotnet add package htmxRazor ``` Full changelog, API docs, and live demos at . Source at . **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, htmx, htmxRazor, programming --- ### [htmxRazor v1.3.0: Data Table, Accessibility, and Modern CSS](https://www.woodruff.dev/htmxrazor-v1-3-0-data-table-accessibility-and-modern-css/) **Published:** March 15, 2026 **Author:** Chris Woodruff **Content:** v1.3.0 lands today with six features organized around a clear theme: production patterns for .NET developers building server-rendered UIs that work correctly for everyone, including keyboard users and screen reader users. Here is what shipped. ## **Data Table** The data table is the feature request I hear most from .NET developers evaluating htmx. Until now there was no MIT-licensed ASP.NET Core Tag Helper solution for this pattern. v1.3.0 changes that. Three Tag Helpers compose the component: - `` – the wrapper; handles loading state, sticky header, and ARIA attributes - `` – child helper that registers column definitions: field, header text, sortable, filterable, width, and alignment - `` – pagination controls that slot directly into the table via the same slot pattern used by `` Sort and filter interactions emit `hx-get` requests and the server returns `` partials. A new `DataTableRequest` model binder picks the sort field, direction, page number, page size, and filter values off the query string so handler code stays readable: ``` public IActionResult OnGetTableData(DataTableRequest request) { var query = _db.Products.AsNoTracking(); if (!string.IsNullOrEmpty(request.Sort)) query = request.SortDirection == "desc" ? query.OrderByDescending(e => EF.Property(e, request.Sort)) : query.OrderBy(e => EF.Property(e, request.Sort)); var items = query .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToList(); return Partial("_ProductTableBody", items); } ``` Column declarations live inline in markup: ``` ``` Every sort button is a native ``. Every sortable column carries `aria-sort`. Every filter input includes `aria-label`. The table renders with `role="grid"` and a proper ``. The loading state toggles `aria-busy` on the container. This is how it should be built from the start. One note on security: the `DataTableRequest` model binder accepts any string value for the `Sort` field. Your handler should validate that value against an allow-list of known property names before passing it to `EF.Property`. The demo page shows this pattern. ## **Focus Management After Swaps** WCAG 2.4.3 requires that dynamic content changes move keyboard focus to a predictable location. When htmx swaps content, focus stays wherever it was before the swap, which may now point at a removed element. That strands keyboard users with no indication that anything changed. v1.3.0 ships `rhx-focus-swap.js` and adds `rhx-focus-after-swap` to the base Tag Helper class, making it available on any component: ``` Load ``` Three special values: - `"first"` — focus the first focusable element within the swapped content - `"self"` — focus the element itself - `"none"` — explicitly opt out Dialog and Drawer Tag Helpers default to `"first"` without any configuration required. Focus fires via `requestAnimationFrame` to let the DOM settle before the call. ## **Command Palette** `` opens on Cmd+K (Mac) or Ctrl+K (Windows/Linux), fires a debounced `hx-get` to a search endpoint, and renders entirely server-provided results. Keyboard navigation uses arrow keys to move through items, Enter to select, and Escape to close and return focus to the trigger element. ``` ``` Results group server-side with `` and ``. The search input carries the full ARIA combobox pattern: `role="combobox"`, `aria-expanded`, `aria-controls`, and `aria-autocomplete="list"`. The panel itself is `role="dialog"` with `aria-modal="true"`. There is no client-side state management for the result list — the server owns that entirely. ## **Container Queries** The card, dialog, split panel, and data table components now adapt to their container width using `@container` queries. Components respond to the space they occupy, not the viewport. This matters most in dashboard and sidebar layouts where the same component might sit inside a full-width section or a 280px sidebar column. ``` /* Before */ @media (max-width: 480px) { .rhx-card__image { display: none; } } /* After */ @container (max-width: 480px) { .rhx-card__image { display: none; } } ``` No Tag Helper changes. No configuration. Container query support lands in Chrome 105+, Firefox 110+, and Safari 16+, which covers the full modern browser surface for a .NET 10 audience. ## **Skip Nav and Landmarks** Two new Tag Helpers for page-level accessibility, addressing WCAG 2.4.1 (Bypass Blocks): `` renders a visually hidden link that becomes visible on focus and jumps keyboard users past navigation to a configurable target. `` wraps content in the correct semantic landmark element with a proper `aria-label`: `main`, `nav`, `aside`, `header`, `footer`, `section`, `search`, or `form`. Both components are small in surface area but foundational for any application that needs to pass a WCAG audit. ## **APG Keyboard Audit** All four existing interactive components were audited against the W3C ARIA Authoring Practices Guide keyboard patterns. The components covered: Tabs, Tree, Dropdown, and Combobox. The gaps found and closed: - Type-ahead search in Tree and Dropdown - `Home` and `End` key support in Dropdown - `Alt+ArrowDown` and `Alt+ArrowUp` patterns in Combobox - Missing `aria-expanded`, `aria-haspopup`, and role attributes across components A shared type-ahead utility now lives in `rhx-core.js` for use across components. The patterns from this audit carry directly into the new Data Table and Command Palette keyboard implementations. ## **A note on the European Accessibility Act** The European Accessibility Act took effect in June 2025, making WCAG 2.2 AA a legal requirement for digital products sold in EU markets. The accessibility work in v1.3.0 is not decoration. Components ship with correct ARIA semantics, keyboard navigation, and focus management because that is what the standard requires and what users depend on. ## **Install** ``` dotnet add package htmxRazor ``` Full changelog, API docs, and live demos at htmxRazor.com. Source at [github.com/cwoodruff/htmxRazor](https://github.com/cwoodruff/htmxRazor). **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, programming --- ### [htmxRazor 1.2.0: Toast Notifications, Pagination, and the End of CSS Specificity Fights](https://www.woodruff.dev/htmxrazor-1-2-0-toast-notifications-pagination-and-the-end-of-css-specificity-fights/) **Published:** March 7, 2026 **Author:** Chris Woodruff **Content:** The first feature release after htmxRazor hit 1.1 is here, and it targets the three complaints I hear most from .NET developers building server-rendered apps with htmx: “I need toast notifications,” “I need pagination that works with htmx from the start,” and “your CSS keeps fighting with mine.” Version 1.2.0 addresses all three. Here is what shipped. ### Toast Notifications That Actually Work with htmx Every htmx-powered app needs toast notifications. A user submits a form, the server processes it, and you need to tell them what happened. Until now, your options in the ASP.NET Core world were to wire up a JavaScript toast library by hand or build your own partial-view-plus-htmx-oob-swap plumbing. htmxRazor 1.2.0 ships a complete toast notification system. Drop a `` on your layout, then trigger toasts from the server using the `HxToast()` or `HxToastOob()` extension methods. The component handles auto-dismiss timers, severity variants (success, warning, danger, info), stacking when multiple toasts fire at once, and `aria-live` announcements so screen readers pick up every notification automatically. No JavaScript. No third-party library. One Tag Helper and a server-side method call. ### Pagination Built for htmx Pagination is another pattern that shows up in nearly every production app, yet nobody had shipped a .NET Tag Helper that wires up htmx navigation correctly. The new `` component gives you page buttons, ellipsis for large ranges, first/last/prev/next controls, and size variants. All page transitions happen through htmx, so you get partial page updates without full reloads. If you have been hand-coding pagination partials on every project, this replaces all of that with a single component. ### CSS Cascade Layers: No More Specificity Wars This is the change that will matter most to teams adopting htmxRazor in existing applications. Every component library ships CSS that eventually collides with your own styles. You write a rule, the library’s rule wins because of higher specificity, and you start sprinkling `!important` everywhere. It is a familiar and miserable cycle. Version 1.2.0 wraps all htmxRazor component CSS inside `@layer` declarations. Cascade layers let the browser resolve specificity in a predictable order: any CSS you write outside a layer will always beat CSS inside one. That means your application styles win by default, with zero specificity hacks needed. This single change makes htmxRazor significantly easier to adopt in brownfield projects that already have their own stylesheets. ### Accessibility: ARIA Live Region Manager The new `` component solves a problem that most developers do not realize they have until an accessibility audit flags it. When htmx swaps content on the page, screen readers do not automatically announce the change. Users who rely on assistive technology can miss critical updates entirely. The live region manager listens for htmx swaps and pushes announcements to screen readers with configurable politeness levels (`polite` or `assertive`) and atomic update control. If you care about building applications that work for all of your users, this component closes a real gap. ### View Transitions and hx-on:\* Support Two smaller additions round out the release. The new `rhx-transition` and `rhx-transition-name` attributes let you wire up the View Transitions API for animated page transitions with no custom JavaScript. And the `hx-on:*` dictionary attribute on the base Tag Helper class brings full support for htmx 2.x event handler attributes across every component in the library. ### Upgrade Now ``` dotnet add package htmxRazor --version 1.2.0 ``` Browse the full docs and live demos at , and check the source at . htmxRazor is MIT licensed and accepting contributions. If toast notifications, proper pagination, or cascade layers solve a problem you have been working around, give 1.2.0 a try. **Categories:** htmx **Tags:** .NET, C#, dotnet, htmxRazor, programming, UX, wed components --- ### [Patterns of Distributed Systems in C# and .NET: A New Series for People Who Ship Real Systems](https://www.woodruff.dev/patterns-of-distributed-systems-in-c-and-net-a-new-series-for-people-who-ship-real-systems/) **Published:** January 29, 2026 **Author:** Chris Woodruff **Content:** Distributed systems do not fail because you missed a feature. They fail because responsibility is unclear. Two nodes act, both think they are right, and your data becomes a debate. This series is my pushback against cargo cult architecture. We are going to talk about the small, repeatable techniques that stop outages, not the buzzwords that decorate slides. Unmesh Joshi’s Patterns of Distributed Systems collects those techniques into a catalog you can apply. Each pattern names a recurring problem and proposes a practical move. My goal in this series is to translate those moves into C# and .NET code you can lift into production with minimal ceremony. Catalog link: ## Series Purpose This series exists for one reason: to make the behavior of distributed systems explicit. You will see how the patterns shape ownership, ordering, and acknowledgement, so your system stops relying on luck. Across the posts, you will learn how to: - Choose a single decision maker when your business rules assume one - Keep the state consistent when nodes and networks misbehave - Make time and ordering explicit instead of trusting clocks - Build recovery paths that do not create new failures Each post focuses on one pattern. You will get the intent, the failure story that pattern prevents, and C# examples you can adapt directly. I will also include a link back to the catalog entry so you can cross-check details. ## Series Pattern List The table below lists the patterns I plan to cover. After each post is published, I will replace the placeholder with a link to that post. PatternShort description[Leader and Followers](https://www.woodruff.dev/distributed-system-pattern-leader-and-followers-in-net-one-decision-maker-many-replicas-fewer-outages/)One node decides for a group, others replicate the result[Leader Election](https://www.woodruff.dev/leader-election-in-net-picking-one-boss-without-creating-two/)Select a leader and replace it safely after failure[Lease](https://woodruff.dev/lease-pattern-in-net-a-lock-with-an-expiration-date-that-saves-your-data/)Reject writes from a stale leader after a failover[Fencing Token](https://www.woodruff.dev/fencing-tokens-and-generation-clock-in-net-stop-zombie-leaders-from-writing/)Suspicion-based failure detection instead of fixed thresholds[Generation Clock](https://www.woodruff.dev/fencing-tokens-and-generation-clock-in-net-stop-zombie-leaders-from-writing/)Track leadership terms so every decision has an epochQuorumUse a majority for reads and writes to survive failuresCompare and SwapConditional writes that prevent lost updatesHeartbeatPublish liveness and detect silence earlyPhi Accrual Failure DetectorSuspicion-based failure detection instead of fixed thresholdsGossip DisseminationShare membership state without a central coordinatorWrite Ahead LogPersist intent before applying state changesSegmented LogManage retention and compaction by splitting logs into segmentsHigh Water MarkDefine what is committed and safe for clientsLow Water MarkDefine what can be deleted because no consumer needs itIdempotent ReceiverMake duplicate delivery harmlessTransactional OutboxPublish events reliably without losing themSagaLong running workflows with compensationsRetryRecover from transient failure without causing a stampedeTimeoutBound waiting so failures do not spreadCircuit BreakerStop hammering a dependency that is already failingRequest Waiting ListBackpressure that protects your service under loadRead RepairHeal replica drift during readsHinted HandoffBuffer writes for down replicas and replay laterAnti EntropyBackground reconciliation to reduce driftMerkle TreeFind differences quickly by comparing hashes by rangeLamport ClockOrdering without trusting wall clocksHybrid Logical ClockCausality plus physical time for easier diagnosisVersion VectorDetect concurrent writes so you do not overwrite silentlyTwo Phase CommitCoordinate a commit decision across participants## What I Am Not Going to Do I am not going to treat these patterns as optional decoration. If your system runs on more than one node, you are already living with these problems. You can solve them intentionally, or you can solve them at 2 a.m. while production is on fire. ## A Short Story About Two Leaders and One Pizza Order A team once had a nightly invoicing job that everyone described as “single instance.” That was not a design, it was a wish. Then the service gained a second node for availability. One night, both nodes started at the same moment. Both ran the job. Customers woke up to duplicate invoices. Finance woke up to a phone queue that sounded like a denial-of-service attack. The fix was embarrassingly simple once the team stopped negotiating with reality. We made one node the leader for that job using a lease, and we fenced writes with a term so the old leader could not keep invoicing after a takeover. The next morning, the only duplicates were the pizza slices we ordered to celebrate not being on a call. That is why these patterns matter. They turn “we assume only one thing happens” into “only one thing can happen.” **Categories:** Patterns **Tags:** .NET, C#, distributed, dotnet, patterns, programming --- ### [Fencing Tokens and Generation Clock in .NET: Stop Zombie Leaders From Writing](https://www.woodruff.dev/fencing-tokens-and-generation-clock-in-net-stop-zombie-leaders-from-writing/) **Published:** February 27, 2026 **Author:** Chris Woodruff **Content:** Leader election and leases answer a comforting question: who should be in charge right now. They do not fully answer the dangerous question: who can still write right now. A node can lose its lease, another node can become leader, and the old leader can still push writes through an existing database connection. When that happens, your system is not highly available. It is producing competing truths. Fencing tokens exist to end that story. Every leadership term gets a monotonically increasing number. Every leader write carries that number. The storage layer rejects stale numbers. The check sits at the boundary where corruption would otherwise enter. ## The Failure That Leases Do Not Prevent Picture this sequence. 1. Node A acquires the lease for `group-a` and starts writing to SQL Server. 2. Node A hits a long GC pause or a network stall. Its lease renewal stops. 3. The lease expires. Node B acquires the lease and becomes the new leader. 4. Node A resumes. Its SQL connection never died. It keeps writing as if nothing changed. Both nodes are alive. Both can reach the database. Without fencing, the database will accept both writers. If you have ever wondered how a “singleton” job ran twice with a lease in place, this is the missing piece. ## Pattern Definition and Intent A **fencing token** is a monotonically increasing value associated with leadership. A **generation clock** is the mechanism that produces those increasing values for a key, group, or shard. **Intent:** make it so only the current leader can perform writes, even when older leaders are still running. **Rule:** every write includes the token, and storage rejects writes with a smaller token than what it has already accepted. This is not a polite convention between services. This is an enforced guardrail at the write boundary. ## Mental Model Split responsibility into two contracts. - The lease decides who is expected to lead. - The fencing token decides who is permitted to write. Leases provide liveness. Fencing provides safety. You need both when correctness matters. ## What You Need to Build You need four things. 1. A token generator tied to leadership terms. 2. Token propagation through every leader write path. 3. A storage check that rejects stale tokens. 4. Telemetry for rejected writes so you can spot instability fast. Where the token goes depends on your architecture. - HTTP request headers for synchronous APIs - message metadata for asynchronous commands - database commands for direct writes - log append requests for event streams The goal stays the same: the write boundary sees the token and enforces monotonic progress. ## Generation Clock Options A generation clock must produce a number that only goes up for a given scope. Common options follow. ### Store the Term With the Lease Record When a leader takes over, increment the term stored alongside the lease. The lease and term move together. Good fit when you already have a durable coordinator. ### Dedicated Epoch Store With Atomic Increment Maintain an `epoch:{group}` value and bump it atomically on takeover. Good fit when you want to separate leadership liveness from write authority. ### Database Sequence per Group Use SQL Server to issue terms, either with a sequence object or a per-group row you update atomically. Good fit when SQL Server is the source of truth and you want the check and the clock in one system. ### Redis INCR per Key `INCR epoch:group-a` returns a monotonic value. Good fit when Redis is already your coordinator and you want a simple clock. No matter which clock you pick, one constraint stays: the term must come from an atomic operation, not from local memory. ## C# Model Types and Interfaces Keep the token explicit and hard to ignore. ``` public sealed record FencingToken(long Value); public interface IFencedWriter { Task AppendAsync(FencingToken token, string stream, ReadOnlyMemory payload, CancellationToken ct); } ``` You also need a source for the current token that leader code can ask for. ``` public interface IFencingTokenSource { ValueTask CurrentAsync(CancellationToken ct); } ``` ## A SQL Server Fenced Writer That Rejects Stale Leaders Below is a fenced writer that appends to a stream table. The table stores the highest token it has accepted. Any write carrying a smaller token is rejected. Schema example: ``` CREATE TABLE dbo.StreamWrites ( StreamId nvarchar(200) NOT NULL, Token bigint NOT NULL, Data varbinary(max) NOT NULL, CreatedAtUtc datetime2(3) NOT NULL CONSTRAINT DF_StreamWrites_CreatedAtUtc DEFAULT SYSUTCDATETIME() ); CREATE INDEX IX_StreamWrites_StreamId_Token ON dbo.StreamWrites(StreamId, Token); ``` Writer implementation: ``` using Microsoft.Data.SqlClient; public sealed record FencingToken(long Value); public interface IFencedWriter { Task AppendAsync(FencingToken token, string stream, ReadOnlyMemory payload, CancellationToken ct); } public sealed class SqlFencedWriter : IFencedWriter { private readonly SqlConnection _conn; public SqlFencedWriter(SqlConnection conn) => _conn = conn; public async Task AppendAsync(FencingToken token, string stream, ReadOnlyMemory payload, CancellationToken ct) { using var cmd = _conn.CreateCommand(); cmd.CommandText = @" DECLARE @current BIGINT = (SELECT ISNULL(MAX(Token), 0) FROM dbo.StreamWrites WITH (UPDLOCK, HOLDLOCK) WHERE StreamId = @stream); IF (@current > @token) THROW 50001, 'Stale fencing token', 1; INSERT INTO dbo.StreamWrites(StreamId, Token, Data) VALUES (@stream, @token, @data); "; cmd.Parameters.AddWithValue("@stream", stream); cmd.Parameters.AddWithValue("@token", token.Value); cmd.Parameters.AddWithValue("@data", payload.ToArray()); await cmd.ExecuteNonQueryAsync(ct); } } ``` Why the locking hints matter: - `UPDLOCK` and `HOLDLOCK` serialize writers for the same stream. - The max-token check and the insert execute as one critical section per stream. - A stale leader gets a hard failure at the database boundary. This is the moment where correctness becomes enforceable, not aspirational. ## Integrating With Leader Election and Leases The integration pattern is straightforward: 1. Acquire the lease. 2. Obtain a new fencing token for this leadership term. 3. Store that token in memory for leader-only components. 4. Include the token on every write. 5. If lease renewal fails, stop leader work and discard the token. A tiny token source can hold the current token. ``` public sealed class InMemoryFencingTokenSource : IFencingTokenSource { private long _value; public void Set(FencingToken token) => Interlocked.Exchange(ref _value, token.Value); public ValueTask CurrentAsync(CancellationToken ct) => ValueTask.FromResult(new FencingToken(Interlocked.Read(ref _value))); } ``` Your leadership loop sets the token after takeover and clears it on leadership loss. ``` public interface IEpochStore { Task NextAsync(string key, CancellationToken ct); } public sealed class LeaderTermCoordinator { private readonly IEpochStore _epoch; private readonly InMemoryFencingTokenSource _tokens; public LeaderTermCoordinator(IEpochStore epoch, InMemoryFencingTokenSource tokens) { _epoch = epoch; _tokens = tokens; } public async Task OnBecameLeaderAsync(string groupKey, CancellationToken ct) { var next = await _epoch.NextAsync($"epoch:{groupKey}", ct); _tokens.Set(new FencingToken(next)); } public void OnLostLeadership() { _tokens.Set(new FencingToken(0)); } } ``` Leader-only code now has to provide a token every time it writes. ``` public sealed class StreamAppender { private readonly IFencingTokenSource _tokens; private readonly IFencedWriter _writer; public StreamAppender(IFencingTokenSource tokens, IFencedWriter writer) { _tokens = tokens; _writer = writer; } public async Task AppendAsync(string stream, byte[] payload, CancellationToken ct) { var token = await _tokens.CurrentAsync(ct); if (token.Value == 0) throw new InvalidOperationException("Not leader"); await _writer.AppendAsync(token, stream, payload, ct); } } ``` The key point is not the in-memory storage. The key point is that every write carries the token and the database enforces monotonic progress. ## Testing Strategy Treat stale token rejection as required behavior, not an edge case. Integration tests to write: 1. Token increases on takeover. 2. Writes with the latest token succeed. 3. Writes with an older token fail with the expected error. 4. Concurrent writes from two tokens result in only the newer token being accepted. A minimal test sketch: ``` using Microsoft.Data.SqlClient; public async Task StaleWriterIsRejected() { var conn = new SqlConnection("Server=.;Database=TestDb;Trusted_Connection=True;Encrypt=False;"); await conn.OpenAsync(); var writer = new SqlFencedWriter(conn); await writer.AppendAsync(new FencingToken(10), "orders", new byte[] { 1, 2, 3 }, CancellationToken.None); try { await writer.AppendAsync(new FencingToken(9), "orders", new byte[] { 9 }, CancellationToken.None); throw new Exception("Expected stale token rejection"); } catch (SqlException ex) when (ex.Number == 50001) { // expected } } ``` Fault injection test worth running: - Acquire lease and token on Node A. - Pause Node A longer than TTL so Node B takes over and gets a higher token. - Resume Node A and attempt a write with the old token. - Verify the database rejects it. That test is your insurance policy against zombie leaders. ## Operational Checklist You want these signals visible. - current leader identity per group - current token per group - rate of stale token rejections - leader changes per hour - SQL latency for fenced writes Alerts to treat seriously: - sustained stale token rejections - rapidly increasing tokens for a group, which indicates flapping - failed renewals correlated with coordinator latency A stale token rejection is not noise. It is proof that the guardrail is catching real risk. ## Common Mistakes - generating tokens locally without atomic increment - checking tokens in application code but not at the database boundary - forgetting to propagate tokens through async paths and background jobs - allowing followers to write without a token - logging stale token errors and continuing as if nothing happened If the storage layer does not reject stale tokens, you do not have fencing. You have a comment. ## Wrap Up and What Comes Next Leases decide who should lead. Fencing tokens decide who is allowed to write. If you already ship leader election and leases, add fencing before you trust the system with money, inventory, or anything that triggers a compliance audit. It is the simplest way to prevent a stalled process from rewriting reality. **Categories:** Patterns **Tags:** .NET, C#, distributed, dotnet, patterns, programming --- ### [Stop Wrestling with JavaScript: htmxRazor Gives ASP.NET Core the Component Library It Deserves](https://www.woodruff.dev/stop-wrestling-with-javascript-htmxrazor-gives-asp-net-core-the-component-library-it-deserves/) **Published:** February 25, 2026 **Author:** Chris Woodruff **Content:** Here is an uncomfortable truth the ASP.NET Core community has been avoiding for too long: server-rendered web development should not require you to adopt React, Vue, or Angular just to get a decent set of UI components. For years, .NET developers have been stuck choosing between two bad options. You can wire up Bootstrap by hand, bolting htmx attributes onto generic HTML and writing the same boilerplate for every project. Or you can adopt Blazor, pulling in a 2 MB WebAssembly runtime to get component abstractions that were designed for a completely different rendering model. Neither path respects the developer who chose server rendering on purpose. That gap is exactly why htmxRazor exists. ## What htmxRazor Actually Is htmxRazor is an open-source UI component library built as ASP.NET Core Tag Helpers. It ships 72 production-ready components across 10 categories: buttons, form controls, dialogs, tabs, navigation, carousels, data visualization, feedback indicators, and more. Every component renders clean, semantic HTML on the server and treats htmx attributes as first-class properties. That last point matters. This is not a generic component library with htmx compatibility tacked on as an afterthought. When you write `hx-get`, `hx-post`, `hx-target`, or `hx-swap` on an htmxRazor component, the Tag Helper understands those attributes and renders them correctly within its own markup structure. Setup takes two lines of code in `Program.cs`: ``` builder.Services.AddhtmxRazor(); app.UsehtmxRazor(); ``` Register the Tag Helpers in `_ViewImports.cshtml`, and you are building with components immediately. No webpack configuration. No npm install. No bundler. One NuGet package. ``` @using htmxRazor.Example @namespace htmxRazor.Example.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, htmxRazor ``` ## The Problem with the Status Quo Think about what happens on a typical ASP.NET Core project that uses htmx today. You start with raw HTML or Bootstrap. You add htmx for interactivity. Then you spend hours wiring up form validation, building accessible dialogs, creating tab components, and making everything work with ASP.NET Core’s model binding. You repeat this work on the next project. And the next. Blazor solves the component problem but introduces its own complexity. You need WebAssembly or SignalR, you lose the simplicity of standard HTTP request/response patterns, and you carry a runtime that dwarfs the size of most applications it serves. For teams that chose Razor Pages or MVC because they wanted a lighter model, Blazor feels like trading one set of problems for another. htmxRazor targets the developers caught in that middle ground: people who like server rendering, who like htmx, and who want real components without the overhead. ## 72 Components, Zero Client-Side Runtime The component catalog covers real application needs across ten categories. **Actions** include buttons with eight variants (brand, success, danger, neutral, and more) plus button groups and dropdowns. **Forms** give you inputs with model binding, textareas, selects, comboboxes, checkboxes, switches, radio groups, sliders, ratings, color pickers, file inputs, and number inputs. All of these integrate with ASP.NET Core’s `ModelExpression` for automatic label generation, type detection, and validation message rendering. **Feedback** components cover callouts, badges, tags, spinners, skeleton loaders, progress bars, progress rings, and tooltips. **Navigation** includes tabs, breadcrumbs, tree views, and carousels. **Overlays** provide dialogs, drawers, and collapsible details panels. Beyond the basics, htmxRazor includes **imagery** components (icons with 43 built-in glyphs, avatars, animated images, before/after comparisons, zoomable frames), **formatting** helpers (bytes, dates, numbers, relative time), **utility** components (copy buttons, QR codes, animations, popups, popovers), and **composite patterns** like active search, infinite scroll, lazy loading, and polling. Every one of these components renders server-side. The total bundle shipped to the browser is CSS plus the htmx script, roughly 14 KB gzipped. Compare that to Blazor’s 2 MB WebAssembly runtime or even Bootstrap’s 24 KB JavaScript plus 22 KB CSS. ## A Real Design System, Not a Bootstrap Wrapper htmxRazor owns its entire visual system. It does not depend on Bootstrap, Tailwind, or any external CSS library. The design system is built on CSS custom properties (design tokens) that control colors, spacing, typography, borders, shadows, and every other visual decision. Components use BEM naming with an `rhx-` prefix, keeping CSS specificity predictable and collisions nonexistent. Theming works through a single attribute on the `` element: ``` ``` Switch to dark mode by changing that value to `"dark"`, or toggle it programmatically with `RHX.toggleTheme()`. Every component responds to the theme change automatically because all styling flows through the token system. You can override any design token with standard CSS: ``` :root { --rhx-color-brand-500: #6366f1; --rhx-radius-md: 0.75rem; } ``` This means htmxRazor adapts to your brand identity without requiring you to fork the source or fight against opinionated defaults. ## Accessibility Is Not an Afterthought Every component ships with semantic HTML, appropriate ARIA attributes, keyboard navigation, and screen reader support. This was a design goal from the start, not something bolted on after the component catalog was complete. Form components generate proper `` associations. Dialogs trap focus. Interactive elements respond to keyboard events. The markup each component produces passes automated accessibility checks. For teams working under WCAG compliance requirements, this saves significant effort compared to building accessible patterns from scratch on every project. ## 1,436 Unit Tests The library includes 1,436 unit tests covering Tag Helper rendering behavior. These tests verify that components produce correct HTML structure, that attributes propagate properly, that model binding generates expected output, and that accessibility markup is present. This is not a weekend experiment or a proof of concept. The test suite reflects the kind of coverage you need before trusting a component library in production applications. ## Who Should Care About This If you are building with ASP.NET Core and you have already chosen (or are considering) htmx for interactivity, htmxRazor removes the boilerplate between your decision and your working UI. If you are evaluating whether to adopt Blazor or stick with Razor Pages, htmxRazor offers a third option: keep the server-rendered model you prefer, get real components, and add interactivity with htmx’s simple attribute-based approach. If you are building internal tools, admin panels, or line-of-business applications where developer productivity matters more than chasing the newest client-side tooling, 72 prebuilt components with model binding and validation integration will save you weeks of work per project. ## Get Started Install from NuGet: ``` dotnet add package htmxRazor ``` See every component with live examples at [htmxRazor.com](https://htmxRazor.com). Browse the source, file issues, or contribute at [github.com/cwoodruff/htmxRazor](https://github.com/cwoodruff/htmxRazor). The project is MIT licensed and accepting contributions. If server-rendered components with htmx integration solve a problem you have been working around, give htmxRazor a look and let the project know what you think. **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, programming, web development --- ### [Lease Pattern in .NET: A Lock With an Expiration Date That Saves Your Data](https://www.woodruff.dev/lease-pattern-in-net-a-lock-with-an-expiration-date-that-saves-your-data/) **Published:** February 7, 2026 **Author:** Chris Woodruff **Content:** Indefinite locks belong to a world where processes never crash and networks never split. That world does not exist. In a distributed system, “I hold the lock” can mean “I held the lock before my VM paused for 45 seconds.” A lease fixes that by putting a deadline on ownership and forcing the owner to keep renewing that claim. A lease is a lock with a time limit. When the clock runs out, other nodes may take over. That single detail prevents stuck work after crashes and limits the damage during partitions. This post shows a production shaped lease using Redis with atomic acquire, atomic renew, and a rule that most teams avoid until it hurts: when renewal fails, you stop acting like the owner. ## The problem with indefinite locks Classic locking assumes three things that distributed systems break daily. - The lock holder can always release the lock. - Everyone can observe the release. - Time does not jump. None of those are reliable. Processes crash and never execute finally blocks. Networks drop packets and hide releases. Pause events stretch “a second” into “a minute.” If your system depends on an indefinite lock, it will either freeze forever or run twice. A lease does not make failure disappear. It makes failure survivable. ## Pattern definition and intent A lease is exclusive ownership that expires without renewal. Intent: Provide exclusive access with a bounded duration so the system can recover when an owner crashes or becomes unreachable. Key property: Ownership is valid only until ExpiresAt, not until the owner feels like releasing it. ## Core semantics and hard rules A lease implementation needs three invariants. Acquire is atomic and exclusive. Renew is atomic and only the current owner can extend the deadline. Loss of renew ends ownership immediately. That last point is the hard rule. If you cannot renew, you are not the owner. Continuing to act after renewal failure is how “singleton” jobs become duplicate work. ## Design choices that matter ### TTL selection TTL is a trade. Short TTL gives faster recovery and more churn. Long TTL gives slower recovery and less churn. A common starting point is 10 seconds, then adjust after measuring Redis latency, GC pauses, and deployment behavior. ### Renew cadence Renew earlier than TTL. Many teams renew at one third of TTL with jitter. The jitter avoids every node renewing on the same millisecond, which can create coordinator spikes and election flapping. ### Time model Let Redis own the timer. Redis TTL is enforced server side. Do not build correctness on local clocks. ### Ownership token Use a unique token per process instance, stored as the lease value. Renew and release must verify the token. Without this, one node can renew a lease it does not own. ## Redis as the lease store Redis provides the primitives you need. Acquire: SET key token NX PX ttlMillis Renew: Atomic compare token then extend TTL, implemented with Lua Release: Atomic compare token then delete, implemented with Lua ### Acquire ``` using StackExchange.Redis; public sealed class RedisLeaseStore { private readonly IDatabase _db; public RedisLeaseStore(IConnectionMultiplexer mux) => _db = mux.GetDatabase(); public async Task TryAcquireAsync(string key, string token, TimeSpan ttl, CancellationToken ct) { // StackExchange.Redis does not accept CancellationToken on all calls, so keep the operation small. return await _db.StringSetAsync( key: key, value: token, expiry: ttl, when: When.NotExists); } } ``` ### Renew with Lua Renew must be compare and extend in one operation. A read then write is not safe because another node could acquire between those steps. Lua script: - If the stored value matches the token, extend TTL and return 1 - Else return 0 ``` public sealed partial class RedisLeaseStore { private static readonly LuaScript RenewScript = LuaScript.Prepare(@" if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end "); public async Task TryRenewAsync(string key, string token, TimeSpan ttl, CancellationToken ct) { var result = (long)await _db.ScriptEvaluateAsync( RenewScript, new RedisKey[] { key }, new RedisValue[] { token, (long)ttl.TotalMilliseconds }); return result == 1; } } ``` ### Release with Lua Release is optional for safety, but useful for fast handoff during graceful shutdown. It must be token checked to avoid deleting another node’s lease. ``` public sealed partial class RedisLeaseStore { private static readonly LuaScript ReleaseScript = LuaScript.Prepare(@" if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end "); public async Task ReleaseAsync(string key, string token, CancellationToken ct) { var result = (long)await _db.ScriptEvaluateAsync( ReleaseScript, new RedisKey[] { key }, new RedisValue[] { token }); return result == 1; } } ``` ## A lease abstraction you can use everywhere Wrap the store behind a small interface. Keep it focused on behavior, not plumbing. ``` public interface ILease { string Key { get; } string Token { get; } TimeSpan Ttl { get; } Task AcquireAsync(CancellationToken ct); Task RenewAsync(CancellationToken ct); Task ReleaseAsync(CancellationToken ct); } ``` Implementation that uses the Redis store: ``` public sealed class RedisLease(RedisLeaseStore store, string key, TimeSpan ttl, string? token = null) : ILease { public string Key { get; } = key; public string Token { get; } = token ?? Guid.NewGuid().ToString("N"); public TimeSpan Ttl { get; } = ttl; public Task AcquireAsync(CancellationToken ct) => store.TryAcquireAsync(Key, Token, Ttl, ct); public Task RenewAsync(CancellationToken ct) => store.TryRenewAsync(Key, Token, Ttl, ct); public async Task ReleaseAsync(CancellationToken ct) { await store.ReleaseAsync(Key, Token, ct); } } ``` ## Using the lease for leader only work A lease does nothing until you build your control flow around it. The safest approach is a background loop that: - tries to acquire - renews on a schedule - cancels leader work immediately when renew fails ### Leader work gate ``` public sealed class LeaderOnlyService { private volatile bool _isLeader; public bool IsLeader => _isLeader; public async Task RunIfLeaderAsync(Func work, CancellationToken ct) { if (!_isLeader) return; await work(ct); } internal void SetLeader(bool value) => _isLeader = value; } ``` ### Lease driven leader loop ``` using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; public sealed class LeaseLeaderLoop : BackgroundService { private readonly ILease _lease; private readonly LeaderOnlyService _leaderOnly; private readonly ILogger _log; private readonly TimeSpan _renewEvery; private readonly Random _rng = new(); public LeaseLeaderLoop(ILease lease, LeaderOnlyService leaderOnly, ILogger log) { _lease = lease; _leaderOnly = leaderOnly; _log = log; _renewEvery = TimeSpan.FromMilliseconds(_lease.Ttl.TotalMilliseconds / 3); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { if (!_leaderOnly.IsLeader) { var acquired = await _lease.AcquireAsync(stoppingToken); if (acquired) { _leaderOnly.SetLeader(true); _log.LogInformation("Lease acquired for {Key}", _lease.Key); } } else { var renewed = await _lease.RenewAsync(stoppingToken); if (!renewed) { _leaderOnly.SetLeader(false); _log.LogWarning("Lease lost for {Key}", _lease.Key); } } } catch (Exception ex) { _leaderOnly.SetLeader(false); _log.LogError(ex, "Lease loop error for {Key}", _lease.Key); } var jitterMs = _rng.Next(0, 250); await Task.Delay(_renewEvery + TimeSpan.FromMilliseconds(jitterMs), stoppingToken); } } public override async Task StopAsync(CancellationToken cancellationToken) { _leaderOnly.SetLeader(false); await _lease.ReleaseAsync(cancellationToken); await base.StopAsync(cancellationToken); } } ``` ### Example: singleton outbox dispatcher ``` public sealed class OutboxDispatcher(LeaderOnlyService leaderOnly) { public Task TickAsync(CancellationToken ct) => leaderOnly.RunIfLeaderAsync(async innerCt => { // Fetch pending messages, publish them, mark sent. await Task.CompletedTask; }, ct); } ``` Leader loss is immediate because RunIfLeaderAsync checks a volatile flag. Your leader work still needs to be cancellable and to check cancellation regularly. ## Failure stories the lease prevents ### Crash and stuck lock With an indefinite lock, a crash can freeze the work forever. With a lease, the lease expires and another node can acquire it. ### Pause and stale owner A node can pause longer than TTL. When it wakes, it might still believe it owns the work. With proper renew checks, it fails renew and stops. That avoids duplicate work. If the paused node keeps writing anyway, you need fencing tokens at the write boundary. Leases reduce risk, they do not block every stale write by themselves. ### Partition A partition can isolate the leader from Redis. Renew fails. The node relinquishes leadership. Another node on the healthy side acquires the lease after expiry. If the isolated node keeps acting, that is a code bug. The lease pattern gives you a clear rule to enforce. ## Testing strategy Integration tests against Redis are worth the time. Tests to include: - only one node can acquire the lease at a time - renew succeeds for the token holder - renew fails after expiry - release deletes only when token matches - simulated pause longer than TTL results in lease loss Sketch of a basic acquisition test: ``` public async Task OnlyOneOwnerGetsTheLease() { var store = new RedisLeaseStore(ConnectionMultiplexer.Connect("localhost:6379")); var ttl = TimeSpan.FromSeconds(5); var a = new RedisLease(store, "lease:group-a", ttl, token: "A"); var b = new RedisLease(store, "lease:group-a", ttl, token: "B"); var gotA = await a.AcquireAsync(CancellationToken.None); var gotB = await b.AcquireAsync(CancellationToken.None); if (gotA == gotB) throw new Exception("Expected exclusive acquisition"); } ``` Add a pause test by acquiring, waiting past TTL without renewing, then acquiring from another lease. ## Operational checklist Metrics: - acquire success rate - renew success rate - lease loss events - contention rate per key - Redis latency for SET and EVAL Alerts: - repeated lease loss for the same key - frequent leader changes for a key - renew failures correlated with Redis latency spikes Logs: - key, token prefix, acquire vs renew, latency, and exception details ## Common mistakes - using a lock library without understanding token checks - renewing without compare and extend - treating release as the safety mechanism - ignoring renew failures and continuing work - TTL set without measuring coordinator latency - assuming leases prevent stale writes without fencing ## Wrap up A lease is a lock with a deadline. It is safer because it admits reality: owners crash and networks split. Implement atomic acquire, atomic renew, and token checked release. Then enforce the hard rule. If you cannot renew, you stop acting like the owner. Next up is fencing tokens. A lease determines who should lead. Fencing determines who is allowed to write. **Categories:** Patterns **Tags:** .NET, C#, distributed, dotnet, patterns, programming --- ### [Leader Election in .NET: Picking One Boss Without Creating Two](https://www.woodruff.dev/leader-election-in-net-picking-one-boss-without-creating-two/) **Published:** February 4, 2026 **Author:** Chris Woodruff **Content:** If your service runs on more than one node and still has a single instance assumption, you already have leader election. You just do not have it on purpose. Leader election is the pattern that turns “somebody should run this” into “exactly one node is allowed to run this, and it must keep proving it deserves the role.” This post walks through a lease based leader election you can implement in C# with a durable coordinator, a renewal loop, backoff, and the checks that keep your cluster from producing two leaders on a bad day. ## The real problem: accidental multi leader Most teams meet multi leader behavior during a routine event: - a scale out from one instance to two - a rolling deployment - a node pause caused by GC or a noisy neighbor - a partition that leaves both sides alive and confident The symptoms are predictable: - a scheduled job runs twice - a command handler applies the same side effect twice - two writers push conflicting updates - retries amplify the blast radius When you hear “but we only run one instance of that worker,” treat it as a confession, not a design. ## Intent and mental model Leader election selects one leader for a group and replaces it quickly when the leader disappears. The mental model is simple: - leadership is a lease with an expiry timestamp - a leader must renew before expiry - if renewal fails, the node stops acting as leader immediately - followers keep trying to acquire the lease with backoff and jitter A lease is not a lock you hold forever. It is a claim you must keep earning. ## The lease store contract The coordinator can be SQL Server, Redis, etcd, or any durable store that can do an atomic acquire. The interface stays small. ``` public interface ILeaseStore { Task TryAcquireAsync(string key, string owner, TimeSpan ttl, CancellationToken ct); Task TryRenewAsync(string key, string owner, TimeSpan ttl, CancellationToken ct); Task ReadAsync(string key, CancellationToken ct); } ``` What this contract implies: - Acquire must be exclusive for a key while the lease is valid. - Renew must succeed only for the current owner while the lease is still valid. - Read is for diagnostics, dashboards, and tests. ## Design choices that matter A handful of knobs decide whether your election is calm or chaotic. **TTL** Short TTL yields faster failover and more churn. Long TTL yields slower failover and fewer elections. A reasonable starting point is 10 seconds. **Renew interval** Renew at one third of TTL plus small jitter. Renewing at half TTL becomes risky once you include latency spikes. **Backoff and jitter** When a leader dies, every follower will try to acquire. Without backoff and jitter, they slam your coordinator and can trigger flapping. **Coordinator outage behavior** A coordinator outage is a correctness event, not only an availability event. When renew fails, relinquish leadership. The alternative is two leaders writing through different network paths. **Key scope** Use one key per group or shard. A single key for the whole cluster works for singleton jobs, not for partitioned workloads. ## The election loop Each node runs the same loop: - If not leader, attempt acquire. - If leader, attempt renew. - If renew fails, drop leadership and stop leader only work. That is the whole state machine. The hard part is honoring it in every code path. ## A concrete .NET implementation We will build three pieces: - a lease store backed by SQL Server - a leadership service that runs the loop - a leader only gate for work ### SQL lease store Schema: ``` CREATE TABLE dbo.Leases ( LeaseKey nvarchar(200) NOT NULL PRIMARY KEY, OwnerId nvarchar(200) NOT NULL, ExpiresAtUtc datetime2(3) NOT NULL ); ``` Acquire and renew are conditional updates based on expiry and owner. ``` using Microsoft.Data.SqlClient; public sealed class SqlLeaseStore(string connectionString) : ILeaseStore { public async Task TryAcquireAsync(string key, string owner, TimeSpan ttl, CancellationToken ct) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); var now = DateTimeOffset.UtcNow; var expires = now.Add(ttl); // Acquire if missing or expired. await using var cmd = conn.CreateCommand(); cmd.CommandText = @" MERGE dbo.Leases WITH (HOLDLOCK) AS t USING (SELECT @k AS LeaseKey) AS s ON (t.LeaseKey = s.LeaseKey) WHEN NOT MATCHED THEN INSERT (LeaseKey, OwnerId, ExpiresAtUtc) VALUES (@k, @o, @e) WHEN MATCHED AND t.ExpiresAtUtc @now; "; cmd.Parameters.AddWithValue("@k", key); cmd.Parameters.AddWithValue("@o", owner); cmd.Parameters.AddWithValue("@e", expires); cmd.Parameters.AddWithValue("@now", now); return await cmd.ExecuteNonQueryAsync(ct) == 1; } public async Task ReadAsync(string key, CancellationToken ct) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); await using var cmd = conn.CreateCommand(); cmd.CommandText = @" SELECT OwnerId, ExpiresAtUtc FROM dbo.Leases WHERE LeaseKey = @k; "; cmd.Parameters.AddWithValue("@k", key); await using var r = await cmd.ExecuteReaderAsync(ct); if (!await r.ReadAsync(ct)) return (null, DateTimeOffset.MinValue); return (r.GetString(0), r.GetDateTimeOffset(1)); } } ``` ### Leadership service with renewal loop This service implements a tiny state machine and exposes a leader flag and current lease expiry. ``` using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; public interface ILeadership { ValueTask IsLeaderAsync(CancellationToken ct); ValueTask ExpiresAtAsync(CancellationToken ct); } public sealed class LeaseLeadership( ILeaseStore store, ILogger log, string key, string owner, TimeSpan ttl) : BackgroundService, ILeadership { private readonly ILeaseStore _store = store; private volatile bool _leader; private DateTimeOffset _expiresAt; public ValueTask IsLeaderAsync(CancellationToken ct) => ValueTask.FromResult(_leader); public ValueTask ExpiresAtAsync(CancellationToken ct) => ValueTask.FromResult(_expiresAt); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var rng = Random.Shared; while (!stoppingToken.IsCancellationRequested) { try { if (!_leader) { var acquired = await _store.TryAcquireAsync(key, owner, ttl, stoppingToken); if (acquired) { _leader = true; _expiresAt = DateTimeOffset.UtcNow.Add(ttl); log.LogInformation("Leader acquired for {Key} by {Owner}", key, owner); } else { _leader = false; } } else { var renewed = await _store.TryRenewAsync(key, owner, ttl, stoppingToken); if (!renewed) { _leader = false; log.LogWarning("Leader lost for {Key} by {Owner}", key, owner); } else { _expiresAt = DateTimeOffset.UtcNow.Add(ttl); } } } catch (Exception ex) { _leader = false; log.LogError(ex, "Leadership loop error for {Key}", key); } var baseDelay = TimeSpan.FromMilliseconds(ttl.TotalMilliseconds / 3); var jitter = TimeSpan.FromMilliseconds(rng.Next(0, 250)); await Task.Delay(baseDelay + jitter, stoppingToken); } } } ``` This loop is opinionated: if renew fails, leadership ends. That choice prevents a coordinator outage from turning into a split brain event. ### Leader only work gate Once you have ILeadership, guard leader only work in one place. ``` public sealed class LeaderOnlyService(ILeadership leadership) { private readonly ILeadership _leadership = leadership; public async Task RunAsync(Func work, CancellationToken ct) { if (!await _leadership.IsLeaderAsync(ct)) return; await work(ct); } } ``` Use it for outbox dispatchers, schedulers, and partition owners. ``` public sealed class OutboxDispatcher(LeaderOnlyService leaderOnly) { private readonly LeaderOnlyService _leaderOnly = leaderOnly; public Task TickAsync(CancellationToken ct) => _leaderOnly.RunAsync(async innerCt => { // Read pending outbox rows, publish, mark sent. }, ct); } ``` ## Observability and operational signals Leader election is a control plane. Treat it like production code with production signals. Metrics: - leader status per key - renew failures per minute - election wins per hour - time until expiry for the current lease - coordinator latency Alerts: - rapid flapping on a key - renew failures across all nodes - repeated acquire failures with high coordinator latency Logs worth keeping: - key, owner, action (acquire, renew, lose), and latency ## Testing plan Integration tests that pay for themselves: - Start two nodes, assert only one becomes leader. - Stop the leader, wait for TTL, assert the follower becomes leader. - Introduce renewal failures by forcing command timeouts, assert the leader steps down. - Pause the leader process long enough to miss renew, assert takeover happens after expiry. A good property to assert: No two nodes report leadership for the same key while the lease is valid. ## Common mistakes - Calling it leader election while using an in memory lock - Doing long leader work that keeps running after leadership loss - Skipping jitter and creating synchronized acquire storms - Choosing TTL without measuring coordinator latency - Treating coordinator failure as permission to keep acting like leader ## Wrap up Leader election is a correctness feature. It exists to prevent your system from producing competing decisions. Use a lease, renew it often, relinquish on failure, and test the takeover path until it is boring. Next up is Lease and then Fencing Token. Election decides who is leader. Fencing decides whether an old leader can still write after it loses the role. **Categories:** Patterns **Tags:** .NET, C#, distributed, dotnet, patterns, programming --- ### [Distributed System Pattern: Leader and Followers in .NET - One Decision Maker, Many Replicas, Fewer Outages](https://www.woodruff.dev/distributed-system-pattern-leader-and-followers-in-net-one-decision-maker-many-replicas-fewer-outages/) **Published:** January 29, 2026 **Author:** Chris Woodruff **Content:** Distributed systems rarely fail because you picked the wrong cloud service. They fail because two nodes believe they are in charge, both act, and both are “correct” from their own perspective. If your domain has any single authority assumption, and most systems do, you need a way to make that authority real. Leader and Followers is the pattern that turns vague ownership into a concrete contract. One node is the decision maker for a shard, group, or partition. The rest replicate the leader’s decisions and keep the system available when nodes fail. That is the bargain. The cost is that you must implement leadership as a first-class concept, not an incidental side effect of “whichever instance got there first.” This post shows a pragmatic .NET implementation using a renewable lease plus a leadership term. You will also see how to fence writes so an old leader cannot corrupt your data after it loses leadership. ## The Pattern in One Sentence Choose one leader per group to serialize decisions, replicate the resulting state to followers, and make leadership transferable without letting two nodes write as leader at the same time. ## The Failure You Are Trying to Stop If you have ever seen any of these, you have already paid for this pattern, just in a worse way: - Two schedulers run the same job and you double charge a customer. - Two instances process the same command and your inventory goes negative. - A node stalls for 30 seconds, then resumes and continues writing as if nothing happened. - You “fixed” it with retries and now the bug happens faster. Leader and Followers prevents these by making leadership explicit and enforceable. ## Implementation Goals A solid implementation needs four properties: 1. Exclusive leadership per group with a bounded time window. 2. Fast detection of leadership loss. 3. A monotonic term that changes on each leadership transition. 4. Fencing so stale leaders are rejected at the storage boundary. The lease gives you the bounded time window. The term and fencing prevent the stale leader problem. ## Core Interface and the Leader Only Boundary Start by keeping your public surface area honest. Code that must run only on the leader should not be callable without an explicit leadership check. ``` public interface ILeadership { ValueTask IsLeaderAsync(CancellationToken ct); ValueTask TermAsync(CancellationToken ct); } ``` Now wrap leader-only work behind a small, boring gate: ``` public sealed class LeaderOnlyService(ILeadership leadership) { private readonly ILeadership _leadership = leadership; public async Task RunLeaderTaskAsync(Func work, CancellationToken ct) { if (!await _leadership.IsLeaderAsync(ct)) return; await work(ct); } } ``` This does not solve leadership by itself. It prevents “accidental leadership” from leaking everywhere. ## Step 1: Lease Backed Leadership in SQL Server You can implement leases using Redis, etcd, ZooKeeper, or a database. For many .NET teams, SQL Server is already present and operationally understood. A SQL lease is not glamorous, but it is effective. Create a table that stores the current lease owner, expiry, and term. ``` CREATE TABLE dbo.LeadershipLeases ( LeaseKey nvarchar(200) NOT NULL PRIMARY KEY, OwnerId nvarchar(200) NOT NULL, Term bigint NOT NULL, ExpiresAtUtc datetime2(3) NOT NULL ); CREATE INDEX IX_LeadershipLeases_ExpiresAtUtc ON dbo.LeadershipLeases (ExpiresAtUtc); ``` Define the lease rules: - A node can acquire leadership if the lease is expired or already owned by itself. - Each successful acquisition increments the term. - Renew extends ExpiresAtUtc only if the owner matches and the lease has not expired. ### Lease Store Contract ``` public interface ILeaseStore { Task TryAcquireAsync( string leaseKey, string ownerId, TimeSpan ttl, CancellationToken ct); Task TryRenewAsync( string leaseKey, string ownerId, TimeSpan ttl, CancellationToken ct); Task ReadAsync( string leaseKey, CancellationToken ct); } ``` ### SQL Implementation This implementation uses a transaction and locking hints to keep acquisition atomic. ``` using System.Data; using Microsoft.Data.SqlClient; public sealed class SqlLeaseStore(string connectionString) : ILeaseStore { public async Task TryAcquireAsync( string leaseKey, string ownerId, TimeSpan ttl, CancellationToken ct) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); await using var tx = (SqlTransaction)await conn.BeginTransactionAsync(IsolationLevel.Serializable, ct); var now = DateTimeOffset.UtcNow; var expires = now.Add(ttl); // Lock the row for the lease key. await using var select = conn.CreateCommand(); select.Transaction = tx; select.CommandText = @" SELECT LeaseKey, OwnerId, Term, ExpiresAtUtc FROM dbo.LeadershipLeases WITH (UPDLOCK, HOLDLOCK) WHERE LeaseKey = @k; "; select.Parameters.AddWithValue("@k", leaseKey); string? currentOwner = null; long currentTerm = 0; DateTimeOffset currentExpires = DateTimeOffset.MinValue; await using (var reader = await select.ExecuteReaderAsync(ct)) { if (await reader.ReadAsync(ct)) { currentOwner = reader.GetString(1); currentTerm = reader.GetInt64(2); currentExpires = reader.GetDateTimeOffset(3); } } var isExpired = currentOwner is null || currentExpires ValueTask.FromResult(_isLeader); public ValueTask TermAsync(CancellationToken ct) => ValueTask.FromResult(_term); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { if (!_isLeader) { var acquired = await _store.TryAcquireAsync(_leaseKey, _ownerId, _ttl, stoppingToken); _isLeader = acquired.Acquired; _term = acquired.Term; if (_isLeader) _log.LogInformation("Became leader for {LeaseKey} with term {Term}", _leaseKey, _term); } else { var renewed = await _store.TryRenewAsync(_leaseKey, _ownerId, _ttl, stoppingToken); if (!renewed.Renewed) { _isLeader = false; _log.LogWarning("Lost leadership for {LeaseKey}", _leaseKey); } } } catch (Exception ex) { _isLeader = false; _log.LogError(ex, "Leadership loop error for {LeaseKey}", _leaseKey); } await Task.Delay(_renewEvery, stoppingToken); } } } ``` Important behavior: if renewal fails, the node stops acting as a leader immediately. That is non-negotiable. ## Step 3: Fencing Writes With the Term A renewable lease is necessary but not sufficient. A node can pause, GC can stall, networking can get weird, and you can end up with a stale leader still connected to your database. The lease can expire and a new leader can be elected while the old leader remains alive and unaware. The fix is fencing: every leader term is a token. Every write from the leader includes it. Storage rejects writes with a stale term. ### Example: Fenced Stream Writes in SQL Server Create a table for writes that includes the term, and enforce monotonic terms per stream or per aggregate. ``` CREATE TABLE dbo.StreamWrites ( StreamId nvarchar(200) NOT NULL, SequenceNo bigint NOT NULL, Term bigint NOT NULL, Payload varbinary(max) NOT NULL, CreatedAtUtc datetime2(3) NOT NULL, CONSTRAINT PK_StreamWrites PRIMARY KEY(StreamId, SequenceNo) ); CREATE INDEX IX_StreamWrites_StreamId_Term ON dbo.StreamWrites(StreamId, Term); ``` Now a fenced writer that rejects stale terms: ``` using Microsoft.Data.SqlClient; public sealed class FencedStreamWriter(string connectionString) { public async Task AppendAsync(string streamId, long sequenceNo, long term, byte[] payload, CancellationToken ct) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); await using var tx = (SqlTransaction)await conn.BeginTransactionAsync(ct); // Reject if the stream has any write with a greater term. await using var check = conn.CreateCommand(); check.Transaction = tx; check.CommandText = @" DECLARE @maxTerm BIGINT = ( SELECT ISNULL(MAX(Term), 0) FROM dbo.StreamWrites WITH (UPDLOCK, HOLDLOCK) WHERE StreamId = @s ); IF (@maxTerm > @t) THROW 50001, 'Stale leader term', 1; "; check.Parameters.AddWithValue("@s", streamId); check.Parameters.AddWithValue("@t", term); await check.ExecuteNonQueryAsync(ct); await using var insert = conn.CreateCommand(); insert.Transaction = tx; insert.CommandText = @" INSERT INTO dbo.StreamWrites(StreamId, SequenceNo, Term, Payload, CreatedAtUtc) VALUES (@s, @n, @t, @p, SYSUTCDATETIME()); "; insert.Parameters.AddWithValue("@s", streamId); insert.Parameters.AddWithValue("@n", sequenceNo); insert.Parameters.AddWithValue("@t", term); insert.Parameters.AddWithValue("@p", payload); await insert.ExecuteNonQueryAsync(ct); await tx.CommitAsync(ct); } } ``` This is the storage level refusal that prevents corruption. If you do not fence, you are trusting timing and luck. ## Putting It Together: A Leader Only Job Runner Here is a practical use case: one scheduled job should run per cluster, and it must not run twice. ``` public sealed class BillingReconciler(LeaderOnlyService leaderOnly, ILeadership leadership, FencedStreamWriter writer) { private readonly LeaderOnlyService _leaderOnly = leaderOnly; private readonly ILeadership _leadership = leadership; private readonly FencedStreamWriter _writer = writer; public Task RunAsync(CancellationToken ct) => _leaderOnly.RunLeaderTaskAsync(async innerCt => { var term = await _leadership.TermAsync(innerCt); // Example decision: emit a reconciliation command into a stream var streamId = "billing-reconcile"; var nextSeq = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); // placeholder, use real sequencing in production var payload = System.Text.Encoding.UTF8.GetBytes($"reconcile:{DateTimeOffset.UtcNow:O}"); await _writer.AppendAsync(streamId, nextSeq, term, payload, innerCt); }, ct); } ``` If a stale leader tries to write, the fenced writer throws, and the damage stops there. ## Replicating State to Followers Leader and Followers does not require a specific replication mechanism. It requires that the leader’s decisions can be observed and applied by followers. Here are three pragmatic replication models in .NET, in increasing sophistication: ### Model A: Shared durable state, followers read it Leader writes to a database. Followers serve reads from the same database or compute projections from it. Replication is “free” because the database is the replication point. This is often the right starting point. It is also a reminder: your database is part of the distributed system, whether you want it to be or not. ### Model B: Leader appends to a log, followers tail the log Leader writes decisions into an append only log table. Followers poll for new rows and apply them to a local projection. ``` public sealed class FollowerApplier(string connectionString) : BackgroundService { private long _lastSeq; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var batch = await ReadNextBatchAsync(_lastSeq, stoppingToken); foreach (var row in batch) { Apply(row); _lastSeq = row.SequenceNo; } await Task.Delay(TimeSpan.FromMilliseconds(200), stoppingToken); } } private async Task ReadNextBatchAsync( long afterSeq, CancellationToken ct) { var results = new List(); await using var conn = new Microsoft.Data.SqlClient.SqlConnection(connectionString); await conn.OpenAsync(ct); await using var cmd = conn.CreateCommand(); cmd.CommandText = @" SELECT TOP (100) StreamId, SequenceNo, Term, Payload FROM dbo.StreamWrites WHERE SequenceNo > @n ORDER BY SequenceNo; "; cmd.Parameters.AddWithValue("@n", afterSeq); await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { results.Add(( reader.GetString(0), reader.GetInt64(1), reader.GetInt64(2), (byte[])reader["Payload"] )); } return results; } private static void Apply((string StreamId, long SequenceNo, long Term, byte[] Payload) row) { // Apply to a local projection, cache, or in-memory state machine. // Keep it deterministic and idempotent. } } ``` ### Model C: Leader publishes events, followers subscribe This is the same idea as Model B but moved to a message broker. It works when ordering per partition is guaranteed and consumers track offsets. If you already use Azure Service Bus, Kafka, or similar, this model can scale well. ## Reads and Correctness Followers can be used for reads, but only if you are honest about staleness. Common approach: - Writes go to the leader. - Reads go to followers if their replication lag is within a threshold. - Otherwise, route reads to the leader for correctness. This is the point where teams get sloppy. “Eventually consistent” is not a license to guess. ## Testing: Prove You Do Not Get Two Leaders You can integration test this without heroic infrastructure. 1. Start two instances with the same lease key and different owner ids. 2. Verify only one becomes leader. 3. Pause the leader process long enough to miss renewals. 4. Verify the follower becomes leader with a higher term. 5. Resume the old leader and attempt a fenced write. 6. Verify the write is rejected as stale. If your tests do not include stale leader rejection, you do not have a safety story. ## Operational Checklist Metrics you want on a dashboard: - IsLeader boolean per instance - Current term per instance - Lease renew latency and failures - Leadership flaps per hour - Fenced write rejections per hour - Follower lag if you implement tailing replication Alerts worth paging on: - Leadership flapping - Lease renew failures across many nodes - Any sustained rate of stale leader write rejections Those rejections are not “noise.” They are proof that you avoided corruption today. ## Closing Leader and Followers is not a fancy pattern. It is a statement of responsibility. One node decides. Everyone else follows. Leadership changes are explicit, bounded, and fenced. If you build a system that assumes a single decision maker but never implements one, you are not simplifying the architecture. You are outsourcing correctness to chance. If you want to extend this post next, the natural follow on is Fencing Tokens and Generation Clock as a deeper treatment of terms, plus a replication chapter that builds a small replicated log in .NET with a committed index and follower acknowledgements. **Categories:** Patterns **Tags:** .NET, C#, distributed, dotnet, patterns, programming --- ### [Systems Thinking Meets Simplicity-First: A Decision Framework for Software Architects](https://www.woodruff.dev/systems-thinking-meets-simplicity-first-a-decision-framework-for-software-architects/) **Published:** January 23, 2026 **Author:** Chris Woodruff **Content:** Modern technology operates in a paradox. Our tools have never been more powerful, yet our systems have never felt more fragile. Every framework, pipeline, and process claims to simplify development, but most end up multiplying dependencies and eroding clarity. In that chaos, two guiding philosophies emerge: Systems Thinking and Simplicity-First. At first glance, they seem to occupy opposite ends of the spectrum. Systems Thinking invites us to embrace complexity, to see the world as an intricate web of interrelated parts. Simplicity-First urges us to strip away complexity, to build only what’s essential and resist the cult of cleverness. But these philosophies are not opposites. They are complements. Together, they form a balanced framework for understanding complexity without surrendering to it, enabling the building of coherent, sustainable, and humane systems. ## Seeing the Whole: A Shared Rejection of Reductionism Both Systems Thinking and Simplicity-First reject reductionism, the tendency to break problems into isolated parts and optimize them independently. Systems Thinking emerged from fields like ecology, cybernetics, and organizational theory as a response to linear cause-and-effect thinking. It views every problem as part of a larger system, where changes create ripples through feedback loops and dependencies. A system cannot be understood solely by examining its components; it must be understood in terms of how those components interact. Simplicity-First, while rooted in software development, embraces a similar holistic vision. It argues that simplicity is not achieved by simplifying individual functions or microservices, but by simplifying the relationships between them. True simplicity lies in cohesion, in how well the parts of a system fit together and communicate with one another. These two philosophies converge in their insistence that context matters. An elegant algorithm or efficient service means nothing if it introduces friction or opacity into the broader ecosystem. The overall health of the system is more important than the cleverness of its individual parts. ## Complexity: To Manage or To Remove? Here, the two philosophies reveal their distinct but complementary strengths. Systems Thinking recognizes complexity as a fundamental aspect of living systems. Rather than viewing it as a challenge to be eradicated, Systems Thinking seeks to manage complexity by deepening understanding of information flows, energy, and feedback that influence behavior over time. From this perspective, complexity is not a drawback but a potential source of adaptability and resilience. Simplicity-First asks the question most architects avoid: *does this complexity actually need to exist?* It draws a hard line between essential and accidental complexity, as outlined in Fred Brooks’ influential essay “[No Silver Bullet](https://en.wikipedia.org/wiki/No_Silver_Bullet).” Essential complexity is inherent to the domain we’re modeling. Accidental complexity arises from design missteps, over-engineering, and needless abstractions. The practice of Simplicity-First involves ruthlessly identifying which elements of a system are intrinsic and which are imposed. Systems Thinking provides the analytical tools to trace interconnections; Simplicity-First supplies the discipline to cut what shouldn’t be there. ## Case Study: The Service Extraction Decision Theory becomes meaningful only when it changes decisions. Consider a scenario I encounter regularly: a team wants to extract a “Notification Service” from their modular monolith. The feature handles email, SMS, and push notifications. It has clear boundaries. It seems like an obvious candidate for extraction. Let’s apply both frameworks to this decision. ### The Systems Thinking Lens First, we map the dependencies. The notification module touches user preferences, order events, marketing campaigns, and authentication flows. It reads from a shared database and publishes to a message queue that three other modules consume. Systems Thinking reveals the feedback loops: when notifications fail, customer support tickets increase, triggering escalation emails that depend on… the notification system. We see that this “isolated” module is actually a hub in our system’s information flow. ### The Simplicity-First Lens Now we apply the 2 AM Test: if this service fails at 2 AM, can an on-call engineer understand and fix it without the original authors? A separate notification service means maintaining a deployment pipeline, a separate monitoring stack, network partitioning scenarios, and distributed transaction semantics. The engineer must now reason about two systems instead of one. The Half-Rule asks: can we solve this problem with half the components? The monolith already handles notifications. What problem are we actually solving by extracting it? ### The Decision In most cases, the answer is: keep it in the monolith. The “cleanliness” of a separate service is accidental complexity masquerading as good architecture. Systems Thinking showed us the hidden dependencies; Simplicity-First gave us permission to reject the extraction. The exception proves the rule: if the notification system genuinely needs independent scaling (millions of messages per hour), or if a separate team will own it with clear API contracts, extraction may be justified. But that decision should be driven by *measured pressure*, not architectural fashion. ## Conceptual Comparison **Theme****Systems Thinking****Simplicity-First****Intersection**View of the WorldSees reality as a web of interconnected systems where every part influences the whole.Views software, teams, and organizations as systems that must stay understandable and cohesive.Both reject reductionism and silos, emphasizing context and interdependence.Relationship with ComplexityAccepts complexity as intrinsic to living systems and seeks to manage it.Distinguishes essential from accidental complexity, aiming to eliminate the latter.Together they form a balance: understand what must stay complex, simplify what need not be.Goal OrientationStrives for systemic balance, resilience, and sustainability.Strives for clarity, maintainability, and sustainability through reduction of waste.Both optimize for long-term stability over short-term speed.Change & AdaptationEncourages continuous feedback and learning to adapt the system.Embraces iterative simplification: refactoring, pruning, and redesigning based on feedback.Both view improvement as an ongoing loop rather than a one-time event.Human RoleRecognizes human limits and cognitive biases within systems.Designs within human cognitive capacity: favoring understandable over clever.Both center the human as the system’s most fragile but essential component.Ethical FoundationPromotes responsibility for systemic consequences and unintended effects.Advocates for ethical restraint: building only what adds value, avoiding wasteful over-engineering.Both align with sustainable and humane technology practices.## Practical Application **Dimension****Systems Thinking in Practice****Simplicity-First in Practice****Synergistic Outcome**ArchitectureMaps dependencies, flows, and feedback loops between services, teams, and stakeholders.Consolidates unnecessary layers, favors modular monoliths, and limits external coupling.You understand why systems become tangled, and know how to untangle them.Decision-MakingEvaluates impact of decisions across the entire system.Uses the Half-Rule and 2 AM Test to choose minimal viable options.Leads to balanced, context-aware choices rather than fashionable ones.Team CommunicationBuilds shared mental models of how the system works.Simplifies vocabulary, diagrams, and documentation to make knowledge accessible.Shared understanding improves coordination and reduces design entropy.Development ProcessUses causal-loop diagrams and retrospectives to surface feedback.Implements feedback through code review, architectural refactoring, and iteration.Continuous learning leads to continuous simplification.MeasurementMonitors system health using flow efficiency, defect rates, and stability.Measures simplicity through cognitive load, maintainability, and energy use.Both provide actionable metrics for sustainable improvement.Leadership & CultureEncourages cross-functional collaboration and long-term thinking.Rewards clarity, humility, and stewardship over cleverness.Creates organizations where technology and culture evolve coherently.## Feedback and Learning: The System That Simplifies Itself Both philosophies treat improvement as a continuous process, not a destination. In Systems Thinking, feedback loops are the mechanism through which systems learn and adapt. Reinforcing loops amplify changes; balancing loops maintain stability. By understanding these dynamics, leaders can create systems capable of self-correction. Simplicity-First applies this principle to design. Teams refine code, streamline features, and adjust architecture based on real-world feedback. A simple system is not one that was designed perfectly from the start. It’s one that grows and evolves responsibly through continuous pruning. This creates a dynamic equilibrium: continuous adjustment without chaos. Both disciplines provide tools to recognize when systems are drifting toward unnecessary complexity and mechanisms to guide them back toward clarity. ## Finding Leverage Points [Donella Meadows](https://en.wikipedia.org/wiki/Donella_Meadows), in her [seminal work on systems dynamics](https://systemsthinkingalliance.org/donella-meadows-pioneering-contributions-to-systems-thinking-and-environmental-advocacy/), identified “leverage points”, places in a system where small changes produce large effects. This concept beautifully bridges Systems Thinking and Simplicity-First. Consider a system plagued by slow deployments. A Systems Thinking analysis might map the entire CI/CD pipeline, revealing that test suites take 45 minutes because they spin up full database instances. A Simplicity-First lens identifies the leverage point: replace integration tests with contract tests where possible, use in-memory databases for unit tests. The change is small, the impact cascades through developer productivity, deployment frequency, and incident response time. Leverage points are where understanding meets action. Systems Thinking reveals them; Simplicity-First gives us the courage to act on them. ## Sustainability: The Long View Both frameworks advocate against short-term optimization that creates long-term fragility. Systems Thinking emphasizes that every intervention brings consequences, including unintended outcomes that may emerge years later. Sustainable systems account for these downstream effects. In organizational contexts, this means designing structures and incentives that prioritize resilience over quarterly metrics. Simplicity-First frames complexity as debt, not just technical debt, but cognitive and ecological debt. Complex systems require more energy to operate, demand higher expertise for maintenance, and trap organizations in inflexible patterns. By embracing simplicity, we reduce waste in computation, time, and human attention. ## The Human Dimension: Designing for Understanding Both philosophies center on a fundamental truth: the key constraint in software systems is human comprehension. Systems Thinking acknowledges that no individual can fully grasp the intricacies of a complex system. It promotes teamwork, shared mental models, and open communication to surface blind spots. Simplicity-First takes this further: if a system cannot be understood by the team as a whole, it is too complex, regardless of how elegant its individual components may be. Systems should be designed so that understanding is distributed rather than concentrated in heroic individuals. ## The Ethical Imperative of Clarity A connection exists between these philosophies and ethical responsibility that often goes unspoken. Systems Thinking reminds us that every decision carries unforeseen consequences. Simplicity-First adds that complexity amplifies those consequences by making them harder to trace. In an era of intricate algorithms and expansive cloud architectures, clarity is not merely beneficial. It is a responsibility. When systems fail in ways their creators cannot explain, we have failed our users, our organizations, and our profession. ## Conclusion: From Philosophy to Monday Morning The marriage of Systems Thinking and Simplicity-First is not merely academic. It is a practical framework for making better architectural decisions. Systems Thinking grants awareness, the ability to see complexity in its entirety, to trace feedback loops, to anticipate unintended consequences. Simplicity-First provides restraint, the discipline to act responsibly within that complexity, to eliminate what does not serve the whole. Together, they create a philosophy of thoughtful simplicity. What does this mean in practice? When you face your next architectural decision, whether to extract a service, adopt a new framework, or add another layer of abstraction, ask two questions. First: *What are the second-order effects of this change?* Map the dependencies, trace the feedback loops, and identify what this change will touch. Second: *Does this complexity need to exist?* Apply the 2 AM Test. Apply the Half-Rule. Be honest about whether you’re solving a real problem or performing architecture theater. The goal is not to do less. It is to do what truly matters, and to understand why it matters. Systems Thinking illuminates what matters most. Simplicity-First gives us the clarity to build it. **Categories:** Simplicity-First **Tags:** architecture, design, simplicity-first, systems thinking --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Data Transfer Object Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-data-transfer-object-pattern/) **Published:** January 19, 2026 **Author:** Chris Woodruff **Content:** Your domain model exists to protect your business rules. Your API exists to protect your clients. When you expose EF Core entities directly from your ASP.NET Core endpoints, you throw both of those protections away. The Data Transfer Object Pattern (DTO) is the line in the sand. DTOs carry data across boundaries. They flatten and optimize your internal objects for remote calls. They let your domain change without renegotiating contracts with every consumer. In this post, you will see: - What DTOs really are in .NET - How they differ from domain and persistence models - Before and after Minimal API examples - Practical mapping patterns in C# - When DTOs are essential and when they are just ceremony ## Your Domain Is Not An API Contract A very familiar anti-pattern looks like this. ``` app.MapGet("/orders/{id:guid}", async ( Guid id, AppDbContext db, CancellationToken ct) => { var order = await db.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, ct); if (order is null) { return Results.NotFound(); } // Directly returning an EF Core entity return Results.Ok(order); }); ``` It feels efficient: - No extra classes - No mapping code - One entity in, one entity out It also quietly locks your API to your internal model: - Renaming a property on `Order` changes your public JSON - Splitting `Order` into `Order` plus `OrderHeader` breaks clients - Adding a navigation property leaks internal relationships into the contract You did not design a contract. You simply let your persistence model walk out the door. DTOs exist to fix that. ## What A DTO Actually Is A Data Transfer Object is boring on purpose. - It carries data across process boundaries like HTTP, gRPC, or queues - It contains no domain behavior and no persistence logic - It is shaped around consumer needs and contract stability In .NET, DTOs are simple classes or records. Here is a typical order DTO. ``` public record OrderDto( Guid Id, Guid CustomerId, decimal TotalAmount, string Status, IReadOnlyCollection Lines); public record OrderLineDto( Guid ProductId, int Quantity, decimal UnitPrice); ``` DTOs are not your domain model, and they are not your EF entities. They are the shape you promise to the outside world. ## Domain Model vs Persistence Model vs DTO It helps to keep three different models in your head. ### Domain model This is your business. ``` public enum OrderStatus { Draft, Confirmed, Shipped, Cancelled } public sealed class Order { private readonly List _lines = new(); private Order(Guid id, Guid customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; } public Guid Id { get; } public Guid CustomerId { get; } public OrderStatus Status { get; private set; } public IReadOnlyCollection Lines => _lines.AsReadOnly(); public decimal TotalAmount => _lines.Sum(l => l.Total); public static Order Create(Guid customerId, IEnumerable lines) { var order = new Order(Guid.NewGuid(), customerId); foreach (var line in lines) { order.AddLine(line.ProductId, line.Quantity, line.UnitPrice); } if (!order._lines.Any()) { throw new InvalidOperationException("Order must have at least one line."); } order.Status = OrderStatus.Confirmed; return order; } public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (quantity Quantity * UnitPrice; } ``` This model enforces invariants. It cares about business rules, not JSON. ### Persistence model Sometimes your EF entities are the domain. Sometimes, they mainly reflect database constraints. ``` public sealed class OrderEntity { public Guid Id { get; set; } public Guid CustomerId { get; set; } public string Status { get; set; } = default!; public decimal TotalAmount { get; set; } public List Lines { get; set; } = new(); } public sealed class OrderLineEntity { public Guid Id { get; set; } public Guid OrderId { get; set; } public Guid ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } } ``` These types are aware of the database schema and relationships. They do not have to be identical to your domain model. ### DTO model This is the external contract. ``` public record OrderSummaryDto( Guid Id, decimal TotalAmount, string Status); public record OrderDetailsDto( Guid Id, Guid CustomerId, decimal TotalAmount, string Status, IReadOnlyCollection Lines); ``` The DTO model exists so that the domain and persistence can change while the contract stays stable. ## Mapping Between Domain And DTOs You need mapping code. That is the price of decoupling. ### Domain to DTO ``` public static class OrderDtoMapping { extension(Order order) { public OrderSummaryDto ToSummaryDto() => new(order.Id, order.TotalAmount, order.Status.ToString()); public OrderDetailsDto ToDetailsDto() => new( order.Id, order.CustomerId, order.TotalAmount, order.Status.ToString(), order.Lines .Select(l => new OrderLineDto(l.ProductId, l.Quantity, l.UnitPrice)) .ToList()); } } ``` ### DTO to domain command For writes, convert inbound DTOs into command objects. ``` public sealed record CreateOrderRequest( Guid CustomerId, IReadOnlyCollection Lines); public sealed record CreateOrderLineRequest( Guid ProductId, int Quantity, decimal UnitPrice); public sealed record PlaceOrderCommand( Guid CustomerId, IReadOnlyCollection Lines); public sealed record PlaceOrderLine( Guid ProductId, int Quantity, decimal UnitPrice); public static class CreateOrderCommandMapping { public static PlaceOrderCommand ToCommand(this CreateOrderRequest request) => new( request.CustomerId, request.Lines.Select(l => new PlaceOrderLine(l.ProductId, l.Quantity, l.UnitPrice)).ToList()); } ``` The application service now talks in domain terms, not HTTP terms. ## Before vs After: Exposing Entities vs Using DTOs ### Before: returning EF Core entities directly ``` app.MapGet("/orders/{id:guid}", async ( Guid id, AppDbContext db, CancellationToken ct) => { var entity = await db.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, ct); if (entity is null) { return Results.NotFound(); } // Public JSON equals internal EF Core model return Results.Ok(entity); }); ``` Tightly coupled behavior: - JSON structure equals table structure - Any schema change becomes an API change - You often send more data than the client needs ### After: clean DTO response via application service First, an application service that exposes a query method. ``` public interface IOrderQueryService { Task GetDetailsAsync(Guid id, CancellationToken cancellationToken); } public sealed class OrderQueryService(AppDbContext dbContext) : IOrderQueryService { private readonly AppDbContext _dbContext = dbContext; public async Task GetDetailsAsync( Guid id, CancellationToken cancellationToken) { var entity = await _dbContext.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, cancellationToken); if (entity is null) { return null; } // Map EF entity to domain, or map directly to DTO if you treat entity as domain var order = new OrderSnapshot( entity.Id, entity.CustomerId, Enum.Parse(entity.Status), entity.Lines.Select(l => new OrderLineSnapshot(l.ProductId, l.Quantity, l.UnitPrice)).ToList()); return order.ToDetailsDto(); } } // A simple read-only domain snapshot public sealed record OrderLineSnapshot(Guid ProductId, int Quantity, decimal UnitPrice); public sealed class OrderSnapshot( Guid id, Guid customerId, OrderStatus status, IReadOnlyCollection lines) { public Guid Id { get; } = id; public Guid CustomerId { get; } = customerId; public OrderStatus Status { get; } = status; public IReadOnlyCollection Lines => lines; public decimal TotalAmount => lines.Sum(l => l.Quantity * l.UnitPrice); } public static class OrderSnapshotDtoMapping { public static OrderDetailsDto ToDetailsDto(this OrderSnapshot order) => new( order.Id, order.CustomerId, order.TotalAmount, order.Status.ToString(), order.Lines .Select(l => new OrderLineDto(l.ProductId, l.Quantity, l.UnitPrice)) .ToList()); } ``` Then the Minimal API endpoint becomes: ``` app.MapGet("/orders/{id:guid}", async ( Guid id, IOrderQueryService orders, CancellationToken ct) => { var dto = await orders.GetDetailsAsync(id, ct); if (dto is null) { return Results.NotFound(); } return Results.Ok(dto); }); ``` Same endpoint, different attitude: - Contract is explicit - Internals can be refactored - Storage strategy is invisible to clients ## DTOs For Writes: Keeping Input Honest The same problem appears in the other direction when you bind requests directly onto entities. ### Before: binding request body to EF Core entity ``` app.MapPost("/orders", async ( AppDbContext db, OrderEntity request, CancellationToken ct) => { // Request body is bound directly to EF entity db.Orders.Add(request); await db.SaveChangesAsync(ct); return Results.Created($"/orders/{request.Id}", new { request.Id }); }); ``` Risks: - Client can set fields that should be server controlled - Client can accidentally or intentionally break invariants - EF entity ends up full of fields that exist only for API reasons ### After: inbound DTO plus application service Define a clean request DTO. ``` public sealed record CreateOrderRequest( Guid CustomerId, IReadOnlyCollection Lines); public sealed record CreateOrderLineRequest( Guid ProductId, int Quantity, decimal UnitPrice); ``` Command and service: ``` public sealed record PlaceOrderCommand( Guid CustomerId, IReadOnlyCollection Lines); public sealed record PlaceOrderLine( Guid ProductId, int Quantity, decimal UnitPrice); public interface IOrderApplicationService { Task PlaceOrderAsync(PlaceOrderCommand command, CancellationToken cancellationToken); } public sealed class OrderApplicationService(IOrderRepository orders, IUnitOfWork unitOfWork) : IOrderApplicationService { private readonly IOrderRepository _orders = orders; private readonly IUnitOfWork _unitOfWork = unitOfWork; public async Task PlaceOrderAsync( PlaceOrderCommand command, CancellationToken cancellationToken) { await _unitOfWork.BeginAsync(cancellationToken); try { var lines = command.Lines .Select(l => (l.ProductId, l.Quantity, l.UnitPrice)) .ToList(); var order = Order.Create(command.CustomerId, lines); await _orders.AddAsync(order, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); return order.Id; } catch { await _unitOfWork.RollbackAsync(cancellationToken); throw; } } } ``` Mapping from request to command: ``` public static class CreateOrderRequestMapping { public static PlaceOrderCommand ToCommand(this CreateOrderRequest request) => new( request.CustomerId, request.Lines.Select(l => new PlaceOrderLine(l.ProductId, l.Quantity, l.UnitPrice)).ToList()); } ``` Minimal API endpoint now: ``` app.MapPost("/orders", async ( CreateOrderRequest request, IOrderApplicationService service, CancellationToken ct) => { if (request.Lines == null || !request.Lines.Any()) { return Results.BadRequest("Order must contain at least one line."); } var command = request.ToCommand(); var orderId = await service.PlaceOrderAsync(command, ct); return Results.Created($"/orders/{orderId}", new { Id = orderId }); }); ``` The boundary is explicit: - HTTP requests talk in DTOs - The application service talks in commands and domain types - Persistence and invariants live behind the DTOs ## When To Use DTOs DTOs are not optional when any of these are true. ### Public APIs and external clients - Third parties integrate with your service - Mobile apps consume your endpoints - You cannot redeploy clients at will You need a contract that can outlive your current entity design. ### Cross-service messaging - Commands and events on queues - Integration between services or bounded contexts DTOs become message contracts: - Versioned - Documented - Independent of internal model changes ### Security and data minimization You want to avoid: - Leaking internal foreign keys - Exposing audit data or internal flags - Accepting fields from clients that they should never control DTOs let you design exactly what travels across the boundary. ### Performance and payload design Clients may need: - A light summary for list pages - A heavy detail view for a specific screen - A specialized projection for reporting Different DTOs for each scenario keep payloads focused and efficient. ## When DTOs Are Just Noise DTOs are not sacred. Sometimes they are an unnecessary ceremony. ### Small, internal tools - An internal admin API used by one team - Short-lived services - Low risk if the shape changes You may accept using entities directly while the system is experimental, as long as you know this is a temporary trade. ### Simple CRUD that will not evolve much - Tables that map almost exactly to the external data representation - Very stable models with low likelihood of change In that case, DTOs that simply copy every property can be redundant. ### DTOs that mirror entities blindly If your DTOs are literally: ``` public class OrderDto { public Guid Id { get; set; } public Guid CustomerId { get; set; } public string Status { get; set; } = default!; public decimal TotalAmount { get; set; } public List Lines { get; set; } = new(); } ``` you are not gaining much. The pattern is not about duplicating classes. It is about designing contracts. --- ## DTOs, Commands, Queries, And CQRS DTOs fit naturally into a CQRS style. - Input DTOs represent HTTP request payloads - Commands represent intent in domain language - Query DTOs represent read models A common setup: - `CreateOrderRequest` (input DTO) maps to `PlaceOrderCommand` - Command handler uses domain entities and repositories - Domain events may produce `OrderCreatedEventDto` for messaging - Read endpoints return `OrderSummaryDto` or `OrderDetailsDto` built from read models Each step has a clear purpose. ## Practical Guidelines For DTO Design A few rules that keep DTO usage sane. 1. Keep DTOs flat and focused - Flatten nested data where appropriate - Avoid reflecting deep object graphs unless the client really needs them 2. Separate input and output DTOs - Do not reuse the same DTO for create, update, and read - Avoid exposing fields that clients should not control 3. Make mapping explicit - Use extension methods or dedicated mapping classes - Avoid magic reflection-based mapping without tests 4. Validate at the DTO layer - Use data annotations or FluentValidation on DTOs - Keep entity invariants inside the domain, not in controllers ## Introducing DTOs Into An Existing ASP.NET Core App If your current app leaks entities everywhere, you can still fix it incrementally. 1. Find one endpoint that exposes entities - Ideally, one that external clients rely on 2. Design DTOs for that endpoint - Based on what consumers actually use - Strip out internal fields and relationships 3. Add mapping - Domain to DTO for responses - DTO to command or domain input for requests 4. Swap the endpoint to use DTOs - Keep the behavior the same - Keep tests green 5. Repeat for other external endpoints - Prioritize the ones that are public and hard to change Bit by bit, your EF entities stop being your API. ## Closing Thought Every endpoint in your ASP.NET Core application answers one question: Are you talking to your own code, or are you talking to every client and system that depends on you? DTOs are the pattern that admits that difference. Behind the DTO boundary, you can refactor aggressively. In front of it, you owe your callers stability. If you return entities directly today, pick one critical endpoint and move it to DTOs. After that exercise, you will have a much harder time pretending that your persistence model and your contract are the same thing. **Categories:** Patterns --- ### [Stop Building SPAs for Every Screen: htmx + ASP.NET Core Razor Pages Workshop (Open)](https://www.woodruff.dev/stop-building-spas-for-every-screen-htmx-asp-net-core-razor-pages-workshop-open/) **Published:** January 14, 2026 **Author:** Chris Woodruff **Content:** If your default move for “modern UX” is a SPA, you are paying a tax you do not need. You pay for it in build pipelines, duplicated validation rules, fragile client state, and a front end that turns routine CRUD into an engineering project. This workshop is a different bet. We keep the server in charge, keep HTML as the interface, and still ship interactions that feel sharp. Razor Pages stays at the center. htmx becomes the small layer that turns clicks and form posts into targeted fragment updates. I am opening the full workshop materials to everyone. You can clone the repository, run the starter app, and work through the labs at your own pace. You can find the workshop instructions here. The GitHub repo for the workshop is here. ## What You Will Learn ### Hypermedia, in Practical Terms Links and forms are state transitions. The browser requests the next UI state. The server responds with HTML that represents that state. You inspect it, you swap it, you move on. ### The htmx Mental Model You will learn one loop and reuse it across every lab. - Pick a fragment boundary - Wrap it with a stable id - Trigger a request - Return a fragment - Swap it into place ### Hands-On Labs that Map to Real Work You will build a small Razor Pages app and evolve it through patterns you can reuse on Monday. - Fragment-first composition with partials and stable targets - Partial updates with `hx-get` and `hx-post` - Server-side validation with immediate feedback and clean invalid flows - Details panel or modal, confirm delete, paging and filtering with history support - Variable sub-collections, dependent dropdowns, and long-running work with polling - Out-of-band updates for messages and status ## Who Should Join You already know C# and Razor Pages. You want modern interactions without a client-side app taking over your architecture. You want to debug with DevTools and server logs, not with a maze of client state and side effects. ## Why this Matters Many teams keep adding JavaScript stacks to solve problems created by leaving the web platform behind. If you can deliver the same experience with server-rendered fragments, you reduce surface area and regain control. ## How to Participate Clone the repo, run the app, and follow the lab documents in order. Each lab is designed as a sequence of small, testable changes. You can apply the patterns page by page in a real product without a rewrite. ## Help Me Improve It If you find a bug, a confusing step, or a missing explanation, please share what you saw and how to reproduce it. The strongest way is to open an issue in the GitHub repository with: - What you expected - What happened instead - Steps to reproduce - Your environment details (OS, .NET SDK version, IDE) - Screenshots or snippets when useful If you prefer, send the details directly, and I will capture them as an issue. If your current stack claims a SPA is mandatory for every screen, run this workshop. Then decide which complexity you still want to carry. **Categories:** htmx **Tags:** .NET, asp.net core, C#, dotnet, htmx, programming --- ### [Enterprise Patterns, Real Code: Implementing Fowler’s Ideas in C#](https://www.woodruff.dev/enterprise-patterns-real-code-implementing-fowlers-ideas-in-c/) **Published:** November 28, 2025 **Author:** Chris Woodruff **Content:** Most enterprise systems already use patterns from [Martin Fowler’s *Patterns of Enterprise Application Architecture*](https://martinfowler.com/eaaCatalog/). The twist is that many teams use them without naming them, half implement them and then wonder why the codebase fights back. If your ASP.NET solution contains controllers that talk straight to SQL, services that return HTTP responses, and entities that call SaveChanges, you are already remixing Fowler’s patterns. You are just doing it implicitly and at great expense. This series takes the opposite route. We will name the patterns, show where they fit, and implement each one in C# with concrete examples. You will see where these patterns help, where they hurt, and how they combine into real architectures instead of diagram fantasies. ## **How this series will work** In each article, I will: - - - Explain a pattern in plain language, with the original intent from Fowler’s catalog - - Show how it typically shows up in C# and .NET projects - - Give a focused example of when to use it and when to avoid it - - Connect it to neighboring patterns so you see design options, not isolated tricks This introduction maps the territory. Every item in the index below links to a short description, a definition, and an example scenario. Each of those sections will become a full post with code. ## **Pattern index** Use these links to jump to the pattern you care about right now. - - - **[Layered Architecture](https://www.woodruff.dev/stop-letting-your-controllers-talk-to-sql-layered-architecture-in-asp-net-core/)** - - **[Transaction Script](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-transaction-script-pattern-the-shortcut-that-quietly-reshapes-your-system/)** - - **[Domain Model](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-domain-model-pattern-when-your-core-rules-deserve-their-own-gravity/)** - - **[Service Layer](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-service-layer-pattern-making-http-a-client-not-the-boss/)** - - **[Active Record](https://woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-active-record-pattern/)** - - **[Data Mapper](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-data-mapper-pattern/)** - - **[Repository](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-repository-pattern/)** - - **[Unit of Work](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-unit-of-work-pattern/)** - - **[Identity Map](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-identity-map-pattern/)** - - **[Lazy Load](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-lazy-load-pattern/)** - - **[Front Controller and MVC](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-front-controller-and-mvc-pattern/)** - - - Data Transfer Object (DTO) Each section below is both a preview and a contract for the dedicated C# article that will follow. ## Layered Architecture Pattern #### **Definition** Layered Architecture splits an application into distinct layers with clear responsibilities. Fowler’s baseline is: - - - Presentation: handles input and output - - Domain: holds business rules and domain logic - - Data source: manages persistence and integration with data stores The core rule is brutal and simple: if your controllers know SQL or your repositories know HTTP, the layers have already collapsed. #### **Where to use it** Layered Architecture fits: - - - ASP.NET Core applications that will grow beyond a few controllers - - Systems where different teams own UI, business rules, and infrastructure - - Codebases that must survive several technology shifts over their lifetime In the dedicated post you will see a C# solution where projects align with layers, controllers stay thin, domain services stay ignorant of transport, and repositories encapsulate EF Core without leaking it upward. ## Transaction Script Pattern #### **Definition** Transaction Script organizes business logic as procedures that handle a single request or use case end to end. Each script: - - - Reads input - - Performs calculations and decisions - - Persists changes There is minimal domain modeling. The focus stays on the flow of a transaction. #### **Where to use it** Transaction Script works best when: - - - The domain logic is simple and shallow - - You are building reports, admin utilities, or migration tools - - You need results quickly and long term complexity is limited In C#, this often appears as an application service or handler class that works directly with DbContext and simple DTOs. In the article you will see both the benefits and the trap: it feels efficient until rules start to repeat across scripts. ## Domain Model Pattern #### **Definition** Domain Model concentrates business rules inside a rich object model. Entities and value objects express invariants and behavior. Instead of treating data as passive structures, you treat the domain as the center of gravity. Controllers and repositories orbit around it rather than injecting rules into every edge of the system. #### **Where to use it** Domain Model earns its weight when: - - - The business rules are complex and interdependent - - Invariants matter more than raw throughput - - You expect requirements to evolve frequently In C# this means entities with methods that enforce rules, factories that control creation, and services that orchestrate multiple aggregates. The dedicated post will show an aggregate in code, along with tests that lock in behavior before you worry about EF mapping. ## Service Layer Pattern #### Definition Service Layer defines a set of application operations that sit between the outside world and the domain model. It: - - - Coordinates multiple domain objects - - Handles transactions and security policies - - Exposes a clear API for controllers, message handlers, or other clients It is the point where use cases live. #### **Where to use it** Service Layer fits when: - - - You have multiple clients hitting the same core logic: web, background jobs, workers - - You want to expose a stable application API while the UI evolves - - Cross cutting concerns such as logging, permissions, and transaction boundaries must stay consistent In .NET this often becomes a set of application service classes injected into controllers and workers. In the article you will see a C# service layer that makes HTTP a detail, not the boss. ## Active Record Pattern #### **Definition** Active Record merges domain objects with persistence. Each entity: - - - Maps directly to a database row - - Contains business logic - - Knows how to load and save itself Fowler treats it as a close fit for simple domains where the object model mirrors the database closely. #### **Where to use it** Active Record suits: - - - Small systems with straightforward tables - - Prototypes where getting something working matters more than deep abstraction - - Places where simple CRUD with light behavior is enough In C#, you often see Active Record flavor when EF Core entities call SaveChanges directly or static methods perform global queries. The dedicated post will show a disciplined version of Active Record and explain when to retire it in favor of a separate Data Mapper. ## Data Mapper Pattern #### **Definition** Data Mapper sits between domain objects and the database. It: - - - Loads domain objects from data stores - - Persists changes back - - Shields the domain from knowledge of how persistence works The domain classes stay persistence ignorant. The mapper takes on the burden of translation. #### **Where to use it** Data Mapper pays off when: - - - You have a rich Domain Model - - The database schema must evolve independently of the object model - - You want to test domain logic without a database in the way In .NET, EF Core already plays the Data Mapper role. The article will show how to design domain classes that do not depend on EF, then map them using configurations and repositories that wrap the mapper. ## Repository Pattern #### **Definition** Repository represents a collection-like interface for accessing aggregates. It: - - - Hides queries and persistence details - - Exposes methods that work in domain terms, such as GetById, FindActiveForCustomer, Add, Remove - - Lets the domain talk in its own language instead of in SQL or query APIs Fowler includes Repository in the object relational patterns as a way to further isolate domain logic from data access. #### **Where to use it** Repository helps when: - - - The same aggregate appears across many use cases - - You want consistent access patterns for aggregates - - You expect to support multiple query strategies or stores behind the same domain interface In C#, this usually means interface definitions in the domain layer and implementations in an infrastructure project. The dedicated post will include concrete repository designs, and also examples of where a repository introduces more indirection than it earns. ## Unit of Work Pattern #### **Definition** Unit of Work tracks changes to domain objects during a business transaction and writes them out as a single logical batch. It: - - - Records inserts, updates, and deletes - - Coordinates commit or rollback - - Provides a boundary for transactional behavior Fowler presents it as a way to stop writes from spreading unpredictably through a codebase. #### **Where to use it** Unit of Work is valuable when: - - - A single operation touches multiple aggregates or tables - - You need clear transactional boundaries for consistency - - You want to keep domain logic free of save calls In .NET, DbContext already behaves as a Unit of Work, yet many codebases hide that fact. The article will show how to embrace this pattern explicitly and how to wrap EF Core in a higher level unit of work abstraction when needed. ## Identity Map Pattern #### **Definition** Identity Map ensures that each logical entity from the database exists only once in memory per scope. It: - - - Tracks loaded objects by identity - - Returns existing instances instead of creating new ones for the same key - - Helps avoid inconsistent in memory states for the same row This pattern often works with Unit of Work and Data Mapper. #### **Where to use it** Identity Map matters when: - - - The same entity is loaded through different paths in one request - - You attach domain behavior to entities and depend on reference equality - - You care about performance costs of repeated materialization ORMs such as EF Core implement identity maps under the surface. The dedicated post will explain what EF is doing for you and show how to apply Identity Map explicitly when you move outside of ORMs or use multiple contexts. ## Lazy Load Pattern #### **Definition** Lazy Load defers loading of related data until it is actually needed. Instead of fetching entire object graphs, you: - - - Load a root entity - - Represent associations as placeholders - - Trigger actual loading when code accesses the association The pattern targets performance and memory by avoiding unnecessary work. #### **Where to use it** Lazy Load helps when: - - - Most use cases do not need full graphs - - Some navigations are expensive or remote - - You have to control query explosions carefully In .NET, EF Core can use lazy loading proxies, or you can code your own lazy associations. The article will show both approaches and highlight the risk: invisible queries that surprise you in performance profiles. ## Front Controller Pattern #### **Definition** Front Controller centralizes request handling for a web application. Instead of letting every page or endpoint own its own entry point, you: - - - Route all requests through a single handler - - Apply cross cutting logic in one place - - Delegate to controllers or handlers for detailed work Fowler introduces it as a response to duplicated request handling logic. #### **Where to use it** Front Controller aligns with: - - - Web applications that need consistent logging, authentication, and error handling - - Systems that must make routing decisions based on shared policies - - Architectures that use pipelines and middleware In ASP.NET Core, the combination of the hosting pipeline and routing already forms a Front Controller. The dedicated post will show how to control that pipeline intentionally instead of treating it as framework magic. ## Model View Controller (MVC) Pattern #### **Definition** Model View Controller splits UI logic into three parts: - - - Model: the underlying data and behavior - - View: rendering logic - - Controller: input handling and coordination Fowler’s version focuses on server side web MVC. #### **Where to use it** MVC suits: - - - Applications with complex UI interactions that depend on domain rules - - Teams that want clear separation between presentation logic and domain logic - - Systems that must support multiple views on the same model In ASP.NET Core MVC, controllers speak to application services, views render models, and domain rules stay out of both. The article will show how to keep controller code lean instead of letting it morph into a second application layer. ## Data Transfer Object (DTO) Pattern #### **Definition** Data Transfer Object carries data across process boundaries. It: - - - Aggregates fields into a serializable shape - - Avoids sending full domain objects across the wire - - Provides a contract between services, clients, or layers DTOs trade object richness for stability and clarity at integration points. #### **Where to use it** DTOs are worth the effort when: - - - You are exposing public APIs - - Multiple clients consume your service, each with their own evolution pace - - You want to keep domain classes internal to your application In C#, DTOs typically appear as record types in API projects or as message contracts in messaging systems. The article will show mapping patterns between domain objects and DTOs and how to keep them from overflowing with accidental complexity. ## What comes next The rest of this series will go pattern by pattern: - - - Each pattern gets its own post - - Each post includes C# examples, tests where relevant, and context from real projects - - The focus stays on tradeoffs, not worship of diagrams You can read the series start to finish, or you can drop directly into the pattern that matches the pain in your current system and work outward from there. If your code already resembles these patterns, this series gives you language and structure. If it does not, the upcoming posts will show how to reshape it piece by piece without pausing delivery. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core: Front Controller and MVC Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-front-controller-and-mvc-pattern/) **Published:** January 11, 2026 **Author:** Chris Woodruff **Content:** If every controller in your system does its own authentication, logging, and error handling, you do not have an architecture. You have a crowd of small frameworks pretending to cooperate. Front Controller and MVC are the patterns that push back against that drift. - Front Controllers decide what *every* HTTP request must pass through. - MVC decides what belongs inside the web layer and what does not. In ASP.NET Core, the pipeline and routing already give you a natural front controller. The real question is whether you treat that entry point as a disciplined gateway or as a dumping ground. This post walks through: - What Front Controller and MVC mean in practice for ASP.NET Core - A compact custom Front Controller middleware and router - Before and after Minimal API examples - How does this pattern let you own cross-cutting concerns instead of scattering them - When to lean into Front Controller and MVC, and when to keep things minimal ## Who Actually Owns Your HTTP Boundary Picture a typical service after a few sprints: - One team adds authentication checks inside controllers. - Another team adds logging with `ILoggerFactory` directly in endpoints. - A third team adds a custom filter for correlation IDs. - Everyone wires error handling however they feel like that week. No single place shows what happens to an HTTP request from the moment it arrives until the moment it leaves. If you want to add a feature flag, a rate limit, or a security policy, you end up chasing it across controllers, filters, and boilerplate copies. Front Controller exists to collapse that mess into one place. MVC exists to stop controllers from becoming a second domain layer. ## What Front Controller And MVC Actually Are ### Front Controller in practice In Fowler’s terms, a Front Controller is: - A single handler that receives all incoming requests - A gateway that applies cross-cutting policies once - A dispatcher that forwards to specific page controllers or handlers In ASP.NET Core, you already have the makings of this: - `Program.cs` where you build the middleware pipeline - Routing that maps requests to endpoints - A place to plug in logic that will intercept every request The pattern is less about adding brand new mechanisms and more about using the existing ones with intent. ### MVC in practice MVC splits responsibilities: - The model holds domain data and behavior. - Views presents data to the outside world: HTML, JSON, or something else. - The controller translates HTTP requests into domain calls and selects views. In ASP.NET Core: - Models are domain entities, value objects, DTOs, and view models. - Views are Razor views or serialized responses. - Controllers or Minimal API handlers are the thin glue. MVC goes wrong when controllers: - Contain business rules - Talk directly to the database - Duplicate logic that belongs in the domain or application services Front Controller and MVC together say: - Put cross-cutting concerns at the gateway - Keep controllers as simple adapters - Keep business logic in application and domain services ## A Compact Front Controller Middleware You can write a very direct Front Controller in ASP.NET Core using custom middleware and a simple router. ``` public interface IEndpointHandler { Task HandleAsync(HttpContext context); } public interface IEndpointRouter { IEndpointHandler? Match(HttpRequest request); } public class FrontControllerMiddleware(RequestDelegate next) { private readonly RequestDelegate _next = next; public async Task InvokeAsync(HttpContext context, IEndpointRouter router) { // Global cross cutting concerns can sit right here // Example: logging, correlation, feature flags var endpoint = router.Match(context.Request); if (endpoint == null) { context.Response.StatusCode = StatusCodes.Status404NotFound; await context.Response.WriteAsync("Not found"); return; } await endpoint.HandleAsync(context); } } ``` A very simple router: ``` public class SimpleEndpointRouter : IEndpointRouter { private readonly IReadOnlyDictionary _handlers; public SimpleEndpointRouter(IEnumerable handlers) { // Basic example: key by path. _handlers = handlers.ToDictionary( h => h switch { GetOrderEndpoint => "/orders/get", PlaceOrderEndpoint => "/orders/place", _ => string.Empty }, h => h); // In a real implementation, you would key by method and path pattern. } public IEndpointHandler? Match(HttpRequest request) { if (!_handlers.TryGetValue(request.Path, out var handler)) { return null; } return handler; } } ``` And one handler: ``` public class GetOrderEndpoint(IOrderQueryService orders) : IEndpointHandler { private readonly IOrderQueryService _orders = orders; public async Task HandleAsync(HttpContext context) { if (!Guid.TryParse(context.Request.Query["id"], out var orderId)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid id"); return; } var order = await _orders.GetOrderDetailsAsync(orderId); if (order is null) { context.Response.StatusCode = StatusCodes.Status404NotFound; return; } context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(order); } } ``` This is deliberately minimal. It proves a point. - One middleware sees every request. - Route selection is centralized. - Cross-cutting concerns sit right at the boundary. In a real application, you probably rely on ASP.NET Core routing rather than a custom router. The pattern still applies. The pipeline is your Front Controller. The key is what you put there. ## ASP.NET Core As A Front Controller You do not need to reinvent routing to apply this pattern. Use what the framework already gives you, but treat it as a strict boundary. `Program.cs` already acts as the front door. ``` var builder = WebApplication.CreateBuilder(args); // Registration builder.Services.AddAuthentication("Cookies") .AddCookie("Cookies", options => { /* options */ }); builder.Services.AddAuthorization(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); // Your application services, UoW, repositories, etc builder.Services.AddScoped(); builder.Services.AddScoped(); var app = builder.Build(); // Global front controller pipeline app.UseMiddleware(); app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // MVC app.MapMinimalApis(); // extension that registers your minimal endpoints app.Run(); ``` Here, the pipeline is the Front Controller: - Every request receives a correlation id. - Every unhandled exception is handled in a single place. - Authentication and authorization run centrally. Controllers and endpoints do not need to worry about these mechanics. ## Before: Controllers That Improvise Infrastructure Consider an ASP.NET Core API that skipped discipline at the boundary. ``` [ApiController] [Route("api/orders")] public class OrdersController(AppDbContext db, ILogger logger) : ControllerBase { private readonly AppDbContext _db = db; [HttpPost] public async Task PlaceOrder(PlaceOrderRequest request) { if (!User.Identity?.IsAuthenticated ?? true) { logger.LogWarning("Unauthenticated order attempt"); return Unauthorized(); } try { using var transaction = await _db.Database.BeginTransactionAsync(); var order = new Order { Id = Guid.NewGuid(), CustomerId = request.CustomerId, CreatedAtUtc = DateTime.UtcNow }; foreach (var line in request.Lines) { order.Lines.Add(new OrderLine { ProductId = line.ProductId, Quantity = line.Quantity, UnitPrice = line.UnitPrice }); } _db.Orders.Add(order); await _db.SaveChangesAsync(); logger.LogInformation("Order {OrderId} placed by {UserId}", order.Id, User.Identity?.Name); await transaction.CommitAsync(); return CreatedAtAction(nameof(GetById), new { id = order.Id }, new { order.Id }); } catch (Exception ex) { logger.LogError(ex, "Error placing order"); return StatusCode(StatusCodes.Status500InternalServerError, "Unexpected error"); } } [HttpGet("{id:guid}")] public async Task GetById(Guid id) { // similar pattern: local try catch, local logging, direct DbContext, etc // omitted for brevity throw new NotImplementedException(); } } ``` Problems: - Authentication check is inside the action. - Transaction management is inside the action. - Logging and error handling are inside the action. - DbContext is used directly instead of application services. If you need to change the logging format, transaction strategy, or authentication rules, you need to take many actions. This controller does not sit behind a clear Front Controller. It *is* the front controller for its own small kingdom. ## After: Pipeline As Front Controller, Controllers As Glue Refactor so that cross-cutting concerns live in the pipeline and application logic lives in services. Controllers become thin coordinators. ### Global correlation and exception middleware Correlation ID middleware: ``` public class RequestCorrelationMiddleware( RequestDelegate next, ILogger logger) { private const string CorrelationIdHeader = "X-Correlation-Id"; public async Task InvokeAsync(HttpContext context) { if (!context.Request.Headers.TryGetValue(CorrelationIdHeader, out var correlationId)) { correlationId = Guid.NewGuid().ToString(); context.Request.Headers[CorrelationIdHeader] = correlationId!; } context.Response.Headers[CorrelationIdHeader] = correlationId!; using (logger.BeginScope(new Dictionary { ["CorrelationId"] = correlationId.ToString() })) { await next(context); } } } ``` Global exception middleware: ``` public class GlobalExceptionMiddleware( RequestDelegate next, ILogger logger) { public async Task InvokeAsync(HttpContext context) { try { await next(context); } catch (Exception ex) { logger.LogError(ex, "Unhandled exception for {Path}", context.Request.Path); context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; var problem = new { title = "Unexpected error", status = StatusCodes.Status500InternalServerError, correlationId = context.Response.Headers["X-Correlation-Id"].ToString() }; await context.Response.WriteAsJsonAsync(problem); } } } ``` Update `Program.cs` to enforce this entry point: ``` var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddAuthentication("Cookies").AddCookie(); builder.Services.AddAuthorization(); builder.Services.AddScoped(); var app = builder.Build(); app.UseMiddleware(); app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); ``` Now rewrite the controller to be as thin as possible. ``` [ApiController] [Route("api/orders")] public class OrdersController(IOrderApplicationService orders) : ControllerBase { private readonly IOrderApplicationService _orders = orders; [HttpPost] public async Task PlaceOrder(PlaceOrderRequest request, CancellationToken ct) { // Authentication is handled globally if (!User.Identity?.IsAuthenticated ?? true) { return Unauthorized(); } var customerId = GetCustomerIdFromUser(User); // simple mapping var orderId = await _orders.PlaceOrderAsync(customerId, request, ct); return CreatedAtAction(nameof(GetById), new { id = orderId }, new { Id = orderId }); } [HttpGet("{id:guid}")] public async Task GetById(Guid id, CancellationToken ct) { var order = await _orders.GetDetailsAsync(id, ct); if (order is null) { return NotFound(); } return Ok(order); } private Guid GetCustomerIdFromUser(ClaimsPrincipal user) { var idClaim = user.FindFirst("sub") ?? user.FindFirst(ClaimTypes.NameIdentifier); if (idClaim is null) { throw new InvalidOperationException("Missing customer id claim."); } return Guid.Parse(idClaim.Value); } } ``` The heavy lifting has moved into `OrderApplicationService`. ``` public interface IOrderApplicationService { Task PlaceOrderAsync(Guid customerId, PlaceOrderRequest request, CancellationToken cancellationToken); Task GetDetailsAsync(Guid id, CancellationToken cancellationToken); } public sealed class PlaceOrderRequest { public List Lines { get; set; } = new(); } public sealed class OrderLineDto { public Guid ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } } public sealed class OrderDetailsDto { public Guid Id { get; set; } public Guid CustomerId { get; set; } public decimal TotalAmount { get; set; } public string Status { get; set; } = string.Empty; } public class OrderApplicationService(IOrderRepository orders, IUnitOfWork unitOfWork) : IOrderApplicationService { private readonly IOrderRepository _orders = orders; private readonly IUnitOfWork _unitOfWork = unitOfWork; public async Task PlaceOrderAsync( Guid customerId, PlaceOrderRequest request, CancellationToken cancellationToken) { await _unitOfWork.BeginAsync(cancellationToken); try { var lines = request.Lines .Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)) .ToList(); var order = Order.Create(customerId, lines); await _orders.AddAsync(order, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); return order.Id; } catch { await _unitOfWork.RollbackAsync(cancellationToken); throw; } } public async Task GetDetailsAsync( Guid id, CancellationToken cancellationToken) { var order = await _orders.GetByIdAsync(id, cancellationToken); if (order is null) { return null; } return new OrderDetailsDto { Id = order.Id, CustomerId = order.CustomerId, TotalAmount = order.TotalAmount, Status = order.Status.ToString() }; } } ``` The HTTP boundary is now: - Central and predictable in the pipeline. - Thin and focused in controllers. - Backed by a service layer that owns use case orchestration. That is the Front Controller and MVC at work, using built-in tools rather than inventing new ones. ## Minimal APIs: Front Controller Without Controllers Minimal APIs do not remove the Front Controller pattern. They only move it closer to the pipeline. A typical minimal setup: ``` var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.UseMiddleware(); app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); app.MapPost("/orders", async ( PlaceOrderRequest request, IOrderApplicationService orders, ClaimsPrincipal user, CancellationToken ct) => { if (!user.Identity?.IsAuthenticated ?? true) { return Results.Unauthorized(); } var customerId = GetCustomerIdFromUser(user); var id = await orders.PlaceOrderAsync(customerId, request, ct); return Results.Created($"/orders/{id}", new { Id = id }); }); app.Run(); ``` The pipeline still acts as the Front Controller. Your choice is how disciplined you stay: - You can keep auth, logging, and error handling in middleware. - You can drop all of that straight into every endpoint handler. The pattern works or fails based on that decision, not on whether you use controllers or Minimal APIs. ## When Front Controller and MVC Are Worth It You gain leverage from this pattern when: ### Multiple teams add endpoints over time If many developers are contributing to one service: - You want one place to enforce security policies. - You want consistent logs and metrics. - You want a single knob for rate limiting and feature flags. A Front Controller style pipeline gives you that control. MVC conventions keep controllers from becoming grab bags. ### Cross-cutting concerns are non-negotiable If your system must: - Log every request with correlation IDs. - Perform audit logging on sensitive routes. - Wrap responses in a uniform envelope. You need a consistent HTTP entry point. You cannot rely on each controller remembering to play along. ### You already have other enterprise patterns If you use: - Domain Model - Repositories - Service Layer - Unit of Work then there is no good reason to let controllers improvise infrastructure logic. Front Controller and MVC let you complete the picture. ## When Not To Overbuild Front Controller and MVC are tools. You do not need the full structure everywhere. Scenarios where a heavy pattern is overkill: ### Tiny internal APIs and experiments If you have: - A small internal tool - A prototype or throwaway service - A single developer touching the code then a few Minimal APIs with light middleware can be enough. Just keep an eye on growth. Once it becomes core, you can harden the boundary. ### Serverless function per endpoint models If each endpoint is deployed and scaled independently: - The function runtime already acts as a focused entry point. - Trying to layer a complex Front Controller abstraction on top may only add complexity. ### Very simple CRUD sites If the entire application is: - A straightforward CRUD over a handful of tables - No serious cross-cutting rules or policies you can often rely on stock ASP.NET Core conventions with a slim pipeline and modest controllers. In other words, treat the pattern as a response to complexity, not as a checklist item. ## Practical Steps In A Real Codebase If you suspect your HTTP boundary is out of control, a simple process looks like this: 1. Map the actual path of a request - List every middleware and filter that can run. - Read a couple of controllers end-to-end. - Note where auth, logging, error handling, and response shaping happen. 2. Decide what belongs at the front door - Authentication and authorization. - Correlation and logging. - Global exception handling. - Rate limiting, feature flags, audit hooks. 3. Move that logic to the pipeline - Create focused middleware for each concern. - Remove the duplicated code from controllers. - Keep controllers or Minimal APIs focused on orchestrating use cases. 4. Establish controller and endpoint conventions - No direct `DbContext` usage. - No local transaction handling. - Minimal branching. - Delegate to application services and repositories. You now have a real Front Controller and a sane MVC boundary, even if you never mention the pattern names in the code. ## Closing Thought Whatever controls your HTTP boundary controls your power. If you let every controller and endpoint do its own thing, you trade that power for short-term convenience. You also commit to a slow, expensive cleanup later. Treat the ASP.NET Core pipeline as your Front Controller on purpose. Treat controllers and Minimal APIs as thin adapters on purpose. Once you do that, the rest of your architecture finally has the chance to behave like a system instead of a collection of unrelated tricks. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Lazy Load Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-lazy-load-pattern/) **Published:** January 8, 2026 **Author:** Chris Woodruff **Content:** If a single endpoint pulls half your database just to render a small card on a mobile screen, your problem is not the database. Your problem is that you are afraid to say no. Lazy Load is how you say no. You refuse to pay for the cost of related data until it’s actually needed. Used with intent, it protects you from bloated object graphs and unnecessary joins. Used carelessly, it turns every property access into a silent query and your performance into a mystery. This post walks through: - What Lazy Load really is in a .NET context - A manual Lazy Load pattern in C# - How EF Core can do lazy loading for you and how it can hurt - Before and after Minimal API snippets - When the pattern earns its place and when you should avoid it ## The Problem: Loading Everything For Everyone Imagine a simple requirement: ***Show a small order summary in a list.*** You need: - Order Id - Order total - Customer name That is it. Here is what often shows up in production: ``` app.MapGet("/orders/{id:guid}", async ( Guid id, AppDbContext db, CancellationToken ct) => { var order = await db.Orders .Include(o => o.Customer) .Include(o => o.Lines) .ThenInclude(l => l.Product) .Include(o => o.LoyaltyEvents) .Include(o => o.AuditTrail) .SingleOrDefaultAsync(o => o.Id == id, ct); if (order is null) { return Results.NotFound(); } var dto = new { order.Id, order.TotalAmount, CustomerName = order.Customer.Name }; return Results.Ok(dto); }); ``` ``` app.MapGet("/orders/{id:guid}", async ( Guid id, AppDbContext db, CancellationToken ct) => { var order = await db.Orders .Include(o => o.Customer) .Include(o => o.Lines) .ThenInclude(l => l.Product) .Include(o => o.LoyaltyEvents) .Include(o => o.AuditTrail) .SingleOrDefaultAsync(o => o.Id == id, ct); if (order is null) { return Results.NotFound(); } var dto = new { order.Id, order.TotalAmount, CustomerName = order.Customer.Name }; return Results.Ok(dto); }); ``` That response only uses `Id`, `TotalAmount`, and `Customer.Name`. The rest of the graph is dead weight. Every call drags along: - All order lines - All products referenced by those lines - All loyalty events - All audit steps The endpoint does not care, but the database and network do. Lazy Load exists to push back on this habit. ## What The Lazy Load Pattern Actually Does In Fowler’s terms, Lazy Load: - Delays the loading of related data until it is actually needed - Caches that data on first load for the life of the object - Reduces cost for scenarios where you sometimes need the association but often do not Practical definition for .NET: - You represent a relationship as a method or property that can fetch the related entity on demand - The first call triggers a query - Later calls reuse the same instance You can rely on: - Framework support (EF Core lazy loading proxies), or - An explicit pattern you control Let us start with the explicit version. ## Manual Lazy Load In C# Take a domain slice where an order sometimes needs its customer details, but not always. ``` public sealed class Customer(Guid id, string name, string email) { public Guid Id { get; } = id; public string Name { get; private set; } = name; public string Email { get; private set; } = email; } public sealed class OrderWithLazyCustomer { private readonly Func _customerLoader; private Customer? _customer; public OrderWithLazyCustomer( Guid id, Guid customerId, decimal totalAmount, Func customerLoader) { Id = id; CustomerId = customerId; TotalAmount = totalAmount; _customerLoader = customerLoader; } public Guid Id { get; } public Guid CustomerId { get; } public decimal TotalAmount { get; } public async Task GetCustomerAsync() { if (_customer == null) { _customer = await _customerLoader(CustomerId); } return _customer; } } ``` Key points: - `OrderWithLazyCustomer` knows a `CustomerId`, not a `Customer` instance - `GetCustomerAsync` loads the customer only on first access - The loader delegate is injected so the domain does not know about `DbContext` A repository can provide that loader. ``` public interface ICustomerRepository { Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); } public interface IOrderRepository { Task GetOrderWithLazyCustomerAsync( Guid id, CancellationToken cancellationToken = default); } public sealed class EfCoreOrderRepository(AppDbContext dbContext, ICustomerRepository customers) : IOrderRepository { private readonly ICustomerRepository _customers = customers; public async Task GetOrderWithLazyCustomerAsync( Guid id, CancellationToken cancellationToken = default) { var entity = await dbContext.Orders .AsNoTracking() .SingleOrDefaultAsync(o => o.Id == id, cancellationToken); if (entity is null) { return null; } return new OrderWithLazyCustomer( entity.Id, entity.CustomerId, entity.TotalAmount, customerId => _customers.GetByIdAsync(customerId, cancellationToken)); } } ``` Now: - Getting an order is cheap - Getting the customer has a cost that the caller chooses to pay or avoid ## EF Core Lazy Loading: Power And Hazard EF Core can do lazy loading for you using proxies. ### How it works at a high level - You install `Microsoft.EntityFrameworkCore.Proxies` - You configure the context: ``` services.AddDbContext(options => { options.UseSqlServer(connectionString); options.UseLazyLoadingProxies(); }); ``` - You mark navigation properties as `virtual`: ``` public class Order { public Guid Id { get; set; } public Guid CustomerId { get; set; } public virtual Customer Customer { get; set; } = default!; public virtual ICollection Lines { get; set; } = new List(); // Other properties } ``` When code reads `order.Customer` for the first time, EF Core: - Detects the access on a proxy - Runs a query behind the scenes - Assigns the resulting `Customer` instance to the navigation You did not write a query. You did not call any loader method. That is the convenience and the danger. ### What goes wrong in practice - Serialization of an `Order` with lazy navigations can fire many hidden queries - A loop that touches `order.Customer` or `order.Lines` multiple times across a list of orders can produce classic N+1 patterns - It becomes hard to reason about how many queries a single endpoint will run Lazy Load is not a free performance boost. It is a tool that moves query decisions out of your code and into property access. If you use EF Core lazy loading, you need strong logging and discipline. Many teams prefer explicit patterns instead, because you can see queries in the code. ## Before vs After: Minimal API That Overloads Includes Return to the order summary example and make it concrete. ### Before: eager everything This endpoint: - Loads more than the client cares about - Pulls large graphs for simple views - Limits your ability to scale when data grows ### After: explicit Lazy Load for customer First, adjust the repository to return `OrderWithLazyCustomer`. ``` public sealed class EfCoreCustomerRepository(AppDbContext dbContext) : ICustomerRepository { public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { var entity = await dbContext.Customers .AsNoTracking() .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); if (entity is null) { throw new InvalidOperationException("Customer not found."); } return new Customer(entity.Id, entity.Name, entity.Email); } } ``` Then the endpoint uses the lazy variant: ``` app.MapGet("/orders/{id:guid}", async ( Guid id, IOrderRepository orders, CancellationToken ct) => { var order = await orders.GetOrderWithLazyCustomerAsync(id, ct); if (order is null) { return Results.NotFound(); } // Only load customer if the caller requested detailed info // For this example, we keep it simple and always load var customer = await order.GetCustomerAsync(); var dto = new { order.Id, order.TotalAmount, CustomerName = customer.Name }; return Results.Ok(dto); }); ``` To make the pattern more interesting, you can expose two endpoints: - `/orders/{id}` for summaries (no lazy load, only order data) - `/orders/{id}/details` that uses `GetCustomerAsync` and other loaders The point is that Lazy Load turns expensive associations into an explicit choice. ## Lazy Load Combined With Identity Map And Unit Of Work Lazy Load is rarely used alone in serious enterprise code. It sits with: - Identity Map: ensures one instance per entity inside a Unit of Work - Unit of Work: defines the transactional boundary - Repository: defines a domain-centric API for aggregates In EF Core, `DbContext` already brings Identity Map and Unit of Work behavior: - Tracked entities are unique per key within the context - `SaveChangesAsync` commits all tracked changes as a unit When you add Lazy Load on top: - Lazy queries must stay inside the context lifetime - You rely on the same identity map each time a lazy property triggers a query If you build a manual Lazy Load over repositories: - You decide which relationships are lazy - You do not rely on magic proxies - You can still respect one identity per entity by combining with your own Identity Map or with EF’s tracking rules ## When Lazy Load Helps Lazy Load earns its place when you refuse to load data you are not going to use. Typical situations: ### Large graphs with rare use - Orders with hundreds of lines - Customers with big document collections - Entities with heavy histories or audit trails Most requests do not need the full graph. Lazy Load lets you keep relationships while avoiding automatic eager loading. ### Multiple response shapes The same aggregate may serve: - List views that need summary fields - Detail views that need associated entities - Background jobs that need only identifiers and totals Lazy Load allows shared code paths to delay loading the heavy bits until a particular response shape requires them. ### Dealing with occasionally expensive relationships Some relationships are usually cheap, but occasionally blow up: - Most customers have a handful of orders; a few have thousands - Most products have a tiny history; a few have huge trails Lazy Load lets you keep the association available without always paying the worst-case cost. ## When Lazy Load Hurts Lazy Load is very good at hiding your performance problems until you are in production. You should avoid or strictly limit Lazy Load when: ### You cannot predict query counts If you cannot confidently estimate how many queries a given request will run, adding lazy loading is a bad idea. Every property access might hide a round trip. ### You already have N+1 issues Lazy Load is a classic way to turn: - One list of orders into - One query to fetch the list, plus N queries to fetch the customer for each order If you do not have strict discipline or query logging, this pattern will bite. ### Your graphs are small and predictable For simple applications: - With shallow relationships - Where eager loading is cheap and clear a single tuned query is easier to reason about than a maze of lazy loaders. ### You serialize domain objects directly When you hand lazy loaded domain objects directly to JSON or other serializers: - The serializer walks the graph - Each navigation access can fire a query - You end up in a cascade of lazy loads that you did not plan In that world, you require explicit DTOs and queries instead of letting Lazy Load hide the work. ## Bringing Lazy Load Discipline Into Your ASP.NET Core App A sensible path: 1. Turn on detailed EF Core logging in development. 2. Pick one hot endpoint and count the queries it runs. 3. Look for includes or navigations that are never used by the response. 4. Replace those with either: - Manual Lazy Load, or - Separate queries for separate response shapes. 5. Document which associations are lazy and why, so the next developer does not accidentally trigger them in a loop. Lazy Load is not an excuse to stop thinking about queries. It is a way to make your choices explicit. ## Closing Thought Eager loading everywhere is fear. Lazy loading everywhere is denial. The Lazy Load pattern is what you use when you are willing to design your access patterns instead of letting them emerge accidentally. Treat every expensive relationship as a question: - Does this flow really need that customer, those products, that history, right now? If the answer is often no, Lazy Load gives you a clean way to delay the cost without throwing away the relationship. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Identity Map Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-identity-map-pattern/) **Published:** January 6, 2026 **Author:** Chris Woodruff **Content:** If one customer quietly turns into three different in-memory objects during a single request, your domain is already lying to you. You see it when: - Different parts of a request each load the same row again - One instance is updated, another is validated, and a third is saved - Bugs show up as “lost updates” or strange race conditions that never reproduce cleanly in tests The Identity Map pattern exists to stop this drift. It gives you a single rule: ***Within a Unit of Work, there should be exactly one in-memory object per database identity.*** In other words, one row, one object. This post covers: - What is an Identity Map in practical .NET terms - How EF Core already gives you an identity map - How to implement one yourself when you are not using a tracking ORM - Before and after Minimal API examples - When this pattern is worth caring about and when you can safely ignore it ## What Identity Map Really Is Identity Map is simple at its core: - It keeps a lookup from identity (primary key) to entity instance - When code asks for entity X: - If X is already in memory, return that instance - If not, load X, put it in the map, and return the loaded instance It exists so that different parts of your code do not accidentally work on different copies of the “same” entity during a business operation. A minimal implementation in C# looks like this: ``` public class IdentityMap { private readonly Dictionary _entities = new(); public bool TryGet(TKey id, out TValue value) => _entities.TryGetValue(id, out value); public void Add(TKey id, TValue entity) { if (_entities.ContainsKey(id)) { throw new InvalidOperationException("Entity already in map."); } _entities[id] = entity; } public void Clear() => _entities.Clear(); } ``` This structure knows nothing about databases. It just enforces “one id, one instance” inside some scope. ## Identity Map Inside A Repository If you are not using a tracking ORM, you can wire Identity Map into your repositories. Imagine a `Customer` entity and a repository that uses Dapper or raw ADO.NET. ``` public sealed class Customer(int id, string email, decimal creditLimit) { public int Id { get; } = id; public string Email { get; private set; } = email; public decimal CreditLimit { get; private set; } = creditLimit; public void ChangeEmail(string newEmail) { if (string.IsNullOrWhiteSpace(newEmail)) { throw new ArgumentException("Email cannot be empty.", nameof(newEmail)); } Email = newEmail; } } ``` ``` public interface ICustomerRepository { Task FindAsync(int id, CancellationToken cancellationToken = default); Task SaveAsync(Customer customer, CancellationToken cancellationToken = default); } public class CustomerRepositoryWithIdentityMap(string connectionString) : ICustomerRepository { private readonly IdentityMap _identityMap = new(); public async Task FindAsync(int id, CancellationToken cancellationToken = default) { if (_identityMap.TryGet(id, out var cached)) { return cached; } await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( @"SELECT Id, Email, CreditLimit FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", id); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); if (!await reader.ReadAsync(cancellationToken)) { return null; } var customer = new Customer( reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2)); _identityMap.Add(id, customer); return customer; } public async Task SaveAsync(Customer customer, CancellationToken cancellationToken = default) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( @"UPDATE Customers SET Email = @Email, CreditLimit = @CreditLimit WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", customer.Id); cmd.Parameters.AddWithValue("@Email", customer.Email); cmd.Parameters.AddWithValue("@CreditLimit", customer.CreditLimit); await cmd.ExecuteNonQueryAsync(cancellationToken); } } ``` Repository with an Identity Map: Any code that asks this repository for `FindAsync(42)` during the life of this repository will receive the same `Customer` instance. That is Identity Map in the raw. ## EF Core Already Acts As An Identity Map EF Core’s `DbContext` already gives you this behavior: - The first time you load `Customer` with key 42 into a context, EF creates a tracked instance - The second time you query for that same row, EF returns the same instance as long as the query is tracked - The change tracker is exactly an Identity Map keyed by primary key, plus some state metadata This is why attaching, detaching, and mixing `AsNoTracking` queries carelessly causes so much confusion. You are fighting the built-in Identity Map without acknowledging it. The real question is not whether you use Identity Map. You already do if you use EF Core. The real question is whether you understand where its boundaries are. ## Before: Minimal API That Accidentally Creates Multiple Customers Consider a minimal endpoint that updates a customer’s email and triggers a loyalty service that also works with the customer. ``` app.MapPost("/customers/{id:int}/update-email", async ( int id, UpdateCustomerEmailRequest dto, AppDbContext db, LoyaltyService loyalty, CancellationToken ct) => { // First load var customer = await db.Customers.FindAsync(new object[] { id }, ct); if (customer is null) { return Results.NotFound(); } customer.Email = dto.NewEmail; // LoyaltyService uses its own DbContext and AsNoTracking await loyalty.TrackEmailChangedAsync(id, dto.NewEmail, ct); await db.SaveChangesAsync(ct); return Results.Ok(new { customer.Id, customer.Email }); }); ``` A naive `LoyaltyService` implementation: ``` public class LoyaltyService(IDbContextFactory dbContextFactory) { private readonly IDbContextFactory _dbContextFactory = dbContextFactory; public async Task TrackEmailChangedAsync( int customerId, string newEmail, CancellationToken cancellationToken = default) { await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); // Second load (different DbContext) var customer = await db.Customers .AsNoTracking() .SingleOrDefaultAsync(c => c.Id == customerId, cancellationToken); if (customer is null) { return; } db.LoyaltyEvents.Add(new LoyaltyEvent { CustomerId = customer.Id, NewEmail = newEmail, OccurredAtUtc = DateTime.UtcNow }); await db.SaveChangesAsync(cancellationToken); } } ``` What is wrong here: - The endpoint and the loyalty service each have their own identity map (two DbContexts) - Each context has its own `Customer` instance - The loyalty operation commits independently of the main operation From a behavior perspective: - If the outer `SaveChangesAsync` fails, loyalty may already have recorded an event - If the inner `SaveChangesAsync` fails, the email update might succeed while loyalty stays uninformed Within a single request, there is no single notion of “the customer we are working with.” There are copies. ## After: One DbContext, One Identity Map, One Unit Of Work Refactor so that: - There is a single identity map per business operation - The Unit of Work and Identity Map boundaries line up - Loyalty depends on the same context instead of creating its own Wire your DbContext as scoped and your Unit of Work around it: ``` builder.Services.AddDbContext(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); ``` Loyalty writer that uses the shared context: ``` public interface ILoyaltyEventWriter { Task EmailChangedAsync(Customer customer, CancellationToken cancellationToken = default); } public class EfCoreLoyaltyEventWriter(AppDbContext dbContext) : ILoyaltyEventWriter { public Task EmailChangedAsync( Customer customer, CancellationToken cancellationToken = default) { dbContext.LoyaltyEvents.Add(new LoyaltyEvent { CustomerId = customer.Id, NewEmail = customer.Email, OccurredAtUtc = DateTime.UtcNow }); // No SaveChangesAsync here. Unit of Work will handle it. return Task.CompletedTask; } } ``` An application service that works with repositories and Unit of Work: ``` public sealed class UpdateCustomerEmailRequest { public string NewEmail { get; set; } = string.Empty; } public class CustomerApplicationService( ICustomerRepository customers, ILoyaltyEventWriter loyaltyEvents, IUnitOfWork unitOfWork) { private readonly ICustomerRepository _customers = customers; private readonly ILoyaltyEventWriter _loyaltyEvents = loyaltyEvents; public async Task UpdateEmailAsync( int id, string newEmail, CancellationToken cancellationToken = default) { await unitOfWork.BeginAsync(cancellationToken); try { var customer = await _customers.FindAsync(id, cancellationToken); if (customer is null) { throw new InvalidOperationException("Customer not found."); } customer.ChangeEmail(newEmail); await _loyaltyEvents.EmailChangedAsync(customer, cancellationToken); await unitOfWork.CommitAsync(cancellationToken); } catch { await unitOfWork.RollbackAsync(cancellationToken); throw; } } } ``` Minimal API endpoint: ``` app.MapPost("/customers/{id:int}/update-email", async ( int id, UpdateCustomerEmailRequest dto, CustomerApplicationService service, CancellationToken ct) => { try { await service.UpdateEmailAsync(id, dto.NewEmail, ct); return Results.Ok(new { Id = id, dto.NewEmail }); } catch (InvalidOperationException ex) { return Results.BadRequest(ex.Message); } }); ``` Now: - There is exactly one `AppDbContext` per request - That context provides the identity map for `Customer` - Both customer update and loyalty event work with the same `Customer` instance - There is one commit point via Unit of Work Identity Map and Unit of Work now work together rather than fight each other. ## A Custom Identity Map For Non EF Data Access If you are using Dapper or a micro ORM without change tracking, you may want a more explicit pattern. Create a Unit of Work that owns an Identity Map and a collection of repositories. ``` public interface IDapperUnitOfWork : IAsyncDisposable { IdentityMap Customers { get; } Task GetOpenConnectionAsync(CancellationToken cancellationToken = default); Task CommitAsync(CancellationToken cancellationToken = default); } ``` ``` public class DapperUnitOfWork(string connectionString) : IDapperUnitOfWork { private readonly IdentityMap _customers = new(); private IDbConnection? _connection; private IDbTransaction? _transaction; public IdentityMap Customers => _customers; public async Task GetOpenConnectionAsync( CancellationToken cancellationToken = default) { if (_connection is not null) return _connection; var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); _connection = conn; _transaction = _connection.BeginTransaction(); return _connection; } public async Task CommitAsync(CancellationToken cancellationToken = default) { _transaction?.Commit(); await DisposeAsync(); } public async ValueTask DisposeAsync() { if (_transaction is not null) { await Task.Run(() => _transaction.Dispose()); _transaction = null; } if (_connection is not null) { await _connection.DisposeAsync(); _connection = null; } _customers.Clear(); } } ``` Simple implementation: You would then implement a Dapper-based `CustomerRepository` that: - Uses `DapperUnitOfWork.Customers` as its Identity Map - Uses the shared connection and transaction from `GetOpenConnectionAsync` Within that Unit of Work, any call that asks for customer 42 gets the same instance. Identity Map is not about fancy data structures. It is about discipline. ## When To Lean Into an Identity Map Identity Map is worth your attention when: - You have a rich domain model with behavior, not just DTOs - Multiple services or repositories may touch the same entity within an operation - You care about invariants that should apply to one instance, not three copies If you are already using EF Core with a single scoped `DbContext` and a layered architecture, your main job is to respect the identity map EF gives you: - Do not create multiple DbContexts per operation unless you really mean it - Avoid random `AsNoTracking` reads in the middle of workflows that mutate entities - Keep persistence commits at the Unit of Work boundary If you are using non-tracking data access, consider adding an explicit Identity Map to your Unit of Work. Otherwise, you will keep rediscovering the same bug: one database row, many in-memory truths. ## When You Can Skip An Explicit Identity Map You can usually skip a custom Identity Map layer when: - Your application is simple CRUD over a database with thin entities - Each request touches a single entity once and writes it back - You rely on EF Core with one scoped context and never detach or reuse entities across contexts - Your queries are read-only analytics or reporting, where data is treated as immutable snapshots Even then, understanding that EF Core has an identity map prevents you from sabotaging it unintentionally. ## Closing Thought Identity Map is not glamorous. It is bookkeeping. But that bookkeeping decides whether your domain has a single in-memory view of reality or a pile of slightly different copies that fight each other at save time. The next time you see two different `Customer` instances for the same id in one request, ask yourself a blunt question: Is my problem the business rule, or is it the fact that my code cannot agree on which customer it is talking to? **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Unit of Work Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-unit-of-work-pattern/) **Published:** December 30, 2025 **Author:** Chris Woodruff **Content:** If a single business operation calls `SaveChangesAsync` three times, you do not have a transaction. You have a sequence of partial commits that you hope never fails in the middle. Think about a typical “Place Order” flow: - Create an order - Reserve inventory - Update customer credit - Write an audit log In many codebases, each step touches persistence on its own schedule. A service somewhere calls `SaveChangesAsync`. Another service does the same. A helper saves “just to be safe.” Then something throws after step two and before step four. Now you have an order without inventory, or inventory without an order, or an audit log that tells a story that never really happened. The Unit of Work pattern exists precisely to stop that. A Unit of Work: - Tracks changes to domain objects across a business operation - Commits them as a single, consistent change - Gives you a clear boundary: this use case either succeeds or fails as one In .NET, EF Core’s `DbContext` already behaves like a Unit of Work. The problem is that many teams leave that fact implicit and let `DbContext` leak everywhere. Naming Unit of Work explicitly lets you take control of persistence again. This post walks through: - A practical Unit of Work abstraction for ASP.NET Core and EF Core - Before and after Minimal API snippets - How it plays with repositories and application services - When the pattern earns its complexity and when it does not ## What the Unit of Work Pattern Really Is In Fowler’s terms, a Unit of Work: - Keeps track of everything you do during a business transaction - Knows how to persist those changes together - Ensures that either all changes are committed or none are In modern .NET: - `DbContext` tracks changes to entities - `SaveChanges` or `SaveChangesAsync` persists them, often in a transaction So why bother with a Unit of Work abstraction? Because without it, you get: - Multiple contexts per request - `SaveChangesAsync` calls scattered across repositories and services - No obvious place that defines “this is the commit point for this use case” A named Unit of Work lets you express that boundary in code. ## A Simple Unit of Work Abstraction Start with a minimal interface that conveys intent. ``` public interface IUnitOfWork { Task BeginAsync(CancellationToken cancellationToken = default); Task CommitAsync(CancellationToken cancellationToken = default); Task RollbackAsync(CancellationToken cancellationToken = default); } ``` Here is a straightforward EF Core implementation: ``` public class EfCoreUnitOfWork(AppDbContext dbContext) : IUnitOfWork { private readonly AppDbContext _dbContext = dbContext; public Task BeginAsync(CancellationToken cancellationToken = default) { // With one DbContext per request, EF Core will open a transaction when needed. // If you introduce explicit transactions, begin them here. return Task.CompletedTask; } public async Task CommitAsync(CancellationToken cancellationToken = default) { await _dbContext.SaveChangesAsync(cancellationToken); } public Task RollbackAsync(CancellationToken cancellationToken = default) { // If you add explicit transactions, roll back here. // With implicit transactions, you may rely on exceptions to abort work. return Task.CompletedTask; } } ``` This looks almost trivial. That is the point. It takes the implicit behavior of `DbContext` and turns it into an explicit concept that higher layers depend on. If later you decide to introduce explicit database transactions, outbox patterns, or cross-database coordination, you have a place to evolve that logic without rewriting business services. ## The Domain: Order And Customer To show a realistic Unit of Work, use a simple domain. ``` public enum OrderStatus { Draft, Active, Paid, Cancelled } public class Order { private readonly List _lines = new(); private Order(Guid id, Guid customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; } public Guid Id { get; } public Guid CustomerId { get; } public OrderStatus Status { get; private set; } public IReadOnlyCollection Lines => _lines.AsReadOnly(); public decimal TotalAmount => _lines.Sum(l => l.Total); public static Order Create(Guid customerId, IEnumerable lines) { var order = new Order(Guid.NewGuid(), customerId); foreach (var line in lines) { order.AddLine(line.ProductId, line.Quantity, line.UnitPrice); } if (!order._lines.Any()) { throw new InvalidOperationException("Order must have at least one line."); } order.Status = OrderStatus.Active; return order; } public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (quantity Quantity * UnitPrice; } public class Customer(Guid id, string email, decimal creditLimit) { public Guid Id { get; } = id; public string Email { get; private set; } = email; public decimal CreditLimit { get; private set; } = creditLimit; public decimal AvailableCredit { get; private set; } = creditLimit; public void ReserveCredit(decimal amount) { if (amount Set(); public DbSet Customers => Set(); public AppDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasKey(o => o.Id); modelBuilder.Entity() .Property(o => o.Status) .HasConversion(); modelBuilder.Entity() .HasMany(typeof(OrderLine), "_lines"); modelBuilder.Entity() .HasKey(c => c.Id); } } public class EfCoreOrderRepository(AppDbContext dbContext) : IOrderRepository { public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { return await dbContext.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, cancellationToken); } public Task AddAsync(Order order, CancellationToken cancellationToken = default) { dbContext.Orders.Update(order); return Task.CompletedTask; } } public class EfCoreCustomerRepository(AppDbContext dbContext) : ICustomerRepository { public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { return await dbContext.Customers .SingleOrDefaultAsync(c => c.Id == id, cancellationToken); } } ``` With this in place, you are ready to see how Unit of Work changes the shape of your code. ## Before: Place Order Endpoint With Ad Hoc SaveCalls Here is a Minimal API endpoint that coordinates placing an order, reserving credit, and writing a simple audit log. Everything uses `AppDbContext` directly. ``` app.MapPost("/orders/place", async ( PlaceOrderRequest dto, AppDbContext db, CancellationToken ct) => { var customer = await db.Customers .SingleOrDefaultAsync(c => c.Id == dto.CustomerId, ct); if (customer is null) { return Results.NotFound("Customer not found."); } // Reserve credit and save immediately var orderLines = dto.Lines .Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)) .ToList(); var prospectiveOrder = Order.Create(dto.CustomerId, orderLines); var amount = prospectiveOrder.TotalAmount; try { customer.ReserveCredit(amount); } catch (InvalidOperationException ex) { return Results.BadRequest(ex.Message); } await db.SaveChangesAsync(ct); // First commit // Create and save order separately db.Orders.Add(prospectiveOrder); await db.SaveChangesAsync(ct); // Second commit // Write audit log separately db.AuditLogs.Add(new AuditLog { Id = Guid.NewGuid(), OccurredAtUtc = DateTime.UtcNow, Message = $"Order {prospectiveOrder.Id} placed for customer {customer.Id}." }); await db.SaveChangesAsync(ct); // Third commit return Results.Created($"/orders/{prospectiveOrder.Id}", new { prospectiveOrder.Id, prospectiveOrder.Status, prospectiveOrder.TotalAmount }); }); ``` Problems: - Three separate `SaveChangesAsync` calls for one logical operation - If something fails after credit is reserved but before the order is written, the system is inconsistent - There is no single place you can point to and say “this is the commit for Place Order” This is exactly the situation Unit of Work is meant to address. ## After: Place Order Endpoint With Unit Of Work Introduce an application service that knows how to place an order inside a Unit of Work boundary. ``` public sealed class PlaceOrderRequest { public Guid CustomerId { get; set; } public List Lines { get; set; } = new(); } public sealed class OrderLineDto { public Guid ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } } public interface IAuditLogWriter { Task LogAsync(string message, CancellationToken cancellationToken = default); } public class EfCoreAuditLogWriter(AppDbContext dbContext) : IAuditLogWriter { public Task LogAsync(string message, CancellationToken cancellationToken = default) { dbContext.Add(new AuditLog { Id = Guid.NewGuid(), Message = message, OccurredAtUtc = DateTime.UtcNow }); return Task.CompletedTask; } } ``` Application service: ``` public interface IOrderApplicationService { Task PlaceOrderAsync( PlaceOrderRequest request, CancellationToken cancellationToken = default); } public class OrderApplicationService( IOrderRepository orders, ICustomerRepository customers, IAuditLogWriter auditLog, IUnitOfWork unitOfWork) : IOrderApplicationService { public async Task PlaceOrderAsync( PlaceOrderRequest request, CancellationToken cancellationToken = default) { await unitOfWork.BeginAsync(cancellationToken); try { var customer = await customers.GetByIdAsync(request.CustomerId, cancellationToken); if (customer is null) { throw new InvalidOperationException("Customer not found."); } var lines = request.Lines .Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)) .ToList(); var order = Order.Create(request.CustomerId, lines); customer.ReserveCredit(order.TotalAmount); await orders.AddAsync(order, cancellationToken); await auditLog.LogAsync( $"Order {order.Id} placed for customer {customer.Id}.", cancellationToken); await unitOfWork.CommitAsync(cancellationToken); return order.Id; } catch { await unitOfWork.RollbackAsync(cancellationToken); throw; } } } ``` Minimal API endpoint becomes thin: ``` app.MapPost("/orders/place", async ( PlaceOrderRequest dto, IOrderApplicationService service, CancellationToken ct) => { try { var orderId = await service.PlaceOrderAsync(dto, ct); return Results.Created($"/orders/{orderId}", new { Id = orderId }); } catch (InvalidOperationException ex) { return Results.BadRequest(ex.Message); } }); ``` Now the business operation: - Begins a unit of work - Loads customer and validates credit - Creates the order aggregate - Reserves credit - Writes an audit log - Commits exactly once If anything fails before `CommitAsync`, `SaveChangesAsync` never runs. If you later add explicit transactions, `RollbackAsync` will actually revert the database state. The important part is not the specific implementation. The fact that you have made the operation’s commit point visible. ## Using Explicit Database Transactions If you want stronger control, you can extend `EfCoreUnitOfWork` to manage explicit transactions. ``` using Microsoft.EntityFrameworkCore.Storage; public class EfCoreUnitOfWorkWithTransaction(AppDbContext dbContext) : IUnitOfWork { private IDbContextTransaction? _currentTransaction; public async Task BeginAsync(CancellationToken cancellationToken = default) { if (_currentTransaction is not null) { return; } _currentTransaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); } public async Task CommitAsync(CancellationToken cancellationToken = default) { await dbContext.SaveChangesAsync(cancellationToken); if (_currentTransaction is not null) { await _currentTransaction.CommitAsync(cancellationToken); await _currentTransaction.DisposeAsync(); _currentTransaction = null; } } public async Task RollbackAsync(CancellationToken cancellationToken = default) { if (_currentTransaction is not null) { await _currentTransaction.RollbackAsync(cancellationToken); await _currentTransaction.DisposeAsync(); _currentTransaction = null; } } } ``` Your application service remains unchanged. You just swap implementations in DI. ``` builder.Services.AddScoped(); ``` This is where the abstraction starts to pay off. ## When To Use The Unit Of Work Pattern Unit of Work earns its place in your architecture when you care about business-level consistency. Situations where it makes sense: ### Multi-repository operations A single use case touches: - Orders - Customers - Inventory - Audit logs You want: - All changes to succeed together - Or all of them to revert Unit of Work gives you that boundary. ### Non-trivial domain rules Money, reservations, and coordination of scarce resources rarely tolerate partial commits. If your domain has these kinds of rules, you want explicit control over when a transaction begins and commits. ### Rich domain model with repositories and service layer If you have already invested in: - Domain Model - Repository pattern - Application services Unit of Work is the glue that turns “a bunch of repository calls” into a single business operation with a clear commit point. ### Anticipated transactional complexity If you expect to add: - Outbox pattern - External message publishing that must align with the database state - Cross-service or cross-database coordination a Unit of Work abstraction is the future seam for that work. ## When Not To Use The Unit Of Work Pattern Unit of Work is not mandatory. Some scenarios do not justify the extra abstraction. Situations where it may be overkill: ### Simple CRUD with one repository If an endpoint: - Loads a single entity - Applies a small change - Calls `SaveChangesAsync` once then wrapping that in a full Unit of Work abstraction might add boilerplate. A scoped `DbContext` with a single commit at the end of the handler is often enough. ### Read-only operations Pure queries do not need a Unit of Work. Focus instead on projection models and performance. ### Truly independent actions Sometimes a single HTTP request triggers two operations that are intentionally independent, for example: - Logging analytics events - Updating user preferences If those do not need to roll back together, forcing them into one transaction may reduce throughput and complicate error handling. In that case, separate Units of Work or independent commits make sense. ### Anti-pattern: repositories that still call SaveChanges One common misuse: - You introduce `IUnitOfWork` - You keep calling `SaveChangesAsync` inside repository methods At that point, you have two commit concepts competing with each other. Decide which layer owns persistence commits. If you adopt Unit of Work, repositories should only modify tracked entities, not flush changes. ## Bringing Unit Of Work Into An Existing App You can retrofit the Unit of Work gradually. A simple path: 1. Search for every call to `SaveChangesAsync`. 2. Move those calls out of repositories and domain services into application services or endpoints. 3. Wrap the commit point for one nontrivial use case in `IUnitOfWork.CommitAsync`. 4. Introduce `BeginAsync` and `RollbackAsync` only if needed. 5. Observe how much easier it becomes to reason about what each operation actually does to the database. If the experiment makes the code clearer, continue with other use cases. If it does not, you learned something about your current complexity level. ## Closing Thought Unit of Work is not about ceremony. It is about honesty. Every non-trivial business operation already acts like a unit. Either the entire thing makes sense as a whole, or you end up explaining “partial success” to users and auditors. When you let `SaveChangesAsync` hide in random services, you deny that reality. When you give it a name and an interface, you acknowledge it and take control. Treat your important use cases as first-class units. Then make the database follow that decision, not the other way around. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Repository Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-repository-pattern/) **Published:** December 29, 2025 **Author:** Chris Woodruff **Content:** If `DbContext` shows up in every corner of your codebase, you do not have a domain model. You have a thin layer of LINQ wrapped in HTTP. You see it when: - Minimal API endpoints inject `AppDbContext` and write queries inline - Domain services take `DbContext` instead of domain interfaces - Every feature invents its own way to load the same `Order` or `Customer` A tiny change in schema or query behavior then turns into a scavenger hunt across controllers, services, and helpers. The Repository pattern exists to stop that. A repository gives you a **collection-like gateway** for an aggregate. Application code asks the repository for `Order` or `Customer` in domain terms. The repository hides SQL, ORM setup, and query shapes. It becomes the one place where you tune how aggregates are loaded and persisted. In this post, you will see: - What the Repository pattern is in practical .NET terms - A complete `IOrderRepository` and `EfCoreOrderRepository` example - Before and after Minimal API snippets that move from `DbContext` to Repository - When the pattern pays off and when it is just noise ## What the Repository Pattern Actually Is A repository represents a **collection of aggregate roots**. Instead of sprinkling queries everywhere, you give your domain a small interface like: ``` public interface IOrderRepository { Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task FindActiveForCustomerAsync( Guid customerId, CancellationToken cancellationToken = default); Task AddAsync(Order order, CancellationToken cancellationToken = default); Task RemoveAsync(Order order, CancellationToken cancellationToken = default); } ``` Repository responsibilities: - Hide queries and persistence details - Expose methods that work in domain terms, not in SQL terms - Provide a single, consistent access point for a given aggregate Fowler places Repository in the object-relational patterns as a way to further isolate domain logic from data access. In practice, that means **domain code never sees `DbContext` or SQL at all**. It talks to repositories. ## The Order Aggregate: A Quick Domain Model To make this concrete, start with a small `Order` aggregate. ``` public enum OrderStatus { Draft, Active, Cancelled, Completed } public class Order { private readonly List _lines = new(); private Order(Guid id, Guid customerId) { Id = id; CustomerId = customerId; Status = OrderStatus.Draft; } public Guid Id { get; } public Guid CustomerId { get; } public OrderStatus Status { get; private set; } public IReadOnlyCollection Lines => _lines.AsReadOnly(); public decimal TotalAmount => _lines.Sum(l => l.Total); public static Order Create(Guid customerId, IEnumerable lines) { var order = new Order(Guid.NewGuid(), customerId); foreach (var line in lines) { order.AddLine(line.ProductId, line.Quantity, line.UnitPrice); } if (!order._lines.Any()) { throw new InvalidOperationException("Order must have at least one line."); } order.Status = OrderStatus.Active; return order; } public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft && Status != OrderStatus.Active) { throw new InvalidOperationException("Can only change draft or active orders."); } if (quantity Quantity * UnitPrice; } ``` Note what is *not* there: - No `DbContext` - No mapping attributes - No SQL knowledge `Order` cares about behavior. Persistence will live somewhere else. ## Before: Minimal API Endpoints With `DbContext` Everywhere Here is a typical Minimal API endpoint that cancels an order directly using `AppDbContext`. ``` app.MapPost("/orders/{id:guid}/cancel", async ( Guid id, AppDbContext db, CancellationToken ct) => { var orderEntity = await db.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, ct); if (orderEntity is null) { return Results.NotFound(); } if (orderEntity.Status == OrderStatus.Completed) { return Results.BadRequest("Completed orders cannot be cancelled."); } orderEntity.Status = OrderStatus.Cancelled; await db.SaveChangesAsync(ct); return Results.Ok(new { orderEntity.Id, orderEntity.Status }); }); ``` A few problems: - Endpoint knows which includes it needs - Endpoint knows about status rules - Endpoint is responsible for translating domain rules into property changes - If the cancellation rule changes, you touch this endpoint and any other place that cancels orders Now imagine you have: - Another endpoint that cancels from an admin UI - A background worker that cancels expired orders Each one might be implementing its own variation of “how cancellation works.” ## After: Minimal API Endpoints Using `IOrderRepository` Now introduce a repository and let the domain enforce its own rules. First, the repository interface: ``` public interface IOrderRepository { Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task FindActiveForCustomerAsync( Guid customerId, CancellationToken cancellationToken = default); Task AddAsync(Order order, CancellationToken cancellationToken = default); Task RemoveAsync(Order order, CancellationToken cancellationToken = default); } ``` Then the endpoint was refactored to use it: ``` app.MapPost("/orders/{id:guid}/cancel", async ( Guid id, IOrderRepository orders, CancellationToken ct) => { var order = await orders.GetByIdAsync(id, ct); if (order is null) { return Results.NotFound(); } try { order.Cancel(); } catch (InvalidOperationException ex) { return Results.BadRequest(ex.Message); } await orders.AddAsync(order, ct); // in many setups, tracked aggregates are updated automatically return Results.Ok(new { order.Id, order.Status }); }); ``` Now: - The endpoint does not know how to query an order - The endpoint does not encode the cancellation rule - The endpoint calls `order.Cancel()`, and the domain decides whether that is allowed Cancellation logic lives in one place: `Order.Cancel`. You can reuse it from: - Other endpoints - A background worker - A scheduled job without duplicating rule logic. ## Implementing `IOrderRepository` With EF Core You need a concrete implementation in infrastructure. There are two common styles: - Domain entities are also EF Core entities - Domain entities are separate from EF Core entities, and you map between them To keep the example focused, assume `Order` and `OrderLine` are EF Core entities as shown. ``` public class AppDbContext : DbContext { public DbSet Orders => Set(); public AppDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var order = modelBuilder.Entity(); order.HasKey(o => o.Id); order.Property(o => o.Status) .HasConversion(); var line = modelBuilder.Entity(); line.HasKey(l => new { l.ProductId, l.Quantity, l.UnitPrice }); order.HasMany(typeof(OrderLine), "_lines"); } } ``` Repository implementation: ``` public class EfCoreOrderRepository : IOrderRepository { private readonly AppDbContext _dbContext; public EfCoreOrderRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task GetByIdAsync( Guid id, CancellationToken cancellationToken = default) { return await _dbContext.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task FindActiveForCustomerAsync( Guid customerId, CancellationToken cancellationToken = default) { return await _dbContext.Orders .Include(o => o.Lines) .Where(o => o.CustomerId == customerId && o.Status == OrderStatus.Active) .ToListAsync(cancellationToken); } public Task AddAsync(Order order, CancellationToken cancellationToken = default) { // This covers both new and tracked aggregates depending on how you attach them _dbContext.Orders.Update(order); return Task.CompletedTask; } public Task RemoveAsync(Order order, CancellationToken cancellationToken = default) { _dbContext.Orders.Remove(order); return Task.CompletedTask; } } ``` Wire it up in `Program.cs`: ``` builder.Services.AddDbContext(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); }); builder.Services.AddScoped(); ``` From this point forward, everything outside the infrastructure talks only to `IOrderRepository`. ## Before vs After: Querying Orders For A Customer One more example: listing a customer’s active orders. ### Before: Endpoint Owns The Query ``` app.MapGet("/customers/{customerId:guid}/orders/active", async ( Guid customerId, AppDbContext db, CancellationToken ct) => { var orders = await db.Orders .Include(o => o.Lines) .Where(o => o.CustomerId == customerId && o.Status == OrderStatus.Active) .Select(o => new { o.Id, o.Status, Total = o.Lines.Sum(l => l.Total) }) .ToListAsync(ct); return Results.Ok(orders); }); ``` The endpoint knows: - How to filter by status - That lines should be included - That total should be computed by summing line totals Any other feature that needs “active orders for customer” will reinvent this. ### After: Endpoint Delegates To `FindActiveForCustomerAsync` ``` app.MapGet("/customers/{customerId:guid}/orders/active", async ( Guid customerId, IOrderRepository orders, CancellationToken ct) => { var activeOrders = await orders.FindActiveForCustomerAsync(customerId, ct); var response = activeOrders.Select(o => new { o.Id, o.Status, Total = o.TotalAmount }); return Results.Ok(response); }); ``` Now the rules about what “active orders” mean, and how to load them efficiently, live in the repository. The endpoint just shapes the response. If you later decide: - Active should exclude certain edge cases - Orders should always be loaded with a new related entity - Performance requires changing `Include` strategy you change it in one place. ## Repository vs Data Mapper vs EF Core You may ask: if EF Core is already a mapper, why add repositories? Think in layers: - EF Core is a **Data Mapper** implementation. It maps objects to tables. - A Repository is a **domain-facing facade** over that mapper. It hides EF specifics and speaks domain language. Comparisons: - Direct EF Core in the domain or endpoints - Quick to start - Strong coupling to ORM and database schema - Queries scattered everywhere - Data Mapper only - Explicit mapping code between domain objects and records - Still no consistent collection-like abstraction - Repository on top - Aggregates are retrieved and persisted through a single gateway - Domain and application services work in terms of `Order` and `Customer`, not `DbSet` You can use EF Core directly in repository implementations. The repository makes sure the rest of your code does not care. ## When To Use The Repository Pattern A repository is not a rule. It is a tradeoff. It pays off in specific situations. ### 1. Central Aggregates Used Everywhere If an aggregate like `Order` appears in: - Checkout flows - Customer dashboards - Admin tools - Background processes then you want one definitive place that controls: - How it is loaded - Which relationships are included - How archived or soft-deleted data is filtered That is what a repository is for. ### 2. You Want Consistent Access Rules You may have cross-cutting rules such as: - All queries should filter out soft-deleted records - All active orders must eagerly load lines to avoid N+1 issues Repositories let you express these once. Without them, you rely on every caller remembering to apply the same filters and includes. ### 3. Multiple Stores Or Read Models If there is any chance you will: - Add a read-optimized store - Introduce caching for certain aggregates - Split reads and writes across different data sources a repository interface gives you the seam you need. Implement it differently for different scenarios; the rest of the system continues to call the same methods. ### 4. Rich Domain Model With Real Behavior If you invested in domain objects that encapsulate rules, you want those objects to be loaded and persisted in a disciplined way. Repositories are the natural companion to a Domain Model and Service Layer. ## When Not To Use The Repository Pattern Sometimes a repository is just noise. ### 1. Tiny CRUD Applications If your app: - Has a handful of endpoints - Mirrors the database schema directly - Contains almost no business logic then a full Repository layer might slow you down more than it helps. A thin service over `DbContext` or even direct endpoint queries can be acceptable. ### 2. One-Off Tools And Scripts For: - Migration utilities - Maintenance scripts - Quick internal data tools introducing repositories and interfaces often adds ceremony without long-term benefit. These tools are not your core domain. ### 3. Heavy Reporting And Projections Complex read queries that: - Join across multiple bounded contexts - Aggregate data for dashboards and analytics may be better served by dedicated query handlers that return projections rather than aggregates. Forcing them through an aggregate repository can make designs confusing. ### 4. Generic `IRepository` Everywhere The most common anti-pattern: ``` public interface IRepository { Task GetByIdAsync(Guid id); Task AddAsync(T entity); Task RemoveAsync(T entity); } ``` This encourages: - Anemic domain models with generic operations only - No domain-specific methods like `FindActiveForCustomerAsync` - A false sense of abstraction without meaningful domain language If you go with repositories, make them **specific to aggregates** and name methods in domain terms. ## Introducing Repositories Into An Existing App You do not have to refactor everything. A pragmatic path: 1. Pick one high-value aggregate, such as `Order`. 2. Design a repository interface that expresses what callers actually need, in domain terms. 3. Implement it once using your existing data access code. 4. Update a few key endpoints or services to depend on the repository instead of `DbContext`. 5. Observe how much duplication and cognitive load disappears around that aggregate. If it feels cleaner and safer, continue with other aggregates. If it feels like ceremony for your situation, stop there. ## Closing Thoughts A repository is not a magical pattern. It is a disciplined way to say: - “All access to this aggregate goes through this one door.” In ASP.NET Core Minimal APIs, that single decision shifts your structure: - Endpoints become thinner, focused on HTTP concerns - Domain objects hold rules and behavior - Repositories become the single source of truth for how aggregates are loaded and persisted The next time you reach for `DbContext` inside an endpoint or domain service, ask yourself a simple question: *Should this code really know how orders are stored, or should it just ask for an `Order` and let a repository handle the rest?* **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Debugging Entity Framework Core: 8 Real-World Query Anti‑Patterns (and How to Fix Them)](https://www.woodruff.dev/debugging-entity-framework-core-8-real-world-query-anti-patterns-and-how-to-fix-them/) **Published:** December 4, 2025 **Author:** Chris Woodruff **Content:** *This is my post for the 2025 C# Advent. Check out* [*all the great posts*](https://www.csadvent.christmas/?ref=woodruff.dev)*!* I want to wish you a Merry Christmas, Happy Holidays, Happy Hanukkah, Happy Kwanzaa, and, finally, Happy Festivus. I will not be sharing my “Airing of grievances” or challenge anyone to a “Feats of strength.” Entity Framework Core is an excellent library for CRUD operations against a database. It is great to use LINQ to create database queries and retrieve data. What I am most surprised about is that many developers will not look at queries that fetch data, even if they may not be the “quickest.” Developers aren’t deliberately doing anything. They don’t have the skills or insight to address some of the anti-patterns in this blog post. I hope you get out as much as I did, creating the original conference talk and ASP.NET Core demo. This “Bad Book Store” demo intentionally models a “bad” SQL schema to surface common anti‑patterns in Entity Framework Core queries. Each scenario shows: the LINQ shape, why it’s slow (what SQL Server does with it), and two levels of fixes: quick DB-side mitigations and proper data model changes that make EF Core fast by design. You can find the demo at the following GitHub repository. https://github.com/cwoodruff/DebuggingEFCoreMSSQL (the project is in the demo folder) Relevant files to peek at: Relevant files to peek at: - `Pages/Demo/Index.cshtml.cs` — the 8 demo queries and explanations - `Data/BadBookStoreContext.cs` — the EF Core model; note the many `nvarchar`-typed date/number fields - `Data/FixScripts.cs` — T‑SQL fixes (computed columns + indexes) You can find the database BAK file and SQL script to create the database here. https://github.com/cwoodruff/DebuggingEFCoreMSSQL/tree/master/database I recommend creating the MSSQL database (with data) to walk through the demo and review the code for this blog post. ## Query 1 — Orders by `CustomerEmail` + Date Range (Date Stored as String) EF LINQ shape: ``` var start = "2023-01-01"; var end = "2025-12-31"; var q1 = db.Orders .Where(o => o.CustomerEmail == "customer008@example.com" && string.Compare(o.OrderDate!, start) >= 0 && string.Compare(o.OrderDate!, end) < 0 && o.OrderStatus == "Completed") .Select(o => new { o.OrderId, o.OrderDate, o.OrderTotal }); ``` Why it’s slow - `OrderDate` is `nvarchar(30)`. String comparisons kill sargability and a composite seek path doesn’t exist. - Expect index/clustered scans, high logical reads. Fix V1 (quick DB-side) - Persist a computed `datetime2` from string and index it with email: - `ALTER TABLE Orders ADD OrderDate_dt AS TRY_CONVERT(datetime2(3), OrderDate) PERSISTED;` - `CREATE INDEX IX_Orders_CustomerEmail_OrderDate_dt ON dbo.Orders(CustomerEmail, OrderDate_dt) INCLUDE (OrderTotal, OrderStatus);` Right fix (model + EF) - Store `OrderDate` as `datetime2` in the table. In EF, map it as `DateTime`/`DateTime?`. - Add a composite index in the model builder/migration: ``` modelBuilder.Entity() .HasIndex(o => new { o.CustomerEmail, o.OrderDate }); ``` - Write normal range predicates on `DateTime` — EF translates to a seekable range scan. ## Query 2 — Join `Reviews` ↔ `Books` by `Title` (Wide, Non‑Unique Text) EF LINQ shape: ``` var q2 = from r in db.Reviews join b in db.Books on r.BookTitle equals b.Title where r.Rating >= 4 select new { r.ReviewId, b.Isbn, b.Title, r.Rating }; ``` Why it’s slow - Joining on a wide, non‑unique `nvarchar(500)` yields hash joins with big memory grants or scans. Fix V1 (quick DB-side) - `CREATE INDEX IX_Books_Title ON dbo.Books(Title);` — helps the probe side, but it’s a band‑aid. Right fix (model + EF) - Use stable keys: `BookId` or `ISBN` (already the PK). Make `Reviews` store a FK to `Books`. - EF navigation-based join: ``` // After adding Review.BookIsbn FK → Book var q2 = db.Reviews .Where(r => r.Rating >= 4) .Select(r => new { r.ReviewId, r.Book!.Isbn, r.Book.Title, r.Rating }); ``` - Also index common filter columns on `Reviews` (e.g., `Rating`, `CustomerEmail`) as needed. ## Query 3 — Parent ↔ Child Join Without an Index on the FK (`OrderLines.OrderId`) EF LINQ shape: ``` var q3 = from o in db.Orders join ol in db.OrderLines on o.OrderId equals ol.OrderId where o.OrderStatus == "Completed" select new { o.OrderId, ol.BookTitle, ol.Quantity, ol.UnitPrice }; ``` Why it’s slow - Missing index on `OrderLines(OrderId)` means nested loops do repeated scans, or the optimizer resorts to hash joins with full scans. Fix V1 (quick DB-side) - `CREATE INDEX IX_OrderLines_OrderId ON dbo.OrderLines(OrderId) INCLUDE (BookTitle, Quantity, UnitPrice, Currency);` Right fix (model + EF) - Always index foreign keys. In migrations: ``` migrationBuilder.CreateIndex( name: "IX_OrderLines_OrderId", table: "OrderLines", column: "OrderId"); ``` - Prefer navigations: ``` var q3 = db.Orders .Where(o => o.OrderStatus == "Completed") .SelectMany(o => o.OrderLines.Select(ol => new { o.OrderId, ol.BookTitle, ol.Quantity, ol.UnitPrice })); ``` ## Query 4 — Inventory by `BookISBN` Under a String Composite Clustered Key EF LINQ shape: ``` var q4 = db.Inventories .Where(i => i.BookIsbn == "978-1-4028-0009-9") .Select(i => new { i.WarehouseCode, i.BookIsbn, i.QuantityOnHand }); ``` Why it’s slow - Table is clustered on `(WarehouseCode, BookISBN)` — a string composite. Filtering only by the second key without a supporting nonclustered index yields clustered scans. Fix V1 (quick DB-side) - `CREATE INDEX IX_Inventory_BookISBN ON dbo.Inventory(BookISBN);` Right fix (model + EF) - Use a narrow surrogate clustered key (e.g., `InventoryId INT IDENTITY`) and keep `(WarehouseCode, BookIsbn)` with targeted nonclustered indexes that match access patterns. ## Query 5 — `CategoryCsv` LIKE scans (CSV Anti‑Pattern) EF LINQ shape: ``` var category = "Programming"; var q5 = db.Books.Where(b => (b.CategoryCsv ?? "") == category || (b.CategoryCsv ?? "").StartsWith(category + ",") || (b.CategoryCsv ?? "").EndsWith("," + category) || (b.CategoryCsv ?? "").Contains("," + category + ",")) .Select(b => new { b.Isbn, b.Title, b.CategoryCsv }); ``` Why it’s slow - CSV-in-a-column prevents the optimizer from using set logic. Most patterns (`%LIKE%`) devolve to scans. StartsWith can be seekable in some cases, but CSV boundaries break it. Fix V1 (there isn’t a good one) - Indexes can’t fix a denormalized CSV membership test. This demo intentionally shows the limits of indexing. Right fix (model + EF) - Normalize: Do not use Books.CategoryCsv for storing comma delimited data. Use the following: `Book` ↔ `Category` via new `BookCategory` bridge table. Then write a proper join: ``` var q5 = from bc in db.BookCategories where bc.CategoryName == "Programming" join b in db.Books on bc.Isbn equals b.Isbn select new { b.Isbn, b.Title }; ``` - In EF Core 5+, many‑to‑many can be modeled directly; ensure indexes on bridging FK columns. ## Query 6 — Sorting `ActivityLog` by a Text Date Column EF LINQ shape: ``` var q6 = db.ActivityLogs .OrderByDescending(a => a.HappenedAt) .Select(a => new { a.ActivityId, a.HappenedAt, a.Actor, a.Action }); ``` Why it’s slow - `HappenedAt` is `nvarchar(30)`. Sorting requires full sort; large result sets can spill to tempdb. Fix V1 (quick DB-side) - Persist a computed datetime and an index that matches the order: - `ALTER TABLE ActivityLog ADD HappenedAt_dt AS TRY_CONVERT(datetime2(3), HappenedAt) PERSISTED;` - `CREATE INDEX IX_ActivityLog_HappenedAt_dt_DESC ON dbo.ActivityLog (HappenedAt_dt DESC);` Right fix (model + EF) - Store `HappenedAt` as `datetime2`. In EF, order by the typed column; SQL Server can perform an ordered seek and avoid a sort. ## Query 7 — FLOAT Money Math (Correctness > Performance) EF LINQ shape: ``` var sum = await db.OrderLines .Where(ol => ol.OrderId == orderId) .Select(ol => (ol.UnitPrice ?? 0) * (ol.Quantity ?? 0)) .SumAsync(); ``` What goes wrong - Monetary columns as `FLOAT` are imprecise. Binary floating‑point can’t represent many decimal fractions; totals vary and rounding errors accumulate. Fix V1 - There is no indexing or MSSQL fixes for wrong arithmetic. Right fix (model + EF) - Use `DECIMAL(19,4)` (or appropriate) everywhere money appears. In EF: ``` modelBuilder.Entity() .Property(p => p.UnitPrice) .HasPrecision(19, 4); ``` - Keep math on the server with `decimal`-typed expressions; ensure clients use `decimal` too. ## Query 8 — JSON‑ish LIKE Probe in `Orders.Meta` EF LINQ shape: ``` var q8 = db.Orders .Where(o => (o.Meta ?? "").Contains("\"source\":\"mobile\"")) .Select(o => new { o.OrderId, o.Meta }); ``` Why it’s slow - `nvarchar(max)` plus leading `%LIKE%` forces full scans. SQL Server has no statistics on JSON properties inside strings. Fix V1 (quick DB-side) - Project the attribute to a persisted column + index: - `ALTER TABLE Orders ADD Source AS JSON_VALUE(Meta, '$.source') PERSISTED;` - `CREATE INDEX IX_Orders_Source ON dbo.Orders(Source);` - Then filter on `Source = 'mobile'` (seekable). Right fix (model + EF) - Model important attributes as real columns, or map a computed column in EF: ``` modelBuilder.Entity() .Property("Source") .HasComputedColumnSql("JSON_VALUE([Meta], '$.source')", stored: true); ``` - Query `o => EF.Property(o, "Source") == "mobile"` to leverage the index. ## The “Fix V1” Pack (What the Demo Applies) I have added the logic to the demo project to fix the 8 queries that exhibit anti-patterns. The application can apply these “Fix V1” changes to improve EF Core queries. The following are the locations where you will find the code that applies the corrections to the database. `Data/FixScripts.cs` applies pragmatic changes without refactoring the app code: - Computed persisted columns for string dates and JSON projections: - `Orders.OrderDate_dt = TRY_CONVERT(datetime2(3), OrderDate)` - `Orders.Source = JSON_VALUE(Meta, '$.source')` - `ActivityLog.HappenedAt_dt = TRY_CONVERT(datetime2(3), HappenedAt)` - Targeted nonclustered indexes aligned to common predicates/orderings: - `IX_Orders_CustomerEmail_OrderDate_dt` (INCLUDE `OrderTotal, OrderStatus`) - `IX_OrderLines_OrderId` (INCLUDE covering columns) - `IX_Books_Title`, `IX_Inventory_BookISBN`, `IX_ActivityLog_HappenedAt_dt_DESC`, etc. These transform scans/sorts into seeks/ordered reads, dramatically cutting I/O and memory grants without touching LINQ. They’re great for triage but shouldn’t replace proper schema design. ## Quick Checklist You Can Apply Today - Dates/times as `datetime2`, not strings. - Composite indexes that match your filter prefix; INCLUDE for covering. - Index all FKs. - Join on keys, not wide text. - Normalize sets (no CSV in columns). - Prefer `StartsWith` over `%LIKE%`, consider full‑text for advanced search. - Project JSON attributes you query into computed/persisted columns and index them. - Money = `decimal` with explicit precision. - Narrow surrogate clustered keys; avoid `nvarchar` composites. - Measure, compare, iterate. ## Practical EF Core Snippets You Can Adopt - Composite index via migration: ``` modelBuilder.Entity() .HasIndex(e => new { e.CustomerEmail, e.OrderDate }); ``` - Computed column mapping (to match a DB persisted expression): ``` modelBuilder.Entity() .Property("OrderDate_dt") .HasColumnType("datetime2(3)") .HasComputedColumnSql("TRY_CONVERT(datetime2(3), [OrderDate])", stored: true); ``` - Query using a shadow/computed property so the index is used: ``` var q = db.Orders.Where(o => EF.Property(o, "OrderDate_dt") >= start && EF.Property(o, "OrderDate_dt") < end); ``` - Many‑to‑many instead of CSV: ``` modelBuilder.Entity() .HasIndex(bc => new { bc.Isbn, bc.CategoryName }); var booksInCategory = from bc in db.BookCategories where bc.CategoryName == category join b in db.Books on bc.Isbn equals b.Isbn select b; ``` ## Closing Thoughts EF Core performance is a partnership between LINQ shape and storage design. You’ll rarely “optimize” your way out of schema problems from the query layer alone. Use the quick mitigations shown here to triage production issues, but strive to align the types, keys, and indexes with how you query your data. That’s where EF Core really shines: when the model and the database agree on the semantics and the access paths.EF Core performance is a partnership between LINQ shape and storage design. You’ll rarely “optimize” your way out of schema problems from the query layer alone. Use the quick mitigations shown here to triage production issues, but strive to align the types, keys, and indexes with how you query your data. That’s where EF Core really shines: when the model and the database agree on the semantics and the access paths. **Categories:** Entity Framework Core **Tags:** .NET, C#, databases, dotnet, Entity Framework Core, MSSQL, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Data Mapper Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-data-mapper-pattern/) **Published:** December 19, 2025 **Author:** Chris Woodruff **Content:** If every interesting class in your system secretly knows a connection string, your domain is not a model. It is a thin layer of code on top of a data access layer. You see it when: - Domain classes inject `DbContext` directly - Entities call `SaveChanges` on their own - Simple rule tests require poking at a real database At that point, changing a column name feels risky. Changing an important rule feels worse because it’s tangled with SQL and infrastructure. The Data Mapper pattern exists to cut that knot. It handles moving data between your object model and your data store. The mapper worries about tables. The domain worries about behavior. This post walks through a clean C# implementation of the Data Mapper, shows a before-and-after Minimal API, and makes a clear case for when the pattern earns its complexity and when it does not. ## What Data Mapper Actually Does In plain terms, a Data Mapper: - Knows how to load domain objects from a data source - Knows how to persist those objects back to the data source - Keeps the domain model ignorant of any mapping details So your `Customer` class focuses on credit rules, not on how to run `SELECT` and `UPDATE`. As your rules and workflows grow beyond trivial, that split stops being academic. It becomes the difference between a model that can evolve and one that stays glued to the current schema. ## The Customer Domain Class: No SQL Allowed Start with a domain object that understands customers, not tables. ``` public class Customer { public Customer(int id, string email, decimal creditLimit) { Id = id; ChangeEmail(email); CreditLimit = creditLimit; } public int Id { get; } public string Email { get; private set; } = string.Empty; public decimal CreditLimit { get; private set; } public void ChangeEmail(string newEmail) { if (string.IsNullOrWhiteSpace(newEmail)) { throw new ArgumentException("Email cannot be empty.", nameof(newEmail)); } Email = newEmail; } public void UpgradeCredit(decimal newLimit) { if (newLimit < CreditLimit) { throw new InvalidOperationException( "New limit must not be lower than current limit."); } CreditLimit = newLimit; } } ``` Key traits: - No `DbContext` - No connection string - No SQL constructs The type knows how to validate and change its own state. It does not know how to reach the database. ## The Data Mapper Contract Next, define a mapper interface that describes how to move `Customer` instances to and from storage. ``` public interface ICustomerMapper { Task FindAsync(int id, CancellationToken cancellationToken = default); Task SaveAsync(Customer customer, CancellationToken cancellationToken = default); } ``` This interface belongs in your domain or application layer. It is an abstraction: the implementation might use raw ADO.NET, EF Core, Dapper, or something else entirely. ## A Concrete SQL Mapper Now implement `ICustomerMapper` using plain ADO.NET. In a real project you may wrap EF Core instead, but the principle is identical. ``` using System.Data; using System.Data.SqlClient; public class CustomerSqlMapper : ICustomerMapper { private readonly string _connectionString; public CustomerSqlMapper(string connectionString) { _connectionString = connectionString; } public async Task FindAsync( int id, CancellationToken cancellationToken = default) { await using var conn = new SqlConnection(_connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( "SELECT Id, Email, CreditLimit FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", id); await using var reader = await cmd.ExecuteReaderAsync( CommandBehavior.SingleRow, cancellationToken); if (!await reader.ReadAsync(cancellationToken)) { return null; } var customerId = reader.GetInt32(0); var email = reader.GetString(1); var creditLimit = reader.GetDecimal(2); return new Customer(customerId, email, creditLimit); } public async Task SaveAsync( Customer customer, CancellationToken cancellationToken = default) { await using var conn = new SqlConnection(_connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( @"UPDATE Customers SET Email = @Email, CreditLimit = @CreditLimit WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Email", customer.Email); cmd.Parameters.AddWithValue("@CreditLimit", customer.CreditLimit); cmd.Parameters.AddWithValue("@Id", customer.Id); await cmd.ExecuteNonQueryAsync(cancellationToken); } } ``` Now the responsibilities are clear. - `Customer` represents behavior and state - `CustomerSqlMapper` knows how to translate that state into SQL operations The domain model stays free to evolve. The mapper absorbs the pain of schema changes. ## Before: Minimal API That Talks Directly To SQL Take a simple use case: raise a customer’s credit limit. Here is the version that ignores Data Mapper and lets the endpoint juggle everything. ``` app.MapPost("/customers/{id:int}/upgrade-credit", async ( int id, UpgradeCreditRequest dto, IConfiguration config, CancellationToken ct) => { if (dto.NewLimit { var config = sp.GetRequiredService(); var connectionString = config.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Missing connection string."); return new CustomerSqlMapper(connectionString); }); ``` Then define the endpoint: ``` app.MapPost("/customers/{id:int}/upgrade-credit", async ( int id, UpgradeCreditRequest dto, ICustomerMapper mapper, CancellationToken ct) => { if (dto.NewLimit **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Active Record Pattern](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-active-record-pattern/) **Published:** December 15, 2025 **Author:** Chris Woodruff **Content:** Sometimes, your domain is really just rows in a table. You have a `Customers` table with `Id`, `Email`, and `CreditLimit`. You load a row, tweak a field, and write it back. That is the whole story. In that situation, introducing a complete Domain Model, Data Mapper, and Repository stack can feel like ceremony for ceremony’s sake. The **Active Record** pattern exists for precisely this scenario: when your object and your row are basically the same thing. In this post, we will: - Define the Active Record pattern in practical .NET terms - Build a complete `CustomerRecord` example using ADO.NET - Show how it appears “by accident” in ASP.NET Core Minimal APIs - Compare a “before” and “after” endpoint using Active Record - Outline when the pattern is a good fit, and when to walk away from it ## What Active Record Really Is In Fowler’s catalog, **Active Record** means: - Each object instance wraps a row in a database table - The class knows how to: - Load itself (or instances of itself) from the database - Save its changes back - Apply simple business operations In C#, that usually looks like: - A class with properties that mirror columns - Static methods like `Find` or `FindByEmail` that query and return instances - Instance methods like `Save`, `Delete`, and small domain operations The key point: **persistence logic and business logic live together in the same class**. ## A Concrete Active Record: `CustomerRecord` in C# Here is a fully fleshed out Active Record example using raw ADO.NET and SQL Server. ``` using System.Data; using System.Data.SqlClient; public class CustomerRecord(string connectionString) { // Columns public int Id { get; private set; } public string Email { get; private set; } = string.Empty; public decimal CreditLimit { get; private set; } // Factory for a brand-new customer (not yet in DB) public static CustomerRecord CreateNew( string connectionString, string email, decimal creditLimit) { if (string.IsNullOrWhiteSpace(email)) { throw new ArgumentException("Email cannot be empty.", nameof(email)); } if (creditLimit < 0) { throw new ArgumentOutOfRangeException(nameof(creditLimit)); } return new CustomerRecord(connectionString) { Email = email, CreditLimit = creditLimit }; } // Load one customer by Id public static async Task FindAsync( int id, string connectionString, CancellationToken cancellationToken = default) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( "SELECT Id, Email, CreditLimit FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", id); await using var reader = await cmd.ExecuteReaderAsync( CommandBehavior.SingleRow, cancellationToken); if (!await reader.ReadAsync(cancellationToken)) { return null; } return new CustomerRecord(connectionString) { Id = reader.GetInt32(0), Email = reader.GetString(1), CreditLimit = reader.GetDecimal(2) }; } // Insert or update, depending on whether Id is set public async Task SaveAsync(CancellationToken cancellationToken = default) { await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); if (Id == 0) { var insertCmd = new SqlCommand( @"INSERT INTO Customers (Email, CreditLimit) VALUES (@Email, @CreditLimit); SELECT SCOPE_IDENTITY();", conn); insertCmd.Parameters.AddWithValue("@Email", Email); insertCmd.Parameters.AddWithValue("@CreditLimit", CreditLimit); var result = await insertCmd.ExecuteScalarAsync(cancellationToken); Id = Convert.ToInt32(result); } else { var updateCmd = new SqlCommand( @"UPDATE Customers SET Email = @Email, CreditLimit = @CreditLimit WHERE Id = @Id;", conn); updateCmd.Parameters.AddWithValue("@Email", Email); updateCmd.Parameters.AddWithValue("@CreditLimit", CreditLimit); updateCmd.Parameters.AddWithValue("@Id", Id); await updateCmd.ExecuteNonQueryAsync(cancellationToken); } } public async Task DeleteAsync(CancellationToken cancellationToken = default) { if (Id == 0) { throw new InvalidOperationException("Cannot delete a transient customer."); } await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(cancellationToken); var cmd = new SqlCommand( "DELETE FROM Customers WHERE Id = @Id", conn); cmd.Parameters.AddWithValue("@Id", Id); await cmd.ExecuteNonQueryAsync(cancellationToken); Id = 0; } // Simple business behavior public void ChangeEmail(string newEmail) { if (string.IsNullOrWhiteSpace(newEmail)) { throw new ArgumentException("Email cannot be empty.", nameof(newEmail)); } Email = newEmail; } public void IncreaseCreditLimit(decimal delta) { if (delta { var connectionString = config.GetConnectionString("DefaultConnection"); if (connectionString is null) { return Results.Problem("Connection string missing."); } if (string.IsNullOrWhiteSpace(dto.Email)) { return Results.BadRequest("Email cannot be empty."); } await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(ct); // Load var selectCmd = new SqlCommand( "SELECT Id, Email, CreditLimit FROM Customers WHERE Id = @Id", conn); selectCmd.Parameters.AddWithValue("@Id", id); await using var reader = await selectCmd.ExecuteReaderAsync(ct); if (!await reader.ReadAsync(ct)) { return Results.NotFound(); } var currentEmail = reader.GetString(1); var creditLimit = reader.GetDecimal(2); // Business rule: here, in the endpoint if (currentEmail == dto.Email) { return Results.BadRequest("Email is unchanged."); } // Save var updateCmd = new SqlCommand( "UPDATE Customers SET Email = @Email WHERE Id = @Id", conn); updateCmd.Parameters.AddWithValue("@Email", dto.Email); updateCmd.Parameters.AddWithValue("@Id", id); await updateCmd.ExecuteNonQueryAsync(ct); return Results.Ok(new { Id = id, Email = dto.Email, CreditLimit = creditLimit }); }); public sealed class ChangeEmailRequest { public string Email { get; set; } = string.Empty; } ``` This works, but: - Persistence is tightly coupled to the endpoint - Business rule checks live in the endpoint - Reusing this logic elsewhere means copy and paste ## “After” Minimal API: Endpoint Using Active Record Now see the same use case implemented through `CustomerRecord`. ``` app.MapPost("/customers/{id:int}/change-email", async ( int id, ChangeEmailRequest dto, IConfiguration config, CancellationToken ct) => { var connectionString = config.GetConnectionString("DefaultConnection"); if (connectionString is null) { return Results.Problem("Connection string missing."); } var customer = await CustomerRecord.FindAsync(id, connectionString, ct); if (customer is null) { return Results.NotFound(); } try { customer.ChangeEmail(dto.Email); } catch (ArgumentException ex) { return Results.BadRequest(ex.Message); } await customer.SaveAsync(ct); return Results.Ok(new { customer.Id, customer.Email, customer.CreditLimit }); }); ``` What changed: - The endpoint no longer knows SQL - The endpoint no longer cares how `CustomerRecord` persists itself - Email validation rules live in `ChangeEmail` instead of in the endpoint This is the “happy path” for Active Record: the endpoint’s job is just HTTP input/output; the record’s job is data plus simple business behavior. ## Where Active Record Shows Up Without Being Named Even if you never write raw ADO.NET: - EF Core entities that contain methods that call `SaveChanges` - Entities that reach up to `DbContext` or service locators - Static helper methods on your entity types that perform queries All of these indicate an **Active Record vibe**. Example of accidental Active Record with EF Core: ``` public class Customer { private readonly AppDbContext _dbContext; public Customer(AppDbContext dbContext) { _dbContext = dbContext; } public int Id { get; private set; } public string Email { get; private set; } = string.Empty; public decimal CreditLimit { get; private set; } public async Task SaveAsync(CancellationToken ct = default) { if (Id == 0) { _dbContext.Customers.Add(this); } await _dbContext.SaveChangesAsync(ct); } } ``` The entity now: - Knows about `AppDbContext` - Decides how to save itself That is Active Record, just hidden behind EF Core. ## When to Use Active Record Active Record is not a mistake by default. It shines in specific contexts. ### 1. Simple, CRUD-heavy domains If your system is mostly: - Creates, reads, updates, and deletes records - Has minimal rules beyond field-level validation then Active Record can deliver: - Fast development - Low conceptual overhead - Straightforward mapping between the database and code Examples: - Internal admin tools - Simple contact or configuration systems - Back-office utilities that mirror tables directly ### 2. Small applications and utilities For: - Command-line migration tools - One-off synchronization apps - Tiny websites with a handful of screens Active Record lets you focus on the task instead of on architecture. ### 3. Early prototypes and spikes When you are: - Exploring a problem space - Validating whether a product idea has legs Active Record gives you a straight line from concept to working software. If the idea dies, you are not stuck with a bunch of unused abstractions. ### 4. Edge subsystems In a larger system, some parts are inherently table-shaped: - Reporting tables - Lookup tables and reference data - Audit logs Using Active Record at the edges (while using a richer model in the core) can be a pragmatic mix. A useful rule: > If the main question is “how do I get and save this row,” Active Record is probably fine. ## When **Not** to Use Active Record The pattern breaks down once the domain stops being “just rows”. ### 1. Rich, evolving business rules If you have: - Cross-aggregate invariants (orders, customers, and products must stay consistent together) - Complex workflows (sagas, long-running processes) - Frequent rule changes that must remain coherent Active Record becomes a liability because: - Every record class now mixes behavior with persistence - Rules often involve multiple records, but there is no clear place to express those interactions In those cases, a Domain Model, along with repositories or data mappers, is a better center of gravity. ### 2. Persistence concerns infect every rule When your “business methods” are full of: - Connection strings - Queries - Transaction management you lose the ability to reason about rules without thinking about the database. You also make testing drastically harder. ### 3. Testing behavior feels heavy If testing “change email” requires: - Setting up a real database - Managing connections - Cleaning up test data then your design makes business rules more expensive to validate than they need to be. With a separate domain model and a data mapper, you can test rules in memory and leave persistence to separate tests. ### 4. Long-lived, collaborative systems In systems that: - Live for years - Have teams joining and leaving - See rules accumulate over time a clean separation between domain logic and persistence buys flexibility. Active Record tends to tangle those concerns together, making deep refactoring more painful. A useful rule: > If the main question is “how should this rule behave over time,” Active Record is not the right center. ## Comparing Active Record with Domain Model + Repository Let us recast our `CustomerRecord` behavior into a simple Domain Model approach. ### Active Record version ``` // Already shown above customer.ChangeEmail(newEmail); await customer.SaveAsync(ct); ``` ### Domain Model plus Repository version Domain entity: ``` public class Customer { public Customer(int id, string email, decimal creditLimit) { Id = id; ChangeEmail(email); CreditLimit = creditLimit; } public int Id { get; } public string Email { get; private set; } = string.Empty; public decimal CreditLimit { get; private set; } public void ChangeEmail(string newEmail) { if (string.IsNullOrWhiteSpace(newEmail)) { throw new ArgumentException("Email cannot be empty.", nameof(newEmail)); } Email = newEmail; } public void IncreaseCreditLimit(decimal delta) { if (delta { var customer = await customers.GetByIdAsync(id, ct); if (customer is null) { return Results.NotFound(); } try { customer.ChangeEmail(dto.Email); } catch (ArgumentException ex) { return Results.BadRequest(ex.Message); } await customers.SaveAsync(customer, ct); return Results.Ok(new { customer.Id, customer.Email, customer.CreditLimit }); }); ``` Here: - The **domain entity** knows the rules - The **repository** knows persistence - The endpoint coordinates, but neither “owns” both at once In more complex domains, that separation scales better than Active Record. ## A Practical Way Forward You do not need to swear off Active Record or embrace it everywhere. Treat it as one tool in your kit. - Use Active Record where: - The domain is simple - The lifetime is short - The code is close to the data - Use Domain Model + Repository where: - Rules are central - Behavior crosses entities - The system must evolve for years A useful exercise for your current codebase: 1. Find a class that both loads/saves itself and contains non-trivial rules. 2. Ask: Is this really just a table row, or is it a key domain concept? 3. If it is just a row, lean into Active Record and keep it small and honest. 4. If it is a key concept, consider splitting behavior from persistence before the complexity grows. That deliberate choice, more than the pattern name, is what keeps enterprise systems from quietly turning into unstructured scripts that happen to be written in C#. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Service Layer Pattern - Making HTTP a Client, Not the Boss](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-service-layer-pattern-making-http-a-client-not-the-boss/) **Published:** December 2, 2025 **Author:** Chris Woodruff **Content:** Open a typical ASP.NET Core project, and you will often see the same shape: - Controllers that validate input, construct entities and call several repositories - Direct calls to external services (payments, credit, email) from controller actions - Transactions managed in random places with `SaveChangesAsync` or manual transaction scopes If you have ever tried to add a second client (a background worker, a message handler, or a gRPC API), you probably copied a large chunk of controller logic and hoped no one noticed. The **Service Layer** pattern exists to stop that. Instead of letting controllers improvise business workflows, you define **application services** that expose operations like `PlaceOrder`, `CancelOrder`, `ApproveRefund`. Every client calls those services. Controllers become thin, infrastructure becomes a detail, and the domain model stops depending on HTTP. In this post, you will see: - What the Service Layer Pattern really is in practical .NET terms - A “before vs after” comparison of fat endpoints vs thin endpoints using a service layer - A complete C# example: `IOrderApplicationService` and `OrderApplicationService` - How the Service Layer Pattern sits between controllers, domain model and infrastructure ## What the Service Layer Pattern Actually Is In Fowler’s description, the **Service Layer** Pattern: - Defines a boundary for your application’s operations - Encapsulates business workflows as methods on application services - Coordinates domain objects, transactions, and external systems - Exposes a consistent API to any client (web, messaging, scheduled jobs, etc.) In a .NET application, that typically means: - **Interfaces** like `IOrderApplicationService` in an application layer project - **Implementation classes** that use domain models, repositories, credit services, and unit-of-work components - **Controllers / minimal APIs** that depend on these interfaces, not on `DbContext` or domain objects directly The key idea: **HTTP is just another client**. It does not own the business process. ## Before Service Layer: Controllers Doing Everything Let’s start with a familiar “before” example: placing an order with a credit check in a Minimal API endpoint. ``` app.MapPost("/orders/place", async ( PlaceOrderRequest dto, AppDbContext db, ICreditGateway creditGateway, CancellationToken ct) => { // Validate request if (dto.Lines is null || dto.Lines.Count == 0) { return Results.BadRequest("Order must contain at least one line item."); } // Build domain entity directly in the endpoint var order = new OrderEntity { Id = Guid.NewGuid(), CustomerId = dto.CustomerId, Status = "Draft", CreatedAt = DateTime.UtcNow, Lines = dto.Lines.Select(l => new OrderLineEntity { ProductId = l.ProductId, Quantity = l.Quantity, UnitPrice = l.UnitPrice }).ToList() }; if (!order.Lines.Any()) { return Results.BadRequest("Order must contain at least one line item."); } var totalAmount = order.Lines.Sum(l => l.Quantity * l.UnitPrice); // Call external credit service var approved = await creditGateway.ApproveAsync( dto.CustomerId, totalAmount, ct); if (!approved) { return Results.BadRequest("Credit check failed."); } // Save using DbContext directly await using var transaction = await db.Database.BeginTransactionAsync(ct); db.Orders.Add(order); await db.SaveChangesAsync(ct); await transaction.CommitAsync(ct); // Build HTTP response return Results.Created($"/orders/{order.Id}", new { order.Id, totalAmount }); }); ``` This endpoint: - Validates the request - Builds an order object - Calculates totals - Calls the credit service - Manages a transaction - Persists with `DbContext` - Returns HTTP responses Now imagine you want: - A background worker that places orders from a message queue - A gRPC method that performs the same operation - A scheduled job that replays failed orders You either duplicate this workflow or you create awkward “helper” classes that are really an informal service layer with no clear interface. ## After Service Layer: Controllers as Thin Clients Now we introduce a **Service Layer**. ### Step 1: Define the application contract ``` public sealed class PlaceOrderRequest { public Guid CustomerId { get; set; } public List Lines { get; set; } = new(); } public sealed class PlaceOrderLineRequest { public Guid ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } } public interface IOrderApplicationService { Task PlaceOrderAsync( PlaceOrderRequest request, CancellationToken cancellationToken = default); } ``` This interface lives in an **Application** project. It knows nothing about HTTP or EF Core. ### Step 2: Implement the service, orchestrating the domain and infrastructure Assume we have: - A domain model `Order` and `OrderLine` - An `IOrderRepository` for persistence - An `ICustomerCreditService` abstraction for credit checks - An `IUnitOfWork` abstraction for transaction boundaries ``` public interface IOrderRepository { Task AddAsync(Order order, CancellationToken cancellationToken = default); } public interface ICustomerCreditService { Task ApproveAsync( Guid customerId, decimal amount, CancellationToken cancellationToken = default); } public interface IUnitOfWork { Task BeginAsync(CancellationToken cancellationToken = default); Task CommitAsync(CancellationToken cancellationToken = default); Task RollbackAsync(CancellationToken cancellationToken = default); } ``` Service implementation: ``` public class OrderApplicationService : IOrderApplicationService { private readonly IOrderRepository _orders; private readonly ICustomerCreditService _creditService; private readonly IUnitOfWork _unitOfWork; public OrderApplicationService( IOrderRepository orders, ICustomerCreditService creditService, IUnitOfWork unitOfWork) { _orders = orders; _creditService = creditService; _unitOfWork = unitOfWork; } public async Task PlaceOrderAsync( PlaceOrderRequest request, CancellationToken cancellationToken = default) { await _unitOfWork.BeginAsync(cancellationToken); try { // Construct the domain aggregate var lines = request.Lines.Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)); var order = Order.Create(request.CustomerId, lines); // External credit check var approved = await _creditService.ApproveAsync( request.CustomerId, order.TotalAmount, cancellationToken); if (!approved) { throw new InvalidOperationException("Credit check failed."); } // Persist via repository await _orders.AddAsync(order, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); return order.Id; } catch { await _unitOfWork.RollbackAsync(cancellationToken); throw; } } } ``` Here, **all the orchestration lives in one place**: - Domain aggregate creation - Credit checks - Transaction boundaries - Persistence And none of it knows about HTTP. ### Step 3: Thin Minimal API endpoint Now the endpoint shrinks dramatically. ``` app.MapPost("/orders/place", async ( PlaceOrderRequest dto, IOrderApplicationService service, CancellationToken ct) => { try { var id = await service.PlaceOrderAsync(dto, ct); return Results.Created($"/orders/{id}", new { Id = id }); } catch (InvalidOperationException ex) when (ex.Message == "Credit check failed.") { return Results.BadRequest(ex.Message); } }); ``` The endpoint: - Accepts `PlaceOrderRequest` from the request body - Calls a single method on `IOrderApplicationService` - Translates known domain/application exceptions to HTTP status codes Everything else is the application’s job, not HTTP’s job. ## Before vs After: An MVC Controller Example If you prefer MVC controllers, the difference is similar. ### Before: fat controller action ``` [ApiController] [Route("api/orders")] public class OrdersController : ControllerBase { private readonly AppDbContext _db; private readonly ICreditGateway _creditGateway; public OrdersController(AppDbContext db, ICreditGateway creditGateway) { _db = db; _creditGateway = creditGateway; } [HttpPost("place")] public async Task Place([FromBody] PlaceOrderRequest dto, CancellationToken ct) { if (dto.Lines is null || dto.Lines.Count == 0) { return BadRequest("Order must contain at least one line item."); } var order = new OrderEntity { Id = Guid.NewGuid(), CustomerId = dto.CustomerId, Status = "Draft", CreatedAt = DateTime.UtcNow, Lines = dto.Lines.Select(l => new OrderLineEntity { ProductId = l.ProductId, Quantity = l.Quantity, UnitPrice = l.UnitPrice }).ToList() }; var total = order.Lines.Sum(l => l.Quantity * l.UnitPrice); var approved = await _creditGateway.ApproveAsync(dto.CustomerId, total, ct); if (!approved) { return BadRequest("Credit check failed."); } await using var tx = await _db.Database.BeginTransactionAsync(ct); _db.Orders.Add(order); await _db.SaveChangesAsync(ct); await tx.CommitAsync(ct); return CreatedAtAction(nameof(GetById), new { id = order.Id }, new { order.Id }); } [HttpGet("{id:guid}")] public async Task GetById(Guid id, CancellationToken ct) { var order = await _db.Orders.FindAsync(new object[] { id }, ct); if (order == null) return NotFound(); return Ok(order); } } ``` ### After: controller delegates to the Service Layer ``` [ApiController] [Route("api/orders")] public class OrdersController : ControllerBase { private readonly IOrderApplicationService _service; public OrdersController(IOrderApplicationService service) { _service = service; } [HttpPost("place")] public async Task Place([FromBody] PlaceOrderRequest dto, CancellationToken ct) { try { var id = await _service.PlaceOrderAsync(dto, ct); return CreatedAtAction(nameof(GetById), new { id }, new { id }); } catch (InvalidOperationException ex) when (ex.Message == "Credit check failed.") { return BadRequest(ex.Message); } } [HttpGet("{id:guid}")] public async Task GetById(Guid id, CancellationToken ct) { // this might still use a query service or repository // the key here is that the "Place order" workflow no longer lives here return Ok(new { Id = id }); } } ``` The controller is now: - Easier to read - Easier to test (mock `IOrderApplicationService`) - Less likely to diverge from other clients that call the same use case ## Where the Service Layer Pattern Fits in a Typical .NET Architecture A simple project layout that uses the Service Layer Pattern might look like this: - `MyApp.Domain` - Entities and value objects (`Order`, `OrderLine`, `Customer`, etc.) - Domain services - Repository interfaces (`IOrderRepository`, `ICustomerRepository`) - `MyApp.Application` - Application services (`IOrderApplicationService`, `OrderApplicationService`) - DTOs and commands/queries (`PlaceOrderRequest`, `CancelOrderRequest`) - `MyApp.Infrastructure` - EF Core `DbContext` - Repository implementations - Unit of Work implementation - External service adapters (`CustomerCreditService`) - `MyApp.Web` - Minimal API or MVC controllers - DI configuration Dependency rules: - `Web` → `Application` → `Domain` - `Infrastructure` → `Application`, `Domain` - `Domain` has no dependency on `Web` or `Infrastructure` The Service Layer lives in the **Application** project and becomes the main API that the outside world uses. ## Smells That You Need a Service Layer You probably need a Service Layer if: - Controllers or endpoints are long and hard to test - Multiple controllers or clients implement similar workflows differently - Transaction handling and credit checks are scattered across your codebase - Adding a new client (e.g., message consumer) feels like copying controller code In short, if **business processes lack a single home**, you are a good candidate for a Service Layer. ## Introducing a Service Layer Pattern into an Existing App You do not have to rewrite your system to adopt this pattern. Start small. 1. **Pick a single important use case** For example: place an order, cancel an order, approve a refund. 2. **Extract orchestration into a service** Move: - Creation and modification of domain objects - External service calls - Transaction handling 3. **Change controllers to call the service** Keep validation and HTTP concerns in controllers; move business coordination out. 4. **Add a second client** Create a background worker, message handler, or integration endpoint that calls the same service. You will see reuse and consistency immediately. 5. **Repeat for other use cases** As you migrate workflows to the Service Layer, controllers shrink and the application model becomes explicit. ## Closing Thoughts The Service Layer pattern is not about ceremony. It is about **choosing one place where each business operation lives**, then letting every client call that place. When you make HTTP a client instead of the boss: - Controllers stay simple and focused on transport concerns - Domain rules and workflows have a clear home - Adding new delivery channels stops being a duplication exercise Find one fat endpoint in your ASP.NET Core application. Extract its workflow into a service. Let the controller become a thin client. That single move often changes how you think about the entire system. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Domain Model Pattern - When Your Core Rules Deserve Their Own Gravity](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-domain-model-pattern-when-your-core-rules-deserve-their-own-gravity/) **Published:** December 1, 2025 **Author:** Chris Woodruff **Content:** Look at a typical enterprise ASP.NET Core application, and you often see the same pattern: - Controllers validating requests, calculating totals, and applying discounts - EF Core entities that are little more than property bags - Stored procedures that quietly decide which orders are valid If you need to know how orders work, you do not open a single file. You read controllers, queries, and database scripts until your eyes blur. The truth about the business lives everywhere and nowhere. The Domain Model is the pattern that reverses this arrangement. Instead of clever controllers and dumb entities, you move the rules into rich objects. Entities and value objects enforce invariants. The application layer orchestrates use cases by telling those objects what to do. This post shows what that looks like in C#, and why putting rules next to data changes how your system behaves over time. ## What Domain Model Really Is In Fowler’s terms, a Domain Model: - Represents the business domain with rich objects - Encapsulates rules and invariants inside those objects - Treats the framework, database, and transport as details at the edges In practical .NET terms: - Your `Order` type knows what a valid order looks like - Your `Customer` type knows whether it is eligible for a specific feature - Controllers, message handlers, or background jobs call methods on those types What it is not: - It is not simply having classes called `Order` and `Customer` with auto properties - It is not pushing every rule into a single God object - It is not a diagram alone, while the code keeps all the rules in the controllers The whole point is to make the rules you care about first-class citizens in your code. ## A Concrete Domain Model Slice Here is a small, but real, `Order` aggregate with `OrderLine` in C#. ``` public class Order { private readonly List _lines = new(); private Order(Guid customerId) { Id = Guid.NewGuid(); CustomerId = customerId; Status = OrderStatus.Draft; } public Guid Id { get; } public Guid CustomerId { get; } public OrderStatus Status { get; private set; } public IReadOnlyCollection Lines => _lines.AsReadOnly(); public decimal TotalAmount => _lines.Sum(l => l.Total); public static Order Create(Guid customerId, IEnumerable lines) { var order = new Order(customerId); foreach (var line in lines) { order.AddLine(line.ProductId, line.Quantity, line.UnitPrice); } if (!order._lines.Any()) { throw new InvalidOperationException("Order must have at least one line."); } return order; } public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("Cannot change a non draft order."); } if (quantity { if (dto.Lines is null || dto.Lines.Count == 0) { return Results.BadRequest("Order must have at least one line."); } var orderEntity = new OrderEntity { Id = Guid.NewGuid(), CustomerId = dto.CustomerId, Status = "Draft", CreatedAt = DateTime.UtcNow }; foreach (var lineDto in dto.Lines) { if (lineDto.Quantity { var order = await db.Orders .Include(o => o.Lines) .SingleOrDefaultAsync(o => o.Id == id); if (order == null) { return Results.NotFound(); } if (order.Status != "Draft") { return Results.BadRequest("Cannot change a non draft order."); } if (!order.Lines.Any()) { return Results.BadRequest("Cannot discount an empty order."); } if (dto.Percent = 50) { return Results.BadRequest("Discount percent out of range."); } foreach (var line in order.Lines) { line.UnitPrice = line.UnitPrice * (1 - dto.Percent / 100m); } await db.SaveChangesAsync(); return Results.Ok(new { order.Id }); }); ``` The same rules are repeated in different forms: - Draft status checks - Non-empty order checks - Discount percent range checks - Positive quantity rules The code works until a new rule arrives and someone updates one endpoint but misses the others. ## After Domain Model: Controller As Orchestrator Now see how the controller changes when you let the domain model handle behavior. Assume you already use the `Order` aggregate from earlier and have a repository. ``` public interface IOrderRepository { Task AddAsync(Order order, CancellationToken cancellationToken = default); Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); } ``` ### Creating an Order ``` app.MapPost("/orders", async ( CreateOrderDto dto, IOrderRepository orders, CancellationToken ct) => { var lines = dto.Lines.Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)); Order order; try { order = Order.Create(dto.CustomerId, lines); } catch (Exception ex) when (ex is ArgumentOutOfRangeException || ex is InvalidOperationException) { return Results.BadRequest(ex.Message); } await orders.AddAsync(order, ct); return Results.Created($"/orders/{order.Id}", new { order.Id }); }); public record CreateOrderDto( Guid CustomerId, List Lines); public record CreateOrderLineDto( Guid ProductId, int Quantity, decimal UnitPrice); ``` The endpoint now: - Translates input DTOs into domain `OrderLine` objects - Delegates invariant to `Order.Create` and `OrderLine` constructors - Catches domain exceptions and maps them to HTTP responses The logic that defines a valid order lives inside `Order`, not inside the endpoint. ### Applying a Discount ``` app.MapPost("/orders/{id:guid}/discounts", async ( Guid id, ApplyDiscountDto dto, IOrderRepository orders, CancellationToken ct) => { var order = await orders.GetByIdAsync(id, ct); if (order == null) { return Results.NotFound(); } try { order.ApplyDiscount(dto.Percent); } catch (ArgumentOutOfRangeException ex) { return Results.BadRequest(ex.Message); } await orders.AddAsync(order, ct); // or SaveChanges via Unit of Work return Results.Ok(new { order.Id, order.TotalAmount }); }); public record ApplyDiscountDto(decimal Percent); ``` The discount rule is expressed once, in the domain model: ``` public void ApplyDiscount(decimal percent) { if (percent = 50) { throw new ArgumentOutOfRangeException(nameof(percent)); } foreach (var line in _lines) { line.ApplyDiscount(percent); } } ``` Controllers have one job: - Load the aggregate - Tell it what to do - Persist the result - Translate domain errors to responses That is the essence of Domain Model in a web app. ## Why Putting Rules Next To Data Matters Shifting behavior into domain objects does more than make code “cleaner”. It changes several properties of your system. ### One Place To Ask “What Is The Rule” If a product owner asks: > What exactly are the conditions for applying a discount? You can answer by opening `Order.ApplyDiscount` and related collaborators. There is no tour of controllers, repositories, and stored procedures. ### Transport Independence Imagine you want a background service that runs a nightly promotion: - It reads eligible orders from the database - It applies a discount to each - It sends confirmation emails With a Domain Model, this worker calls the same `ApplyDiscount` method that your HTTP endpoint uses. If you switch to messaging or add a gRPC API, they all reuse the same behavior. ### Stronger, Cheaper Tests You can write unit tests directly against `Order`: ``` [Fact] public void ApplyDiscount_Throws_WhenPercentOutOfRange() { var order = Order.Create( Guid.NewGuid(), new[] { new OrderLine(Guid.NewGuid(), 1, 100m) }); Assert.Throws(() => order.ApplyDiscount(0)); Assert.Throws(() => order.ApplyDiscount(60)); } [Fact] public void Submit_SetsStatusToSubmitted_WhenDraftAndHasLines() { var order = Order.Create( Guid.NewGuid(), new[] { new OrderLine(Guid.NewGuid(), 1, 100m) }); order.Submit(); Assert.Equal(OrderStatus.Submitted, order.Status); } ``` No test server, no HTTP, no database. You can exhaustively test the behavior that matters while keeping integration tests focused on wiring. ## Integrating Domain Model With Application And Infrastructure Domain Model does not live alone. It cooperates with: - An application layer that coordinates use cases - An infrastructure layer that persists, aggregates, and talks to external systems A typical setup in .NET: - `MyApp.Domain` - Entities, value objects, domain services and interfaces for repositories - `MyApp.Application` - Application services that orchestrate commands and queries - `MyApp.Infrastructure` - EF Core mappings, repository implementations, unit of work - `MyApp.Web` - Controllers or minimal APIs that call application services Example application service using `Order`: ``` public interface IOrderApplicationService { Task CreateOrderAsync(CreateOrderCommand command, CancellationToken ct = default); } public class OrderApplicationService : IOrderApplicationService { private readonly IOrderRepository _orders; public OrderApplicationService(IOrderRepository orders) { _orders = orders; } public async Task CreateOrderAsync(CreateOrderCommand command, CancellationToken ct = default) { var lines = command.Lines.Select(l => new OrderLine(l.ProductId, l.Quantity, l.UnitPrice)); var order = Order.Create(command.CustomerId, lines); await _orders.AddAsync(order, ct); return order.Id; } } public record CreateOrderCommand( Guid CustomerId, IReadOnlyCollection Lines); public record CreateOrderLineCommand( Guid ProductId, int Quantity, decimal UnitPrice); ``` Controllers or endpoints call `IOrderApplicationService`, not `Order` directly. That keeps HTTP details and use case orchestration together, while the domain model stays focused on rules. ## Signs You Are Pretending To Have A Domain Model Many teams say, “We are doing DDD,” while their code tells a different story. Look for these patterns. - Entities with only auto properties and no behavior - Controllers or handlers performing status transitions and complex validations - Stored procedures implementing key rules, such as discount criteria or eligibility - Domain types that depend directly on `DbContext` or `HttpContext` If any of those describe your system, you have building blocks for a domain model, not an actual model. ## First Steps Toward A Real Domain Model You do not need a significant rewrite. Start small. 1. **Pick one important concept** Order, Subscription, Invoice, or any aggregate that matters to the business. 2. **Move a single rule into that entity** For example, “order must have at least one line” or “cannot modify submitted orders”. 3. **Expose behavior, not just state** Add methods like `AddLine`, `ApplyDiscount`, `Submit`, instead of letting the outside world mutate collections directly. 4. **Write tests against the entity** Prove that the rules hold even when no controller or database is involved. 5. **Refactor controllers to call the domain model** Remove duplicated checks, catch domain exceptions, map them to HTTP responses. Repeat that in the parts of the system that hurt the most. Over time, the gravity of the domain model grows, and the framework falls into its proper role as plumbing. If your core rules are worth money, they are worth a real home in your code. Treat them as the main asset, not as an afterthought squeezed into controllers and stored procedures. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Enterprise Patterns for ASP.NET Core Minimal API: Transaction Script Pattern - The Shortcut That Quietly Reshapes Your System](https://www.woodruff.dev/enterprise-patterns-for-asp-net-core-minimal-api-transaction-script-pattern-the-shortcut-that-quietly-reshapes-your-system/) **Published:** November 29, 2025 **Author:** Chris Woodruff **Content:** Picture this. Product wants a minor discount tweak: if an order total is below 100, no discount. You open an endpoint, add a conditional, save and push. Ten minutes, job done. Three months later, that simple rule exists in six different endpoints, each with its own tiny twist. Someone fixes a bug in two of them, forgets the others, and now nobody can answer a basic question: what is the real discount rule in this system? That creeping mess has a name: Transaction Script. Most teams start here. Some stay here forever and pay for it in every release. This post walks through what Transaction Script really is, how it looks in ASP.NET Core, where it shines, and how to recognize when it stops being a shortcut and becomes structural debt. You will see C# examples of a pure Transaction Script, followed by a first refactoring step that opens the door to a richer design. ## **What Transaction Script Actually Is** Transaction Script treats each use case as a single procedure that does everything: - Reads the request - Loads data - Applies business rules - Persists changes - Shapes the response There is no separate domain model with behavior, no dedicated service layer that orchestrates aggregates. There is one script per scenario. The pattern is not automatically wrong. It is brutally simple, which makes it powerful in the proper context and dangerous when the domain keeps growing. ## **A Concrete Example: Apply Discount as a Transaction Script** Here is a straight Transaction Script inside an ASP.NET Core minimal API endpoint. ``` app.MapPost("/discounts/apply", async (ApplyDiscountDto dto, AppDbContext db) => {     var customer = await db.Customers.FindAsync(dto.CustomerId);     if (customer == null)     {         return Results.NotFound("Customer not found.");     }     var order = await db.Orders         .Include(o => o.Lines)         .FirstOrDefaultAsync(o => o.Id == dto.OrderId && o.CustomerId == dto.CustomerId);     if (order == null)     {         return Results.NotFound("Order not found.");     }     var total = order.Lines.Sum(l => l.Quantity * l.UnitPrice);     if (total < 100)     {         return Results.BadRequest("Order total too low for discount.");     }     order.ApplyDiscount(dto.DiscountPercent);     await db.SaveChangesAsync();     return Results.Ok(new { order.Id, order.TotalAmount }); }); public record ApplyDiscountDto(Guid CustomerId, Guid OrderId, decimal DiscountPercent); ``` Everything lives in one block: - Data access through AppDbContext - Business rule about the minimum total - Behavior that applies a discount - HTTP responses and status codes That is pure Transaction Script: a single procedure handles the entire transaction. ## **When Transaction Script Makes Sense** Used deliberately, Transaction Script fits real scenarios. ### **Small, Focused Endpoints** If a feature is bounded and straightforward, a script like this can be ideal: - One administrative endpoint that fixes a specific data issue - A tiny internal API for a tool that may never grow beyond a handful of operations - A migration utility that processes a file and writes the results once In these cases, creating a rich domain model or elaborate service layer can be pure overhead. ### **Short-Lived Features And Experiments** Sometimes you need a spike: - Experiment with a new discount rule to see if customers respond - Build an internal endpoint that may be replaced by a better system later - Capture data for a temporary campaign A Transaction Script lets you wire this up rapidly. If the feature dies quickly, you never pay a heavy design cost. ### **Teams Under Immediate Delivery Pressure** There are moments when: - The business is blocked until a particular rule exists - You are in the middle of an incident and need a quick workaround - The system is early, and the domain rules are still chaotic In those conditions, a well-written script can keep the system moving while you learn what the real domain boundaries look like. The critical word there is “while”. At some point, the domain stabilizes. If the scripts remain the main design element, they become anchors. ## **The Real Cost: Repeating Yourself Into A Corner** The example above does not look frightening. The trouble starts when requirements evolve. Imagine: - A new discount rule for VIP customers that uses a lower threshold - Another use case that applies discounts during checkout, not only via this endpoint - A batch job that reconciles discounts at the end of the day Every time you add a rule in script form, you have a choice: - Copy the logic into the new script - Call the existing script from somewhere strange - Extract part of it into a helper or static method Most teams take the fastest path, which often means copy, tweak, repeat. After a few iterations you have: - Slightly different minimum totals scattered across scripts - Conditionals that no one dares to touch because they are not sure who depends on them - Business logic that can only be understood by reading several endpoints line by line At that stage, you are not doing “simple procedural code”. You are maintaining a distributed domain model made of duplicated fragments. ### **Recognizing When Your Scripts Are Out Of Control** You do not need a fancy metric. Look for these practical signals. ### **Same Rule, Many Places** Search for the literal “Order total too low for discount” string. If you find variations of the same check in multiple endpoints or handlers, you already have a smell. - Different thresholds per scenario, but no central decision point - Edge conditions handled in some scripts, forgotten in others That is a sign that the domain concept “discount eligibility” deserves its own abstraction. ### **Scripts That Do Everything** If an endpoint: - Validates input - Loads multiple aggregates - Calculates totals - Applies discounts - Logs audits - Sends notifications - Saves changes You no longer have a transaction script… You have a transaction novel. Changing anything inside that block risks side effects everywhere. ### **Testing Feels Painful** If writing tests for your discount behavior: - Requires spinning up a full test server - Requires hitting HTTP routes for every variant - Requires seeding a complex database state repeatedly Then the rules are too entangled with infrastructure. Transaction Scripts have swallowed your business logic. ## **First Refactor: Keep The Script, Extract The Rule** You do not need to jump straight from scripts to a full Domain Model. The first step can be modest: pull core rules into a domain abstraction, let the script delegate. Introduce a small domain service that knows how discounts work. ``` public interface IDiscountPolicy {     DiscountDecision Evaluate(decimal orderTotal, decimal requestedPercent); } public record DiscountDecision(bool IsAllowed, string? Reason); public class MinimumTotalDiscountPolicy : IDiscountPolicy {     private readonly decimal _minimumTotal;     public MinimumTotalDiscountPolicy(decimal minimumTotal)     {         _minimumTotal = minimumTotal;     }     public DiscountDecision Evaluate(decimal orderTotal, decimal requestedPercent)     {         if (orderTotal < _minimumTotal)         {             return new DiscountDecision(false, "Order total too low for discount.");         }         if (requestedPercent = 50)         {             return new DiscountDecision(false, "Discount percent out of allowed range.");         }         return new DiscountDecision(true, null);     } } ``` Now change the script to use this policy. ``` app.MapPost("/discounts/apply", async (     ApplyDiscountDto dto,     AppDbContext db,     IDiscountPolicy discountPolicy) => {     var customer = await db.Customers.FindAsync(dto.CustomerId);     if (customer == null)     {         return Results.NotFound("Customer not found.");     }     var order = await db.Orders         .Include(o => o.Lines)         .FirstOrDefaultAsync(o => o.Id == dto.OrderId && o.CustomerId == dto.CustomerId);     if (order == null)     {         return Results.NotFound("Order not found.");     }     var total = order.Lines.Sum(l => l.Quantity * l.UnitPrice);     var decision = discountPolicy.Evaluate(total, dto.DiscountPercent);     if (!decision.IsAllowed)     {         return Results.BadRequest(decision.Reason);     }     order.ApplyDiscount(dto.DiscountPercent);     await db.SaveChangesAsync();     return Results.Ok(new { order.Id, order.TotalAmount }); }); ``` You still have a Transaction Script. It still coordinates the transaction. Something important has changed. - The rule about whether a discount is allowed now lives in one place - Other scripts or background jobs can reuse IDiscountPolicy - You can unit test the policy with plain C# tests without a test server or database This is the hinge where your system can move toward a Domain Model or a more structured Service Layer. ## **When Transaction Script Should Give Way To Other Patterns** Once a rule becomes central to the product, you want more than a simple procedure. ### **Domain Model For Rich, Interconnected Rules** If discounts: - Depend on customer status, product category, time of day, and campaign rules - Interact with loyalty points, tax calculations, and fulfillment - Require sophisticated validation and simulation then a Domain Model gives you a place to express those relationships. Aggregates, value objects, and domain services can capture those rules in a way that scripts never will. In that world, discount logic might live inside an Order aggregate or a dedicated DiscountEngine, tested directly and used from multiple entry points. ### **Table Module For Table Focused Operations** If most of the logic operates on a single table or view, such as OrderSummary or InvoiceRow, and the operations are set-oriented, a Table Module can be enough. A DiscountTableModule class that encapsulates queries and updates for discount fields can centralize behavior without inventing a full object model. The critical point is not the pattern’s name. It is whether the rules live in a singular, testable place, rather than in scattered fragments. ## **Practical Guidelines For .NET Teams** If you build ASP.NET Core systems, you can treat Transaction Script as a controlled tool rather than a reflexive default. - Use scripts for isolated use cases with simple rules and short life spans - Watch for duplication: once a rule appears in more than one script, extract it into a domain component - Keep orchestration and infrastructure in the script, push domain decisions into services or entities - Add tests around the extracted domain pieces before you touch the scripts again The goal is not to outlaw Transaction Script. The goal is to avoid waking up one day in a codebase where every important rule lives inside an untested endpoint. ## **A Challenge For Your Current Codebase** Pick one feature today: 1. Find an endpoint or handler that reads from the database, applies a rule, and writes back in a single method. 2. Identify a single rule inside that method that business people care about. 3. Extract that rule into a domain service or helper with its own unit tests. 4. Leave the rest of the script intact, let the endpoint call the new abstraction. Run the tests. Show that nothing changed in behavior. Then ask yourself: if this one small extraction already made the rule clearer, what happens when you repeat that move across the parts of the system that hurt the most? That is how Transaction Script turns from a permanent architecture into a stepping stone. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Stop Letting Your Controllers Talk to SQL: Layered Architecture in ASP.NET Core](https://www.woodruff.dev/stop-letting-your-controllers-talk-to-sql-layered-architecture-in-asp-net-core/) **Published:** November 28, 2025 **Author:** Chris Woodruff **Content:** Walk into almost any long-lived enterprise codebase, and you will find the same pattern: - Controllers that know about routing, JSON, SQL, and domain rules - Repositories that reach up into `HttpContext` - Business rules scattered across UI, stored procedures, and helper classes At that point, adding a new feature feels like surgery without a map. You poke at one place, something bleeds somewhere else, and nobody is sure why. Layered architecture exists to stop that. In this post, we will walk through a practical version of Fowler’s layered architecture in ASP.NET Core and C#. You will see: - What each layer is allowed to know - How to wire up a fundamental feature using three layers - How does this structure make change cheaper and failure less chaotic The example centers on a simple use case: creating an order. ## The core idea: three layers, three distinct responsibilities Fowler’s baseline looks like this: 1. **Presentation layer** Handles input and output. In web apps, this means HTTP, routing, model binding, and formatting responses. 2. **Domain layer** Holds business rules, domain services, and aggregates. It talks about orders, customers, payments, not controllers or `DbContext`. 3. **Data source layer** Owns persistence. It uses EF Core, raw SQL, caching, and talks to any external data store. A simple rule captures the intent: > If your controllers know SQL or your repositories know HTTP, you already lost separation. The rest of this post shows what it looks like when you refuse to cross those lines. ## The scenario: placing an order in three slices We will build a feature that lets a client create an order. Requirements: - Clients call `POST /orders` with customer and line items - An order must contain at least one line item - The system persists the order in a relational database We will implement that with: - A presentation layer endpoint - A domain service and aggregate - A data source layer using EF Core ## Presentation layer: thin HTTP endpoint The presentation layer should only: - Accept input - Call the domain layer - Shape the HTTP response It should not: - Reach into `DbContext` - Perform business rules beyond basic request validation - Use EF Core directly Example with ASP.NET Core minimal APIs: ``` // Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); builder.Services.AddScoped(); var app = builder.Build(); app.MapPost("/orders", async (CreateOrderDto dto, IOrderService orderService) => { // Basic request-level validation only if (dto.Lines is null || dto.Lines.Count == 0) { return Results.BadRequest("Order must contain at least one line item."); } var orderId = await orderService.CreateOrderAsync(dto.CustomerId, dto.Lines); return Results.Created($"/orders/{orderId}", new { Id = orderId }); }); app.Run(); public record CreateOrderDto(Guid CustomerId, List Lines); public record OrderLineDto(Guid ProductId, int Quantity); ``` Notice what the endpoint **does not** do: - It does not call `AppDbContext` - It does not calculate totals - It does not decide how orders are stored It simply coordinates HTTP and the domain service. ## Domain layer: business rules and language of the problem The domain layer decides what an order is allowed to do. It should: - Enforce invariants - Capture domain rules in one place - Express intent through the language of the business It should not: - Know about HTTP - Know about EF Core - Reference ASP.NET Core packages Domain service interface: ``` public interface IOrderService { Task CreateOrderAsync( Guid customerId, IReadOnlyCollection lines); } ``` Domain service implementation: ``` public class OrderService : IOrderService { private readonly IOrderRepository _orders; public OrderService(IOrderRepository orders) { _orders = orders; } public async Task CreateOrderAsync( Guid customerId, IReadOnlyCollection lines) { if (!lines.Any()) { throw new InvalidOperationException("Order must contain at least one line item."); } var order = Order.Create( customerId, lines.Select(l => new OrderLine(l.ProductId, l.Quantity)).ToList()); await _orders.AddAsync(order); return order.Id; } } ``` Domain model for `Order` and `OrderLine`: ``` public class Order { private readonly List _lines = new(); private Order(Guid customerId) { Id = Guid.NewGuid(); CustomerId = customerId; Status = OrderStatus.Draft; CreatedAt = DateTime.UtcNow; } public Guid Id { get; } public Guid CustomerId { get; } public OrderStatus Status { get; private set; } public DateTime CreatedAt { get; } public IReadOnlyCollection Lines => _lines.AsReadOnly(); public decimal TotalAmount => _lines.Sum(l => l.Total); public static Order Create(Guid customerId, IEnumerable lines) { var order = new Order(customerId); foreach (var line in lines) { order.AddLine(line.ProductId, line.Quantity, line.UnitPrice); } if (!order._lines.Any()) { throw new InvalidOperationException("Order must have at least one line item."); } return order; } public void AddLine(Guid productId, int quantity, decimal unitPrice) { if (Status != OrderStatus.Draft) { throw new InvalidOperationException("Cannot modify a non draft order."); } if (quantity o.Lines) .SingleOrDefaultAsync(o => o.Id == id, cancellationToken); } } ``` DbContext: ``` public class AppDbContext : DbContext { public DbSet Orders => Set(); public DbSet OrderLines => Set(); public AppDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(builder => { builder.HasKey(o => o.Id); builder.Property(o => o.CustomerId).IsRequired(); builder.Property(o => o.Status).IsRequired(); builder.Property(o => o.CreatedAt).IsRequired(); builder.HasMany(typeof(OrderLine), "_lines") .WithOne() .HasForeignKey("OrderId") .IsRequired(); }); modelBuilder.Entity(builder => { builder.HasKey("Id"); builder.Property("OrderId"); builder.Property(l => l.ProductId).IsRequired(); builder.Property(l => l.Quantity).IsRequired(); builder.Property(l => l.UnitPrice).IsRequired(); }); } } ``` The repository understands EF Core and the database schema. The domain does not. ## How does this structure make change cheaper The layered structure looks simple until you try to change things. That is where it earns its keep. ### 1. Business rule changes Suppose the business wants new rules: - Orders below a specific total should be rejected - Some customers have higher minimum totals Where does that logic go? - Not in the controller - Not in the repository It belongs in the domain layer, near the `Order` aggregate or in domain services. You might extend `OrderService`: ``` public async Task CreateOrderAsync( Guid customerId, IReadOnlyCollection lines) { if (!lines.Any()) { throw new InvalidOperationException("Order must contain at least one line item."); } var order = Order.Create( customerId, lines.Select(l => new OrderLine(l.ProductId, l.Quantity)).ToList()); var minimum = await GetCustomerMinimumAsync(customerId); if (order.TotalAmount < minimum) { throw new InvalidOperationException( $"Order total must be at least {minimum} for this customer."); } await _orders.AddAsync(order); return order.Id; } private Task GetCustomerMinimumAsync(Guid customerId) { // Look up from a configuration service or customer settings return Task.FromResult(100m); } ``` Presentation and data source layers stay unchanged. ### 2. Switching transports: HTTP today, messaging tomorrow Imagine you want to process orders from a message queue and via HTTP. With a layered design, you write a message handler that calls the same `IOrderService`: ``` public class OrderCreatedMessageHandler { private readonly IOrderService _orderService; public OrderCreatedMessageHandler(IOrderService orderService) { _orderService = orderService; } public async Task HandleAsync(OrderCreatedMessage message) { var dtoLines = message.Lines .Select(l => new OrderLineDto(l.ProductId, l.Quantity)) .ToList(); await _orderService.CreateOrderAsync(message.CustomerId, dtoLines); } } public record OrderCreatedMessage(Guid CustomerId, List Lines); ``` No duplication of rules. No extra data access logic. You add another presentation layer entry point. ### 3. Swapping EF Core for another persistence mechanism If you ever need to switch persistence, the blast radius is clear. - Domain layer remains the same - Presentation layer remains the same - Only `IOrderRepository` implementations and `AppDbContext` related types change You can introduce another repository, for example `DapperOrderRepository`, and swap registrations in the DI container. ## How teams quietly destroy their layers Codebases rarely lose layering due to a single catastrophic decision. They lose it through a stream of small, “temporary” shortcuts. ### Shortcut 1: “Just this one query in the controller” A developer needs a special report. They already have access to `AppDbContext` in the controller, so they write: ``` public class ReportsController : ControllerBase { private readonly AppDbContext _dbContext; public ReportsController(AppDbContext dbContext) { _dbContext = dbContext; } [HttpGet("reports/orders")] public async Task GetOrdersReport() { var data = await _dbContext.Orders .Include(o => o.Lines) .Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7)) .ToListAsync(); // Transform into view model return Ok(data); } } ``` It works. It ships. It also teaches the team that controllers are fair game for data access. After a few months, there is no clear separation at all. Better approach: move data access to a query service or repository and call that from the controller. ### Shortcut 2: Repositories returning view models Another developer builds a dashboard. They want to avoid extra mapping, so they let the repository spit out DTOs used by the UI. ``` public interface IOrderRepository { Task GetRecentSummariesAsync(); } ``` That feels efficient. It also tangles the data source layer with a specific presentation need. Six months later, a background worker wants a different representation, and the repository keeps growing special cases. Better approach: keep repositories returning domain objects or well-defined query models that are not tied directly to controllers. ### Shortcut 3: Domain services reading `HttpContext` Sometimes, domain services need user information or tenant context. The easy path is to inject `IHttpContextAccessor` directly. ``` public class DiscountService { private readonly IHttpContextAccessor _accessor; public DiscountService(IHttpContextAccessor accessor) { _accessor = accessor; } public decimal GetDiscount() { var user = _accessor.HttpContext?.User; // ... } } ``` This locks the domain to ASP.NET Core. Reusing the domain in background services or tests becomes painful. Better approach: define an abstraction such as `ICurrentUser` or `ICurrentTenant` in the domain layer, and implement it in the presentation or infrastructure layer using `HttpContext`. ## Enforcing layers with project structure Code style and good intentions are not enough. The solution’s physical structure should reinforce the boundaries. A common layout: - `MyApp.Domain` - Entities, value objects, domain services, domain interfaces - `MyApp.Application` (optional, if you separate application from pure domain) - Use cases, application services, DTOs - `MyApp.Infrastructure` - EF Core mappings, repositories, integration with external services - `MyApp.Web` - ASP.NET Core host, controllers, endpoints, filters Dependency rules: - `MyApp.Web` references `MyApp.Domain` and `MyApp.Application` - `MyApp.Infrastructure` references `MyApp.Domain` and `MyApp.Application` - `MyApp.Domain` does not reference any other project - `MyApp.Application` references `MyApp.Domain` only You can even add build checks or analyzers to prevent forbidden references. ## Raising the stakes: what you lose without layers If this sounds abstract, consider the cost of ignoring it. - You lose the ability to isolate a single change. Every change becomes a search through controllers, EF queries, and domain objects. - You lose optionality. Moving to messaging, splitting services, or swapping databases becomes nearly impossible without a rewrite. - You lose trust. Diagrams and architecture documents claim there are layers. The code says otherwise. Eventually, the team stops listening to both. Layered architecture will not save you from every problem. It does something more modest and more powerful: It gives you clear seams to cut, adapt, and evolve. ## A simple challenge for your current system Pick one feature in your application. Then: 1. Identify the presentation, domain, and data source pieces. 2. Count how many times each one crosses its boundary. 3. Move a single business rule out of a controller into a domain service or aggregate. 4. Move a single data access concern from a controller to a repository. You do not fix an entire enterprise codebase in one refactor. You fix it one layer at a time, one leak at a time. Once your controllers stop talking to SQL and your repositories stop knowing HTTP, everything else gets easier. **Categories:** Patterns **Tags:** .NET, C#, dotnet, patterns, programming --- ### [Licensing Compliance in the Courtroom: Why It Matters More Than You Think](https://www.woodruff.dev/licensing-compliance-in-the-courtroom-why-it-matters-more-than-you-think/) **Published:** October 17, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking to enhance your organization's systems through quality assessments, I would love to connect with you. Let's explore how I can contribute to your success. You can easily reach me via my contact page here: https://woodruff.dev/contact/ ``` In nearly every industry, open-source code fuels innovation. Developers harness open libraries, frameworks, and tools to accelerate features and cut costs. Yet, in this race for speed, many teams overlook a vital element: licensing. Each open-source component carries specific terms that guide its use, sharing, and modification. When these terms are overlooked or misunderstood, convenience can swiftly become a legal and financial burden. Licensing transcends technicality. It represents a business imperative. What seems like free software often entails obligations that must be honored to ensure compliance. Neglecting those obligations can lead to lawsuits, compel public disclosure of proprietary code, or even disrupt essential operations. The narrative isn’t about whether companies embrace open-source software, but rather how they manage it with responsibility and foresight. ## The hidden risk of poor license management Many organizations underestimate just how intricate open-source licensing can be. It’s all too common for developers to grab third-party dependencies without taking the time to check their compatibility or any existing license restrictions. Often, this is done with good intentions: the goal is to solve problems swiftly and deliver valuable solutions. However, without proper governance, every new library introduced could become a ticking time bomb. Imagine a scenario where incompatible licenses are tangled together in the same codebase. A single oversight might violate the copyleft provisions of a restrictive license like the GPL, potentially forcing the company to expose its proprietary code. Additionally, organizations sometimes neglect to give credit to original authors or fail to include mandatory license notices—both significant violations in the open-source world. The implications of these oversights aren’t always immediate. They often bubble up later during audits, mergers, or even litigation. A potential buyer might hesitate or even reduce their offer if they stumble upon unverified open-source usage. Similarly, a client might demand indemnification for software that unknowingly infringes on third-party rights. When legal issues crop up, what started as technical slip-ups can escalate into high-stakes courtroom battles, making it clear that managing open-source licensing deserves far more attention. ## The financial and reputational consequences Legal battles over software licensing are very real and can have significant consequences. Major corporations have faced lawsuits for non-compliance, which have resulted in costly settlements, product recalls, and substantial reputational damage. Smaller companies are equally at risk; defending against a single claim can easily surpass their entire annual development budget. The reputational damage from licensing issues often overshadows the financial toll. Once a company is labeled as non-compliant, partners become reluctant to collaborate, and customers lose trust. In today’s business landscape, where transparency is paramount, nothing erodes credibility faster than a licensing scandal. From an operational perspective, poor compliance severely hampers engineering productivity. When violations surface, development stalls as teams rush to audit, replace, or rewrite the affected code. These delays have far-reaching impacts on projects and strain relationships with stakeholders who expect timely delivery. Companies cannot afford to be complacent about compliance; it’s essential for their success and credibility. ## Building compliance through collaboration The solution is not to avoid open-source software but to manage it intentionally. Licensing compliance requires coordination between engineering, architecture, and legal functions. Each plays a distinct role. Engineers must understand the licenses that accompany the code they use. Architects must design systems with clear separation between proprietary and open-source components. Legal teams must establish policies that guide acceptable use and ensure adherence to contractual obligations. The key is to integrate these efforts into the development lifecycle rather than treating them as one-time audits. Compliance begins with awareness and continues through automation and verification. ## Practical strategies for achieving compliance #### **Inventory and tracking** Every organization should maintain a living inventory of its software assets. This includes open-source dependencies, frameworks, and libraries. The inventory should record the version, license type, and origin of each component. When a new library is added, it must be reviewed before being integrated into production systems. #### **Automated scanning tools** Modern tools such as Snyk, WhiteSource, and FOSSA can automatically detect licenses and flag potential conflicts. Integrating these tools into continuous integration pipelines ensures that non-compliant components are identified and addressed before release. Automation does not replace human review; instead, it enhances it, ensuring consistent oversight. #### **License education** Developers should receive regular training on how to interpret common open-source licenses, such as the MIT, Apache 2.0, and GPL licenses. Understanding the distinctions between permissive and restrictive licenses empowers teams to make informed choices. Education prevents accidental misuse and builds a culture of responsibility. #### **Legal review and policy enforcement** Legal teams should collaborate with technical leadership to define a clear open-source policy. The policy should specify approved licenses, attribution requirements, and escalation procedures. When engineering teams operate within these boundaries, they reduce uncertainty and simplify their decision-making process. ## The long-term benefits of proactive compliance Organizations that invest in licensing compliance gain measurable advantages beyond risk reduction. #### **Reduced legal exposure** By ensuring that all dependencies comply with their licenses, companies reduce the likelihood of lawsuits and cease-and-desist orders. Legal due diligence becomes smoother during mergers or funding rounds, accelerating transactions and improving valuation. #### **Stronger negotiating position** Compliance establishes credibility. When a company demonstrates control over its intellectual property, it inspires confidence among investors, partners, and clients. This assurance serves as a competitive differentiator in an environment where trust and accountability are paramount. #### **Improved reputation and culture** Compliance reflects discipline. A company that respects software licenses demonstrates its respect for intellectual property. Internally, it fosters a culture of accountability and professionalism. Externally, it builds trust that supports long-term growth. ## Case study: Two paths, two outcomes A software startup incorporated open-source code under restrictive terms without verifying its compatibility. When a major customer requested a license review, auditors uncovered multiple violations. As a result, the company had to halt deliveries and replace the affected components, which delayed its roadmap and strained investor relations. In contrast, another organization in the same sector implemented automated scanning tools and mandatory license reviews early in its development process. When preparing for acquisition, they presented their compliance logs as evidence of their diligence. The acquiring firm appreciated this level of maturity and proceeded with the deal without hesitation. These two outcomes illustrate a crucial truth: compliance is not just about avoiding penalties; it is also about demonstrating control and foresight. ## Why it matters more than you think Licensing compliance may seem like a minor administrative task, but in reality, it defines how safely a company can innovate. The technical and legal worlds converge in this space, and ignoring it invites risks that reach far beyond engineering. Open-source software accelerates progress, but it also introduces obligations. Those who respect those obligations build durable businesses. Those who neglect them often discover their oversight in the courtroom. **Categories:** Business of Software **Tags:** business of software, law, licensing, open-source --- ### [Secure Application Development Starts With Architecture](https://www.woodruff.dev/secure-application-development-starts-with-architecture/) **Published:** October 9, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking to enhance your organization's systems through quality assessments, I would love to connect with you. Let's explore how I can contribute to your success. You can easily reach me via my contact page here: https://woodruff.dev/contact/ ``` ### Security cannot be patched in later; it is structural Most organizations still treat application security as part of the endgame. They build, test, and deploy first, then scramble to patch vulnerabilities discovered too late. This approach creates a false sense of control. Security cannot simply be bolted onto a finished product. It is not a feature but a foundation. Decisions made at the architectural level determine how resilient, trustworthy, and defensible a system will be once it faces real-world threats. ### When security becomes an afterthought, risk multiplies Teams often prioritize security behind other priorities, such as speed, functionality, or user experience. Tight deadlines drive them to deliver visible features before reinforcing unseen safeguards. The result is predictable: applications that look polished on the surface but hide fragile internals beneath. Attackers thrive in these conditions, exploiting weak authentication mechanisms, misconfigured data storage, and overly permissive access policies. The consequences extend far beyond technical failures. A single breach can damage customer confidence, lead to regulatory penalties, and undermine long-term business objectives. What appears to be a development shortcut becomes an expensive problem later. When security is excluded from architectural planning, the cost of retrofitting it can exceed the cost of building the system itself. ### Architectural levers that determine security strength Security-focused architecture requires a deliberate design mindset. Each layer of an application offers opportunities to strengthen protection. Architects influence outcomes through four key areas: identity, data, access, and infrastructure. #### **Authentication and authorization strategy** The first barrier between attackers and assets is identity management. Strong authentication ensures that users are who they claim to be. Effective authorization ensures that users can access only what they are authorized to access. Architecting a consistent identity strategy prevents loopholes and duplication. Centralized identity providers, token-based authentication, and role-based or attribute-based access controls create traceable and enforceable permissions. #### **Data encryption at rest and in transit** Data is the lifeblood of every application. Protecting it requires ensuring that sensitive information remains unreadable to unauthorized parties at all times. Encryption at rest secures databases, backups, and files even if the storage medium is compromised. Encryption in transit protects data moving between users, services, and APIs. Architectural decisions should embed encryption into system design, not treat it as an optional configuration. When encryption is enforced by default, the likelihood of unprotected data exposure falls dramatically. #### **Segmentation and least-privilege principles** Architectural boundaries are critical for limiting the impact of breaches. Segmentation divides systems into isolated zones where each service has access only to the resources it needs. Least privilege ensures that components, users, and processes operate with the minimum necessary permissions. Together, these principles prevent lateral movement during attacks. If one area is compromised, the attacker cannot easily reach another. Building segmentation into architecture also simplifies monitoring and containment. #### **Secure cloud architecture patterns** Modern applications increasingly rely on cloud environments, which introduce both flexibility and complexity. Secure architectures in the cloud use layered defenses: network isolation, secret management, and automated compliance checks. Cloud-native tools, such as identity-managed keys, private endpoints, and workload isolation, reinforce security at scale. The architectural goal is consistency. By using standard patterns for deployment, authentication, and monitoring, teams avoid the chaos of one-off configurations that introduce hidden risks. ### The measurable benefits of security-first architecture Architectural security produces more than peace of mind. It delivers tangible advantages across risk reduction, compliance readiness, and operational cost. #### **Reduced vulnerability surface** When security is built into architecture, the system naturally resists many common attacks. It becomes increasingly complex for an intruder to find an entry point, and even more challenging to exploit it. Continuous reviews of design patterns keep vulnerabilities contained and predictable. #### **Compliance readiness** Organizations operating in regulated industries must prove they protect data properly. A security-first architecture embeds these controls from day one. Logging, audit trails, and encryption standards align automatically with compliance frameworks. Instead of rushing to meet requirements under pressure, companies demonstrate readiness at all times. #### **Lower long-term costs** The cost of fixing vulnerabilities after release often dwarfs the cost of preventing them during design. Reactive security measures consume engineering resources and cause unplanned downtime. In contrast, early architectural investment ensures predictable costs. It also reduces the need for emergency remediation and crisis management. ### Case example: The cost of ignoring architecture A mid-sized technology company built an internal platform to consolidate customer data. The project moved fast, guided by an aggressive launch schedule. Security was left for the final phase. Weeks before rollout, penetration tests uncovered gaps in access control and unencrypted storage of sensitive identifiers. Fixing these issues required re-engineering major components, which delayed the release by four months and doubled the costs. In contrast, another division within the same organization built a new application using a security-first framework. Identity and encryption requirements were integrated during design. Cloud configurations followed consistent templates. The project stayed on schedule and passed its security review without significant findings. The lesson was clear: security-first architecture does not slow development; it accelerates it by removing uncertainty. ### The most secure applications are designed that way from the start Security is not a phase that happens at the end of development. It is a continuous architectural responsibility that defines how systems operate and evolve. Teams that approach security reactively will always face costly surprises. Those that embed it structurally gain stability, trust, and efficiency. The strongest systems are not those patched the most, but those planned with defense in mind. The architecture of a secure application reflects foresight, discipline, and respect for the users who depend on it. Building that foundation early ensures that the system remains resilient long after the first release. **Categories:** Business of Software **Tags:** business of software --- ### [Bridging the Gap Between Software Engineering and Business Goals](https://www.woodruff.dev/bridging-the-gap-between-software-engineering-and-business-goals/) **Published:** October 6, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking to enhance your organization's systems through quality assessments, I would love to connect with you. Let's explore how I can contribute to your success. You can easily reach me via my contact page here: https://woodruff.dev/contact/ ``` ### Security cannot be patched in later; it is structural Many organizations underestimate the importance of incorporating security from the very beginning, viewing it as an accessory instead of a core feature. This mindset can weaken foundations and allow vulnerabilities to take root once the system is operational, making remediation far more costly and complex. It is essential that security decisions begin at the architectural level, defining how every component interacts and ensuring a robust and secure framework for the future. ### Security as an afterthought increases risk Delaying security measures until the end of a project significantly amplifies risk for the entire business. Deadlines force engineers to prioritize functionality over protection, leading to the rapid release of features that lack essential safeguards. This creates a prime opportunity for attackers, who will exploit any vulnerabilities left unaddressed. The fallout from breaches is not merely about technical corrections; companies face serious reputational damage, customer loss, and hefty regulatory penalties. Every shortcut taken today becomes a dangerous liability tomorrow. It is imperative that security be integrated into every stage of the development process to ensure the safety and integrity of the business. ### Architectural levers for secure systems The most effective way to create secure applications is to treat architecture as the lever that directs security decisions. Specific choices at this level have long-term impact. #### **Authentication and authorization strategy** Determining how users prove their identity and what they can access should never be improvised. A consistent identity provider, token-based authentication, and role-based or attribute-based access controls enforce boundaries. These measures prevent privilege creep and contain potential breaches. #### **Data encryption at rest and in transit** Information is the most valuable resource companies own. Protecting it requires encrypting databases, storage accounts, and file systems while also ensuring that all traffic between services and clients is encrypted. Architectural design should enforce encryption by default, leaving no room for insecure configurations. #### **Segmentation and least-privilege principles** Systems should be designed so that each component has the minimum permissions required to operate. Segmentation prevents lateral movement by attackers. If one service is compromised, strict boundaries stop the attacker from gaining access to unrelated systems. Designing this into the architecture eliminates the temptation to grant broad privileges for convenience. #### **Secure cloud architecture patterns** Cloud services offer powerful tools, but they require careful configuration. Secure reference architectures define patterns for network isolation, key management, monitoring, and identity integration. Organizations that build with these patterns ensure that every deployment follows consistent and secure practices. ### Benefits of security-first architecture Investing in security early creates measurable benefits. #### **Reduced vulnerability surface** When security is woven into architecture, the number of exploitable flaws decreases. Systems with fewer attack paths are more resilient and require fewer emergency patches. #### **Compliance readiness** Regulations surrounding privacy and data protection are expanding. Companies that adopt secure design principles at the start can demonstrate compliance without scrambling to retrofit their systems. #### **Lower long-term costs of fixing breaches** The cost of addressing a breach is far higher than the cost of preventing it. Forensic analysis, customer remediation, legal fees, and lost revenue compound quickly. Security-first architecture avoids these reactive costs and enables predictable investments in ongoing protection. ### Case example: Designing security-first to prevent costly rework A financial services company started developing a new customer portal without integrating security into the architecture. The project moved quickly, but just before launch, an audit found flaws in session management and data protection. Fixing these problems required a major redesign, which delayed the launch by six months and increased costs. By contrast, another project within the same organization adopted a security-first approach from the start. Authentication was standardized, encryption was mandatory, and segmented services limited exposure. This portal launched on schedule, passed compliance reviews with ease, and incurred no significant rework. The difference between the two projects illustrated how early architectural decisions determine long-term outcomes. ### The most secure applications are designed that way from the beginning Security cannot be layered on top of existing systems with lasting success. It must be part of the architecture that defines how systems are built, deployed, and maintained. Organizations that adopt this mindset reduce risks, control costs, and gain customer trust. The most secure applications are not necessarily the ones that are patched most often. They are the ones designed securely from the start. **Categories:** Business of Software **Tags:** business of software --- ### [Make Your GitHub Profile Update Itself (WordPress posts, GitHub releases, LinkedIn newsletters)](https://www.woodruff.dev/make-your-github-profile-update-itself-wordpress-posts-github-releases-linkedin-newsletters/) **Published:** October 7, 2025 **Author:** Chris Woodruff **Content:** Want your GitHub profile to look alive without spending your weekends copy-pasting links? Let’s wire it to your actual work: blog posts from WordPress, newly published releases, and your newsletter issues. You will get a profile that quietly refreshes itself on a schedule and on events. We will use: - A **profile README** repo (`github.com//`) - A **GitHub Actions** workflow that runs on a schedule and optional webhooks - A tiny **Python script** that fetches data and rewrites sections between markers - RSS for WordPress, GitHub’s API for releases, and a practical workaround for LinkedIn > Demo vibe: low maintenance, high signal. Set it once, let it run. --- ## What you will build Your profile will contain three live sections: ![](https://woodruff.dev/wp-content/uploads/2025/10/github-profile-967x1024.png)The Action regenerates these blocks and commits changes only when the content actually changes. --- ## Prerequisites - A GitHub account with a **profile repo** named exactly your username Example: if your handle is `cwoodruff`, the repo is ``cwoodruff`/`cwoodruff`` - WordPress category or site feed. Example: `https://woodruff.dev/category/blog/feed/` - Python packages used in CI: `feedparser`, `requests`, `PyYAML` (installed by the workflow) - For LinkedIn, one of: - Cross-post your newsletter to your blog (recommended) - Or provide a stable RSS URL - Or provide a small JSON cache URL you control (via Zapier/Make/n8n or a gist) --- ## Step 1: Add markers to your profile README In your profile repo, edit `README.md` and add: ``` ## Latest on the Blog ## Fresh Releases ## Latest LinkedIn Newsletters ``` You can place these anywhere in your README and style them however you like. The markers must stay intact. --- ## Step 2: Add the workflow Create `.github/workflows/update-profile.yml`: ``` name: Update Profile on: schedule: - cron: "*/30 * * * *" # every 30 minutes workflow_dispatch: {} repository_dispatch: types: [wp-post-published, li-newsletter-published, release-published] permissions: contents: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install deps run: | python -m pip install --upgrade pip pip install feedparser requests PyYAML - name: Update README env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_PAT: ${{ secrets.GH_PAT }} # optional if you need cross-repo access WORDPRESS_FEED: "https://woodruff.dev/category/blog/feed/" GITHUB_USER: "" # Comma-separated repos to watch releases for; leave blank to scan all your public repos RELEASE_REPOS: "owner1/repoA,owner2/repoB" LINKEDIN_RSS: "" # set if you have one LINKEDIN_WEBHOOK_CACHE: "" # set to a JSON list URL if using a low-code relay ITEMS_PER_SECTION: "10" run: | python scripts/update_readme.py - name: Commit changes run: | if [[ -n "$(git status --porcelain)" ]]; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add README.md git commit -m "chore: refresh profile sections" git push else echo "No changes." fi ``` --- ## Step 3: Add the updater script Create `scripts/update_readme.py`: ``` import os, re, requests, feedparser from datetime import datetime, timezone README = "README.md" def between(s, start, end, new_block): pattern = re.compile(rf"({re.escape(start)})(.*)({re.escape(end)})", re.S) return pattern.sub(rf"\1\n{new_block}\n\3", s) def fmt_item(title, url, meta=None): if meta: return f"- [{title}]({url}) \n {meta}" return f"- [{title}]({url})" def fetch_wordpress(feed_url, limit): d = feedparser.parse(feed_url) items = [] for e in d.entries[:limit]: title = e.title link = e.link date = None if getattr(e, "published_parsed", None): date = datetime(*e.published_parsed[:6], tzinfo=timezone.utc).date().isoformat() elif getattr(e, "updated_parsed", None): date = datetime(*e.updated_parsed[:6], tzinfo=timezone.utc).date().isoformat() summary = (getattr(e, "summary", "") or "").strip() summary = re.sub("", "", summary) if len(summary) > 160: summary = summary[:157] + "..." meta = f"*{date}* — {summary}" if date else summary items.append(fmt_item(title, link, meta)) return "\n".join(items) if items else "_No recent posts_" def gh_headers(): pat = os.environ.get("GH_PAT") or os.environ.get("GITHUB_TOKEN") return {"Authorization": f"Bearer {pat}", "Accept": "application/vnd.github+json"} def fetch_release_candidates(user, release_repos): headers = gh_headers() repos = [] if release_repos: repos = [r.strip() for r in release_repos.split(",") if r.strip()] else: page = 1 while True: resp = requests.get(f"https://api.github.com/users/{user}/repos?per_page=100&page={page}", headers=headers, timeout=30) if resp.status_code != 200: break data = resp.json() if not data: break for r in data: repos.append(f"{r['owner']['login']}/{r['name']}") page += 1 return repos def fetch_latest_releases(repos, limit): headers = gh_headers() rels = [] for full in repos: owner, name = full.split("/", 1) r = requests.get(f"https://api.github.com/repos/{owner}/{name}/releases?per_page=1", headers=headers, timeout=30) if r.status_code == 200 and r.json(): rel = r.json()[0] rels.append({ "repo": full, "tag": rel.get("tag_name"), "name": rel.get("name") or rel.get("tag_name"), "url": rel.get("html_url"), "published_at": rel.get("published_at") }) rels.sort(key=lambda x: x["published_at"] or "", reverse=True) out = [] for r in rels[:limit]: date = r["published_at"][:10] if r["published_at"] else "" meta = f"*{date}* — {r['repo']}" out.append(fmt_item(r["name"], r["url"], meta)) return "\n".join(out) if out else "_No recent releases_" def fetch_linkedin_items(linkedin_rss, linkedin_webhook_cache, limit): if linkedin_rss: d = feedparser.parse(linkedin_rss) items = [] for e in d.entries[:limit]: title = e.title link = e.link date = None if getattr(e, "published_parsed", None): date = datetime(*e.published_parsed[:6], tzinfo=timezone.utc).date().isoformat() meta = f"*{date}*" if date else None items.append(fmt_item(title, link, meta)) return "\n".join(items) if items else "_No recent issues_" elif linkedin_webhook_cache: r = requests.get(linkedin_webhook_cache, timeout=30) if r.status_code == 200: arr = r.json()[:limit] items = [fmt_item(x["title"], x["url"], f"*{x.get('date','')}*") for x in arr] return "\n".join(items) if items else "_No recent issues_" return "_Not configured_" def main(): with open(README, "r", encoding="utf-8") as f: readme = f.read() limit = int(os.environ.get("ITEMS_PER_SECTION", "10")) wp_feed = os.environ.get("WORDPRESS_FEED") wp_block = fetch_wordpress(wp_feed, limit) if wp_feed else "_Not configured_" readme = between(readme, "", "", wp_block) user = os.environ.get("GITHUB_USER") rel_repos = os.environ.get("RELEASE_REPOS", "") repos = fetch_release_candidates(user, rel_repos) if user or rel_repos else [] rel_block = fetch_latest_releases(repos, limit) if repos else "_Not configured_" readme = between(readme, "", "", rel_block) li_rss = os.environ.get("LINKEDIN_RSS", "") li_cache = os.environ.get("LINKEDIN_WEBHOOK_CACHE", "") li_block = fetch_linkedin_items(li_rss, li_cache, limit) readme = between(readme, "", "", li_block) with open(README, "w", encoding="utf-8") as f: f.write(readme) if __name__ == "__main__": main() ``` > Tweak summary length, date formatting, and bullet formatting inside the script as you like. --- ## Step 4: Configure environment variables and secrets Set these in the workflow `env:` block and repo **Settings → Secrets and variables → Actions**: KeyWhat it is`WORDPRESS_FEED`Your WP RSS feed URL, for example your blog category feed`GITHUB_USER`Your GitHub username`RELEASE_REPOS`Optional. Comma-separated `owner/repo` to watch. Leave blank to scan your public repos`LINKEDIN_RSS`Optional. Stable RSS URL for your newsletter if available`LINKEDIN_WEBHOOK_CACHE`Optional. A JSON URL you control that returns a list of `{ "title": "...", "url": "...", "date": "YYYY-MM-DD" }``GH_PAT`Optional. A classic or fine-grained PAT if you need to read private org repos. Not required for public data> The built-in `GITHUB_TOKEN` handles committing back to the profile repo. --- ## Step 5: Optional event triggers for near real-time The cron is fine, but events feel magical. Two easy wins: ### A) WordPress publish → profile refresh If you can fire a webhook on publish, have it call GitHub’s `repository_dispatch` on your profile repo: ``` curl -X POST \ -H "Authorization: token " \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos///dispatches \ -d '{"event_type":"wp-post-published","client_payload":{"slug":""}}' ``` Your workflow listens for `repository_dispatch` with type `wp-post-published` and will run immediately. ### B) Release publish → profile refresh In each project that ships releases, add `.github/workflows/notify-profile.yml`: ``` name: Notify profile on release on: release: types: [published] jobs: notify: runs-on: ubuntu-latest steps: - run: | curl -X POST \ -H "Authorization: token ${{ secrets.PROFILE_PAT }}" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos///dispatches \ -d '{"event_type":"release-published"}' ``` Create `PROFILE_PAT` in those repos as a secret with `repo` scope that can dispatch to your profile repo. --- ## Handling LinkedIn newsletters (the practical path) LinkedIn is the tricky one. Here are approaches that work without drama: 1. **Cross-post to WordPress** with a canonical link back to LinkedIn. Your RSS then covers it automatically. 2. **RSS** if your newsletter has a stable public feed. Put that URL in `LINKEDIN_RSS`. 3. **Low-code relay**: use Zapier/Make/n8n to capture new issues and publish a small JSON file somewhere you control (GitHub Pages, gist raw, a tiny serverless endpoint). Set that URL in `LINKEDIN_WEBHOOK_CACHE`. Example JSON your relay could expose: ``` [ { "title": "Issue 12 — Performance tips", "url": "https://www.linkedin.com/pulse/...", "date": "2025-10-01" }, { "title": "Issue 11 — EF Core tricks", "url": "https://www.linkedin.com/pulse/...", "date": "2025-09-24" } ] ``` --- ## Repo structure ``` / ├─ README.md └─ .github/ └─ workflows/ └─ update-profile.yml └─ scripts/ └─ update_readme.py ``` Commit and push. Your profile will refresh on the next cron tick. --- ## Troubleshooting - **No changes committed**: probably no new items or your markers were altered. Keep the exact `` and ``. - **GitHub API rate limits**: lower the schedule frequency or set `RELEASE_REPOS` to a curated list. - **WordPress feed missing items**: check that your category feed is correct, or point `WORDPRESS_FEED` at a different feed URL. - **LinkedIn empty**: that is expected until you set `LINKEDIN_RSS` or `LINKEDIN_WEBHOOK_CACHE`, or you cross-post to WP. --- ## Make it yours - Change list formatting in `fmt_item` to include categories or icons. - Increase `ITEMS_PER_SECTION` to 15 or 20 if you like longer lists. - Add a fourth block for “Upcoming talks” by reading an iCal feed for events. --- That is it. You now have a living GitHub profile that reflects the work you are actually shipping and writing. If you want, I can tailor the `RELEASE_REPOS` and feed URLs to your exact setup and hand you a PR with everything wired up. **Categories:** Blog, fun tech **Tags:** github, python, scripting --- ### [Software Quality Assessment as an Ongoing Practice, Not a One-Time Event](https://www.woodruff.dev/software-quality-assessment-as-an-ongoing-practice-not-a-one-time-event/) **Published:** October 1, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking to enhance your organization's systems through quality assessments, I would love to connect with you. Let's explore how I can contribute to your success. You can easily reach me via my contact page here: https://woodruff.dev/contact/ ``` ### Quality checks are often treated as a box to tick at release time Many organizations still think of quality as something to verify at the end of a project. The testing phase becomes a ritual where checklists are completed, signatures are collected, and the release is declared ready. This approach reduces quality to a formality rather than a discipline. It treats software like a product leaving a factory line, where a single inspection supposedly determines whether it is fit for customers. The truth is that software never stops changing. Code is constantly written, updated, or retired. Systems evolve as businesses adapt to new markets and requirements. A quality check at release time might confirm that a snapshot of the system works, but it cannot guarantee that the software will remain stable, secure, and maintainable in the months and years that follow. ### One-time assessments do not capture long-term health When organizations limit quality to one-time audits, they miss the opportunity to understand the ongoing health of their systems. Audits often highlight issues, but they rarely provide mechanisms for addressing the root causes of recurring defects. Instead, teams repeat the cycle of testing at release time, discovering problems late, and rushing to patch them under pressure. This cycle damages morale and leads to wasted effort. Developers become frustrated by repeated firefighting. Customers lose confidence when updates cause regressions. Leadership sees costs rise as fixes consume time that could have been used for new features. In short, one-time assessments deliver momentary assurance while ignoring the continuous nature of software development. ### Make quality a continuous architectural practice To achieve sustainable quality, organizations must shift from thinking of quality as an audit to treating it as an architectural principle. Quality becomes part of the design and construction process itself, not something added at the end. This requires tools, practices, and cultural habits that reinforce continuous improvement. When quality is built into daily work, teams no longer scramble to catch problems at the last moment. Instead, they identify and resolve issues as they arise. This steady, incremental approach creates systems that are more reliable, easier to maintain, and less costly in the long run. ### Approaches to continuous quality #### **Automated testing and CI/CD gates** Automation ensures that quality checks happen consistently, not sporadically. Unit tests, integration tests, and regression tests can run automatically with every code commit. Continuous integration and delivery pipelines provide gates that prevent unstable code from reaching production. Automation creates a safety net that catches errors before they affect users. #### **Code quality metrics** Metrics give teams visibility into the state of their code. Measurements such as maintainability, test coverage, and complexity highlight areas of risk. These metrics should not be viewed as punitive but as guidance. A module with rising complexity can be flagged for refactoring before it becomes unmanageable. Regular tracking of metrics creates accountability and keeps teams focused on long-term health. #### **Performance baselines tracked over time** Performance should be monitored as carefully as functionality. Establishing baselines allows teams to detect regressions early. If response times increase or memory usage rises, alerts can prompt an investigation before users complain. Tracking performance over time ensures that growth in features does not come at the expense of speed or reliability. ### Case example: Embedding quality into sprints One organization struggling with recurring incidents decided to embed quality reviews directly into its sprint process. Instead of running separate audits, each sprint included automated testing enhancements, metric reviews, and performance checks. Developers reviewed results as part of their daily stand-ups, treating quality indicators like any other task. Within months, the number of production incidents declined significantly. The team no longer spent its energy fixing issues after release. Instead, they built confidence that new features would integrate smoothly. Leadership noticed the difference in customer satisfaction and in the predictability of delivery schedules. Quality was no longer a bottleneck at release time but an active contributor to business outcomes. ### True quality is continuous, a process, not a phase Sustainable quality is not produced by occasional audits. It comes from repeatable, measurable practices that operate continuously. Software will always change, and with that change comes risk. The only way to manage this risk is through a culture and architecture that make quality part of every step. Organizations that embrace continuous quality benefit from fewer defects, lower costs, and more satisfied customers. The message is clear: true quality is a process, not a phase. **Categories:** Business of Software **Tags:** business of software --- ### [The Hidden ROI of Technical Due Diligence in Software Investments](https://www.woodruff.dev/the-hidden-roi-of-technical-due-diligence-in-software-investments/) **Published:** September 29, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking to enhance your organization's ROI through strategic software investments, I would love to connect with you. Let's explore how I can contribute to your success. You can easily reach me via my contact page here: https://woodruff.dev/contact/ ``` ### Most investment decisions focus on financials, not software foundations When investors assess a company, the starting point is often its financial performance. Key indicators like revenue growth, profitability, and market share are crucial in shaping their investment decisions. However, for technology-driven businesses, the real determinant of value is the robustness of the software that underpins their operations, delivers services, and scales to meet demand. Neglecting the state of the software can transform a seemingly promising investment into a disappointing one. While solid financials may suggest stability, weak software can swiftly erode that impression. This is precisely why conducting thorough technical due diligence is non-negotiable for securing investments. ### Hidden technical debt can erode valuations and stall growth Technical debt is often one of the most underestimated risks in technology investments. This debt builds up when teams make short-term trade-offs in design or development. Factors like poor documentation, rushed architectural decisions, and inadequate testing all contribute to its accumulation. At a small scale, these issues may go unnoticed, but they become apparent as growth accelerates. Hidden technical debt can significantly reduce a company’s valuation and hinder its expansion. A company may promise rapid product delivery but struggle to execute because its systems lack the resilience needed to support new features. Another company might report impressive early adoption numbers but collapse under heavier loads. This risk is very real; investors who acquire companies without thoroughly investigating their technical health often find that growth projections were overly optimistic due to the inability of the underlying systems to sustain them. ### Technical due diligence uncovers risks early The solution is simple: conduct technical due diligence with the same level of rigor as financial or legal reviews. Technical due diligence uncovers flaws that financial metrics may overlook. It clarifies whether a company’s software assets can genuinely support the presented business case. By assessing the state of the codebase, architecture, security posture, and licensing compliance, investors gain insights into the long-term viability of their targets. This process not only identifies potential issues but also establishes a baseline for determining the necessary investment to align technology with business objectives. ### What technical due diligence includes #### **Codebase quality** Software lives and dies by the quality of its code. A review of the codebase assesses its readability, maintainability, and testing. Code that is unnecessarily complex slows down development cycles and multiplies the chance of defects. Clean, consistent code enables new developers to contribute quickly and supports faster release cycles. #### **Architecture scalability** The ability to scale is central to a company’s growth. An assessment of architecture reveals if the system can handle increased traffic, adapt to new markets, and incorporate emerging technologies. Architecture that cannot scale will eventually choke growth and require costly rework. Scalability is not a nice-to-have feature; it is essential to long-term success. #### **Security posture** Security failures carry both financial and reputational costs. A thorough review of security practices examines how applications protect data, manage access, and withstand attacks. With regulations tightening and customers becoming increasingly concerned about privacy, weak security can erode trust and lead to penalties. #### **Licensing compliance** Open-source components form the backbone of most modern applications. If these components are not properly licensed, a company can face legal disputes that jeopardize intellectual property. Technical due diligence reviews licensing records and dependencies to ensure that the assets being acquired are truly owned and free of encumbrances. ### Benefits of technical due diligence The value of technical due diligence extends beyond risk avoidance. It creates positive returns in multiple areas. #### **Avoiding costly rework** When issues are discovered before acquisition, buyers can negotiate solutions or walk away. If overlooked, those same issues often demand millions in remediation. Early detection prevents the hidden drain of expensive rewrites and redesigns. #### **Faster scaling post-acquisition** If systems have been vetted for scalability and code quality, integration into a larger enterprise happens faster. Teams can focus on innovation rather than patching foundational issues. This accelerates time-to-market for new offerings and helps realize growth projections more reliably. #### **Stronger negotiating position for both sides** Clear insight into technical strengths and weaknesses empowers buyers to set fair valuations. Sellers with strong technical hygiene can demonstrate their value with evidence, often commanding higher multiples. The process promotes transparency and confidence, benefiting both parties. ### Technical due diligence is not a cost; it is insurance and acceleration Investors often underestimate technical due diligence, viewing it as an unnecessary expense. In reality, it is essential—acting as both a form of insurance and a powerful growth accelerator. It actively protects capital from hidden risks and ensures that technology assets are fully capable of supporting expansion. Financial reviews may suggest potential, but only technical reviews can definitively confirm that this potential will be realized. The hidden return on investment (ROI) of technical due diligence is significant, as it transforms uncertainty into unwavering confidence. This positions both investors and companies to aggressively pursue growth without the burden of unseen obstacles. **Categories:** Business of Software **Tags:** business of software --- ### [Bringing Simplicity-First to the Page: My Upcoming Book](https://www.woodruff.dev/bringing-simplicity-first-to-the-page-my-upcoming-book/) **Published:** September 27, 2025 **Author:** Chris Woodruff **Content:** I am excited to share some big news. In the second half of 2026, my new book, titled **Software Architecture Made Simple: A ‘Simplicity-First’ Approach to Software in the Age of Complexity, will be released**. This book compiles years of blog articles, newsletters, and social media posts, consolidating them into a single, focused source of knowledge. Think of it as the decluttering of my writing life, except instead of cleaning my garage, I am organizing ideas about software. ### What’s in the Book? The book builds on the **Simplicity-First** philosophy and brings together six key areas: - **Software Architecture** – laying the structural foundation of simplicity. - **Software Construction** – focusing on the craft of building with simplicity in code and process. - **Business Agility** – translating simplicity into adaptive product and organizational strategy. - **Technology and Business Philosophy** – exploring guiding principles that shape long-term vision. - **Business and Technology Ethics** – ensuring simplicity aligns with fairness, responsibility, and trust. - **Sustainability** – connecting simplicity to environmental, economic, and societal longevity. Each area tackles questions developers, architects, and leaders face every day—like: *“How do I avoid designing for edge cases before I even solve the main problem?”* or *“Why does our architecture diagram look like spaghetti someone threw at the wall… and forgot to clean up?”* (Spoiler: if your diagram looks like dinner, you may not be serving your users very well.) This project is both serious in its goals and a bit of fun in its delivery. Simplicity does not mean boring. It means building software that works better for people, for organizations, and for the future. ### A Sneak Peek at the Book’s Structure For those who would like to know how the story unfolds, here is a preview of the sections and chapters that comprise Software Architecture Made Simple: A ‘Simplicity-First’ Approach to Software in the Age of Complexity. #### Part I – The Problem with Complexity 1. The Complexity Trap in Modern Software 2. The Illusion of Future Proofing 3. The 2 AM Test #### Part II – The Simplicity-First Mindset 4. The Half-Rule of Simplicity 5. Green Software Starts at the Keyboard 6. Stop Designing for Edge Cases First #### Part III – Practical Simplicity in Architecture 7. Modular Monoliths over Microservice Sprawl 8. Scaling Smarter, Not Bigger 9. Making Legacy Work for You 10. Simplicity in the Cloud #### Part IV – Simplicity in Practice 11. Teaching Simplicity 12. Simplicity in Tooling and Workflow 13. Debugging and Maintenance Made Simple 14. Simplicity in Team Communication 15. Simplicity in Decision-Making #### Part V – The Green Connection: Simplicity Meets Sustainability 16. Why Simple Software is Green Software 17. Measuring the Impact of Simplicity 18. Designing for Efficiency, Not Waste 19. Case Studies in Green Simplicity #### Part VI – AI Through the Lens of Simplicity 20. AI as an Amplifier of Simplicity or Complexity 21. Human in the Loop Architecture 22. AI-Driven Construction: The Simple Way #### Part VII – The Future of Simple 23. AI, Rust, and the Next Wave of Tools 24. Craftsmanship over Trends 25. The Simplicity-First Playbook ### What’s Next? Between now and release, I’ll be sharing sneak peeks, behind-the-scenes thoughts, and maybe even a few bloopers from the writing process (like the time I tried to explain “modular monoliths” to my dog… he wasn’t impressed). So, buckle up—we’re going to take a journey into a world where simple isn’t dull, it’s brilliant. Because in software, as in life: **Simplicity scales, complexity crumbles.** **Categories:** Simplicity-First **Tags:** .NET, ai, architecture, business of software, C#, dotnet, programming --- ### [System Modernization Without the Burnout: Lessons from Distributed Systems](https://www.woodruff.dev/system-modernization-without-the-burnout-lessons-from-distributed-systems/) **Published:** September 26, 2025 **Author:** Chris Woodruff **Content:** ``` If you're looking for support in successfully migrating your legacy system, I would love to help your organization through this process. Let's connect and explore how I can assist you. You can contact me directly via my contact page here: https://woodruff.dev/contact/ ``` ### The Dreaded “Big-Bang Rewrite” Every developer has heard the horror story: the company that decided to throw away its legacy system and rewrite everything from scratch in one giant project. Months turn into years, budgets double, teams burn out, and in the end, the “new” system often launches missing key functionality, or worse, never launches at all. These dreaded “big-bang rewrites” are supposed to free organizations from legacy pain, but often create bigger headaches than the ones they were trying to solve. ### When Modernization Burns Out Teams Legacy modernization is one of the most critical yet riskiest endeavors in software development. Older applications weigh businesses down with technical debt, security risks, and high maintenance costs. But trying to replace them outright pushes teams beyond their limits. Developers and architects find themselves working double duty, maintaining the old system while building the new one. Stakeholders lose patience when deadlines slip. Leadership grows frustrated as budgets balloon. The result? Teams burn out, morale collapses, and organizations end up stuck with two half-working systems instead of one solid foundation. ### Distributed Systems Show Us a Better Way Here’s the lesson: systems don’t have to be modernized in one piece. Distributed systems, those collections of smaller, independent services, teach us that complexity can be managed by breaking it into smaller, more digestible parts. Instead of trying to fix the whole thing at once, modernization can become a staged, incremental process. This not only reduces risk but also helps keep teams energized and stakeholders engaged. ### Strategies for Sustainable Modernization 1. **Modular Monolith vs. Microservices-First** The industry has long been fascinated by microservices, but jumping straight into them can create chaos. Many organizations find success with a **modular monolith**, a single deployable system organized into clear, independent modules. This structure keeps complexity manageable while setting the stage for future decomposition into microservices if needed. Think of it as renovating a house room by room instead of knocking it down to the studs. You still live in it while you improve it. 2. **Incremental Modernization with Feature Toggles** Feature toggles (or flags) let teams release updates in small increments, hiding unfinished functionality until it’s ready. Instead of waiting months for a “big release,” users see steady improvements while developers can test new code in production safely. This approach not only reduces risk but also provides business leaders with visible progress, which is critical for maintaining support during long modernization projects. 3. **Strangling the Monolith** Coined by Martin Fowler, the **Strangler Fig pattern** is one of the most effective strategies for modernization. Instead of replacing a legacy system wholesale, you wrap it with a new architecture and gradually replace pieces until the old system “withers away.” This method allows for parallel operation: legacy code continues to run while new services gradually take over. It minimizes disruption and lets organizations control the pace of change. ### Modernization Without the Meltdown At a mid-sized real estate technology company, the engineering team faced the challenge of replacing a fragile, point-to-point integration system. Leadership initially proposed a complete rewrite. But after evaluating the risks, the team opted for a staged approach. They began by wrapping the legacy system with a new event-driven integration platform. Using a modular design and consistent event schemas, they gradually shifted critical services to the new platform while the old system continued to run. Over time, more functions migrated until the legacy model could be retired entirely. The results were dramatic: scalability and resilience improved, feature delivery accelerated, and the team avoided the burnout of maintaining two competing systems. Most importantly, the modernization was completed without the chaos of a big-bang rewrite. ### Modernization Without Burnout Is Possible System modernization will always be challenging. But it doesn’t have to be painful, unsustainable, or a career-ending exercise in futility. By applying lessons from distributed systems, breaking down complexity into smaller parts, modernizing incrementally, and adopting staged strategies, organizations can move forward with confidence. Modernization is not about building everything new in one shot. It’s about **making wise, sustainable choices** that keep systems, teams, and businesses healthy for the long run. The dreaded big-bang rewrite may never entirely disappear from boardroom fantasies, but in practice, the path to success is incremental, thoughtful, and built on lessons learned from distributed systems. **Categories:** Business of Software **Tags:** business of software --- ### [Why Fractional Architecture is the Future of Technology Strategy](https://www.woodruff.dev/why-fractional-architecture-is-the-future-of-technology-strategy/) **Published:** September 22, 2025 **Author:** Chris Woodruff **Content:** ``` If you’d like to chat about how I can help your organization as a Fractional Architect, I’d love to connect. You can reach me directly through my contact page here: https://woodruff.dev/contact/ ``` ### Hook: The Myth of Big-Enterprise Architecture For years, software architecture has carried an aura of exclusivity, something reserved for Fortune 500 companies with expansive IT budgets and dedicated teams focused on enterprise systems. Smaller and mid-sized organizations often believed they didn’t need architects, or worse, that architecture was a luxury they could only afford once they “made it big.” This myth has persisted across industries, leaving many SMBs and growth-stage companies building software without the strategic oversight that prevents costly mistakes. But architecture isn’t about size, it’s about direction. Every business that invests in technology makes architectural decisions, whether they acknowledge them or not. Ignoring architecture doesn’t make it disappear; it simply hides the risks until they surface in the form of outages, spiraling costs, or failed initiatives. ### Problem: Complexity Without the Budget Today’s SMBs and scale-ups face the same challenges as larger enterprises: legacy modernization, cloud migration, security compliance, distributed systems, and integration across dozens of applications. The difference? They rarely have the budget for a full-time Chief Architect or CTO. Instead, they patch together decisions in the moment: a database schema optimized for speed rather than scalability, a cloud environment provisioned by whoever had the AWS console password, or a web application written under deadline pressure with no thought for maintainability. These choices may seem small, but they accumulate into a technical debt load that can cripple an organization’s agility. As someone who has led modernization projects at Rocket Homes and architected distributed cloud platforms at Real Time Technologies, I’ve seen firsthand how even well-intentioned teams can struggle when architectural oversight is missing. Without a guiding hand, complexity grows unchecked, and technology drifts out of alignment with business goals. ### Solution: Fractional Architecture as On-Demand Expertise This is where fractional architecture comes in. Instead of hiring a full-time architect, often a six-figure commitment that’s out of reach for many organizations, companies can bring in seasoned architects on a fractional or advisory basis. Think of it as having a strategic partner on demand, without the overhead of a permanent executive role. A fractional architect helps organizations: - **Evaluate technology decisions before they become sunk costs** - **Design modernization strategies that balance risk and reward** - **Implement secure application development practices at the architectural level** - **Guide migrations and integrations without derailing ongoing operations** Much like how companies rely on fractional CFOs for financial expertise or fractional CMOs for marketing strategy, fractional architecture brings senior-level technical strategy into reach for organizations of any size. ### Benefits of Fractional Architecture The appeal isn’t just in cost savings, it’s in agility, accountability, and outcomes. 1. **Access to seasoned expertise without long-term overhead** You gain the insights of someone who has already led cloud migrations, architected distributed systems, and managed modernization efforts, but only pay for what you need. 2. **Flexibility to scale engagement up or down** Some projects require an intense burst of architectural focus, designing a new platform, evaluating a SaaS product, or preparing for due diligence. Other times, only light-touch guidance is needed. Fractional architecture scales with those rhythms. 3. **Faster time-to-decision for technology investments** In fast-moving environments, indecision can be costlier than mistakes. A fractional architect can quickly cut through options, frame trade-offs, and guide leadership toward the best path forward. ### Case Example: Preventing Costly Missteps One company I worked with planned to migrate its on-premise applications directly to a cloud-hosted VM environment. On paper, this looked like a quick win: minimal code changes, a straightforward path, and immediate cloud presence. But when we analyzed their workloads and long-term goals, it became clear that “lift-and-shift” would lock them into higher costs and limit scalability. By pivoting to a modular architecture with Azure App Services and a message-driven integration layer, the company not only reduced operational costs but also enabled independent scaling of key services. That decision, made with architectural foresight, saved them from what could have been years of technical debt and budget strain. This is the hidden power of fractional architecture: preventing expensive mistakes before they happen. ### Closing: The Best of Both Worlds Fractional architecture offers the best of both worlds: the expertise of a senior architect without the financial or organizational burden of a full-time executive hire. It empowers SMBs and growth-stage companies to pursue modernization, cloud migration, and secure development with confidence. The future of technology strategy won’t be defined by who has the biggest architecture team. It will be defined by who makes the most intelligent architectural choices, and fractional architecture puts that within reach for every organization. **Categories:** Business of Software **Tags:** architecture, business of software --- ### [Day 35: Evolution Beyond Biology: Using Genetic Algorithms for Creative Art and Design](https://www.woodruff.dev/day-35-evolution-beyond-biology-using-genetic-algorithms-for-creative-art-and-design/) **Published:** September 18, 2025 **Author:** Chris Woodruff **Content:** Genetic Algorithms are often associated with engineering, scheduling, or optimization problems, but their potential extends into the domain of art and design. When applied to visual composition, generative structures, or music synthesis, GAs can produce unexpected and compelling outcomes. These creative applications demonstrate that evolution-inspired algorithms are not limited to purely functional results. In this post, we explore how you can use Genetic Algorithms in C# to evolve digital art and generative designs. ### Chromosomes for Artistic Parameters To evolve art or design, we must represent the artistic space as a chromosome. For generative visual art, a simple chromosome might encode shapes, colors, and layout features. ``` public class ArtChromosome { public List Shapes { get; set; } = new List(); public double Fitness { get; set; } } ``` Each shape can be a gene representing a primitive form, such as a circle or square, with attributes like position, size, and color. ``` public class ShapeGene { public int X { get; set; } public int Y { get; set; } public int Size { get; set; } public Color Color { get; set; } public ShapeType ShapeType { get; set; } } public enum ShapeType { Circle, Square, Triangle } ``` ### Fitness by Aesthetic Criteria Defining a fitness function in a creative context is subjective. You can use metrics such as symmetry, color harmony, balance, or even crowd-sourced preferences or machine-learned aesthetic scores. Here’s an example of a basic symmetry-based fitness function: ``` public static double EvaluateSymmetry(ArtChromosome chromosome, int canvasWidth) { int symmetryScore = 0; foreach (var shape in chromosome.Shapes) { int mirroredX = canvasWidth - shape.X; bool hasMirror = chromosome.Shapes.Any(s => Math.Abs(s.X - mirroredX) < 5 && s.Y == shape.Y && s.Size == shape.Size && s.ShapeType == shape.ShapeType); if (hasMirror) symmetryScore++; } return symmetryScore / (double)chromosome.Shapes.Count; } ``` This example favors art with horizontal symmetry, often perceived as aesthetically pleasing. ### Mutation and Crossover for Art Mutations can change a shape’s attributes to introduce variety. ``` public static void Mutate(ArtChromosome chromosome) { Random rand = new Random(); int index = rand.Next(chromosome.Shapes.Count); var shape = chromosome.Shapes[index]; shape.X = rand.Next(0, 800); shape.Y = rand.Next(0, 600); shape.Size = rand.Next(10, 100); shape.Color = Color.FromArgb(rand.Next(256), rand.Next(256), rand.Next(256)); } ``` Crossover might involve combining half of the shapes from each parent. ``` public static ArtChromosome Crossover(ArtChromosome parent1, ArtChromosome parent2) { int midpoint = parent1.Shapes.Count / 2; return new ArtChromosome { Shapes = parent1.Shapes.Take(midpoint) .Concat(parent2.Shapes.Skip(midpoint)) .ToList() }; } ``` ### Rendering the Artwork Use `System.Drawing` or a library like `SkiaSharp` to render the artwork based on the chromosome. ``` public static void Render(ArtChromosome chromosome, string filePath) { int width = 800; int height = 600; using var bitmap = new Bitmap(width, height); using var graphics = Graphics.FromImage(bitmap); graphics.Clear(Color.White); foreach (var shape in chromosome.Shapes) { using var brush = new SolidBrush(shape.Color); switch (shape.ShapeType) { case ShapeType.Circle: graphics.FillEllipse(brush, shape.X, shape.Y, shape.Size, shape.Size); break; case ShapeType.Square: graphics.FillRectangle(brush, shape.X, shape.Y, shape.Size, shape.Size); break; case ShapeType.Triangle: // You would implement drawing triangle logic here break; } } bitmap.Save(filePath); } ``` ### Interactive Evolution A compelling way to guide artistic evolution is through human feedback. You could build a simple UI where users rank artwork each generation, and the most liked images influence the next generation. This interactive evolution harnesses subjective preference, which is otherwise hard to encode. ### Expanding to Generative Design The same concepts apply to other creative fields. Use GAs to evolve furniture layouts, architecture structures, or even musical sequences. In each case, define the gene space, fitness criteria, and appropriate variation operators. For example, in generative product design: - Genes could represent dimensions, materials, and structural properties - Fitness could be a combination of aesthetics and mechanical performance (simulated with physics engines) ### Final Thoughts Applying Genetic Algorithms to art and design flips the usual script. Instead of optimization toward strict numerical goals, we explore open-ended creativity. The stochastic nature of GAs, combined with loosely defined objectives, enables novel and unexpected results. These techniques can support ideation in design tools, augment human creativity, and inspire hybrid workflows between algorithm and artist. Genetic Algorithms can be more than tools for function; they can be instruments for imagination. ### Thank You As we wrap up this Genetic Algorithms series, I want to thank you for following along. Whether you read a single post or every day from start to finish, I genuinely appreciate the time you invested. Writing this series has been as much about sharing ideas as it has been about learning together, and your support has made it worthwhile. I hope the concepts, examples, and experiments have sparked new ideas for your own projects and inspired you to keep exploring how evolution in code can solve real-world problems. Here’s to continued learning and building smarter solutions… together. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 34: Genetic Algorithms vs. Other Optimization Techniques: A Developer's Perspective](https://www.woodruff.dev/day-34-genetic-algorithms-vs-other-optimization-techniques-a-developers-perspective/) **Published:** September 17, 2025 **Author:** Chris Woodruff **Content:** Genetic Algorithms (GAs) are a powerful optimization strategy inspired by the principles of natural evolution. But they are far from the only technique in a developer’s toolbox. In this post, we will compare Genetic Algorithms with other widely-used optimization methods such as Gradient Descent, Simulated Annealing, and Particle Swarm Optimization. The goal is to understand when to use GAs and how they differ in behavior, complexity, and application suitability. ### Genetic Algorithms: Population-Based and Stochastic GAs maintain a population of candidate solutions that evolve over generations. At each iteration, individuals are selected, combined (crossover), and mutated to explore the solution space. GAs are particularly effective in large, complex, and poorly understood spaces, especially where the objective function is discontinuous, non-differentiable, or noisy. ``` public class Chromosome { public double[] Genes { get; set; } public double Fitness { get; set; } } public class GeneticAlgorithm { private List population; public void Evolve() { // Selection, crossover, mutation logic } } ``` ### Gradient Descent: Deterministic and Derivative-Based Gradient Descent is a first-order optimization algorithm used primarily in convex optimization and machine learning. It relies on the gradient (partial derivatives) of the objective function to determine the direction of steepest descent. ``` public static double GradientDescent(Func func, Func derivative, double initialGuess, double learningRate, int iterations) { double x = initialGuess; for (int i = 0; i < iterations; i++) { x -= learningRate * derivative(x); } return x; } ``` Gradient Descent is efficient and precise for smooth functions, but it struggles with local minima, discontinuities, or non-differentiable problems. In contrast, GAs can handle these with ease but usually require more computation time. ### Simulated Annealing: Probabilistic with Decaying Temperature Simulated Annealing is a single-solution metaheuristic inspired by the annealing process in metallurgy. It explores the solution space by probabilistically accepting worse solutions to escape local optima, gradually reducing the acceptance rate as the temperature cools. ``` public static double SimulatedAnnealing(Func costFunction, double initialSolution, double initialTemp, double coolingRate) { double current = initialSolution; double best = current; double temp = initialTemp; Random rand = new Random(); while (temp > 0.1) { double next = current + rand.NextDouble() * 2 - 1; // small perturbation double delta = costFunction(next) - costFunction(current); if (delta < 0 || Math.Exp(-delta / temp) > rand.NextDouble()) { current = next; if (costFunction(current) < costFunction(best)) best = current; } temp *= coolingRate; } return best; } ``` Simulated Annealing and GAs both avoid local minima but differ in structure. GAs use populations and recombination while Simulated Annealing works with a single candidate and a cooling schedule. ### Particle Swarm Optimization: Inspired by Flock Behavior Particle Swarm Optimization (PSO) is inspired by the behavior of birds and fish. It maintains a population (swarm) of particles that update their positions in the search space based on their own experience and that of their neighbors. ``` public class Particle { public double Position { get; set; } public double Velocity { get; set; } public double BestPosition { get; set; } } ``` PSO and GAs both operate on populations, but PSO emphasizes sharing information among particles, which tends to make convergence faster for many problems. GAs, with their mutation and crossover mechanisms, promote diversity better and are often more robust for deceptive landscapes. ### Summary of Differences FeatureGenetic AlgorithmsGradient DescentSimulated AnnealingParticle Swarm OptimizationNaturePopulation-basedSingle-solutionSingle-solutionPopulation-basedUses DerivativesNoYesNoNoHandles Local MinimaYesNoYesYesSuitable forComplex, rugged search spacesSmooth, convex functionsProblems with many local optimaContinuous optimizationStochasticYesNoYesYesParallelizableHighlySomewhatSomewhatHighly### When to Use Genetic Algorithms Use GAs when: - The objective function is unknown or difficult to express mathematically - The solution space is vast and multi-modal - Other optimization methods fail to escape local optima - You need to optimize multiple conflicting objectives - You value diversity and exploration over convergence speed ### Final Thoughts GAs are not a silver bullet, but they shine in domains where structure is poorly understood or traditional methods cannot be applied. As a developer, understanding the trade-offs between different optimization techniques allows you to select the right approach based on the problem characteristics. In future posts, we will look at building hybrid models that combine the strengths of different optimization techniques including GAs, hill climbing, and machine learning. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 33: Case Study: Using a Genetic Algorithms to Optimize Hyperparameters in a Neural Network](https://www.woodruff.dev/day-33-case-study-using-a-genetic-algorithms-to-optimize-hyperparameters-in-a-neural-network/) **Published:** September 16, 2025 **Author:** Chris Woodruff **Content:** Tuning hyperparameters for machine learning models like neural networks can be tedious and time-consuming. Traditional grid search or random search lacks efficiency in high-dimensional or non-linear search spaces. Genetic Algorithms (GAs) offer a compelling alternative by navigating the hyperparameter space with adaptive and evolutionary pressure. In this post, we’ll walk through using a Genetic Algorithm in C# to optimize neural network hyperparameters using a practical example. ### The Optimization Problem Let’s say you have a simple feedforward neural network built with ML.NET or a custom framework. Your goal is to find the best combination of the following hyperparameters: - Number of hidden layers (1 to 3) - Neurons per layer (10 to 100) - Learning rate (0.0001 to 0.1) - Batch size (16 to 256) Each combination impacts the training accuracy and performance of the model. Using a GA, we encode each hyperparameter into a gene and build chromosomes representing complete configurations. ### Chromosome Design We can define a chromosome as a list of parameters: ``` public class HyperparameterChromosome { public int HiddenLayers { get; set; } // [1-3] public int NeuronsPerLayer { get; set; } // [10-100] public double LearningRate { get; set; } // [0.0001 - 0.1] public int BatchSize { get; set; } // [16 - 256] public double Fitness { get; set; } } ``` A random generator function initializes a diverse population: ``` public static HyperparameterChromosome GenerateRandomChromosome() { Random rand = new(); return new HyperparameterChromosome { HiddenLayers = rand.Next(1, 4), NeuronsPerLayer = rand.Next(10, 101), LearningRate = Math.Round(rand.NextDouble() * (0.1 - 0.0001) + 0.0001, 5), BatchSize = rand.Next(16, 257) }; } ``` ### Fitness Function: Model Evaluation We define fitness based on validation accuracy from training the neural network using the encoded hyperparameters. To save time during this example, we simulate the evaluation step with a mocked accuracy function: ``` public static double EvaluateChromosome(HyperparameterChromosome chromo) { // Simulate evaluation for example purposes double accuracy = 0.6 + (0.1 * (chromo.HiddenLayers - 1)) + (0.001 * chromo.NeuronsPerLayer) - Math.Abs(chromo.LearningRate - 0.01) + (0.0001 * chromo.BatchSize); chromo.Fitness = accuracy; return accuracy; } ``` In a real-world case, you’d integrate this with a training loop using a machine learning library and compute validation accuracy. ### Genetic Operators Use standard crossover and mutation mechanisms. For simplicity, we’ll implement single-point crossover and bounded random mutation: ``` public static (HyperparameterChromosome, HyperparameterChromosome) Crossover(HyperparameterChromosome p1, HyperparameterChromosome p2) { return ( new HyperparameterChromosome { HiddenLayers = p1.HiddenLayers, NeuronsPerLayer = p2.NeuronsPerLayer, LearningRate = p1.LearningRate, BatchSize = p2.BatchSize }, new HyperparameterChromosome { HiddenLayers = p2.HiddenLayers, NeuronsPerLayer = p1.NeuronsPerLayer, LearningRate = p2.LearningRate, BatchSize = p1.BatchSize } ); } public static void Mutate(HyperparameterChromosome chromo) { Random rand = new(); int geneToMutate = rand.Next(0, 4); switch (geneToMutate) { case 0: chromo.HiddenLayers = rand.Next(1, 4); break; case 1: chromo.NeuronsPerLayer = rand.Next(10, 101); break; case 2: chromo.LearningRate = Math.Round(rand.NextDouble() * (0.1 - 0.0001) + 0.0001, 5); break; case 3: chromo.BatchSize = rand.Next(16, 257); break; } } ``` ### GA Loop The core loop evaluates the population, selects parents, applies crossover and mutation, and evolves across generations. ``` int populationSize = 50; int generations = 30; double mutationRate = 0.1; List population = Enumerable.Range(0, populationSize) .Select(_ => GenerateRandomChromosome()) .ToList(); for (int gen = 0; gen < generations; gen++) { foreach (var chromo in population) EvaluateChromosome(chromo); population = population.OrderByDescending(c => c.Fitness).ToList(); List newPopulation = new List(); while (newPopulation.Count < populationSize) { var parent1 = TournamentSelect(population); var parent2 = TournamentSelect(population); var (child1, child2) = Crossover(parent1, parent2); if (new Random().NextDouble() < mutationRate) Mutate(child1); if (new Random().NextDouble() < mutationRate) Mutate(child2); newPopulation.Add(child1); newPopulation.Add(child2); } population = newPopulation; Console.WriteLine($"Gen {gen}: Best Accuracy = {population[0].Fitness:F4}"); } ``` ### Tournament Selection A common selection method that balances performance and diversity: ``` public static HyperparameterChromosome TournamentSelect(List population, int tournamentSize = 5) { Random rand = new(); var competitors = population.OrderBy(_ => rand.Next()).Take(tournamentSize).ToList(); return competitors.OrderByDescending(c => c.Fitness).First(); } ``` ### Results and Use Cases When integrated with real neural network training, this GA framework can intelligently explore the hyperparameter landscape. It adapts based on feedback and converges toward high-performing configurations over time. This approach is especially useful when: - The search space is non-continuous or complex - Training time is expensive and gradient-free optimization is preferred - Combinatorial parameter dependencies exist GAs will not replace traditional optimizers for every use case, but they can offer robustness and simplicity when tuning difficult or poorly-behaved models. In the next post, we will look at integrating GA-based hyperparameter tuning into an ML pipeline using automated tools and reporting. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 32: When Genetic Algorithms Go Wrong: Debugging Poor Performance and Premature Convergence](https://www.woodruff.dev/day-32-when-genetic-algorithms-go-wrong-debugging-poor-performance-and-premature-convergence/) **Published:** September 15, 2025 **Author:** Chris Woodruff **Content:** Even well-written Genetic Algorithms can fail. You might see little improvement over generations, results clustering around poor solutions, or a complete stall in progress. These symptoms often point to premature convergence, loss of genetic diversity, or flaws in selection and fitness evaluation. Debugging GAs requires tools, insight, and techniques for diagnosis and correction. ### Understanding Premature Convergence Premature convergence occurs when the population loses diversity too early. The algorithm converges to a local optimum and cannot recover due to lack of variation in the gene pool. **Symptoms:** - All individuals have nearly identical genes - Best fitness plateaus early - Mutation has little visible effect **Causes:** - High selection pressure - Low mutation rate - No diversity-preserving mechanism ### Strategy 1: Track Gene Diversity Gene-level analysis reveals how varied your population is. For example, in a character-based chromosome, you can count how many distinct values exist per gene position. ``` public double CalculateDiversity(List population) { int length = population[0].Length; double diversity = 0; for (int i = 0; i < length; i++) { var uniqueGenes = population.Select(chromo => chromo[i]).Distinct().Count(); diversity += uniqueGenes; } return diversity / length; } ``` Use this metric to monitor whether your population is converging too quickly. ### Strategy 2: Visualize Fitness Progress Logging and charting fitness values help you detect stagnation and the onset of convergence. ``` Console.WriteLine($"Gen {generation}: Best={best.Fitness}, Avg={averageFitness}"); ``` If the best fitness does not improve for many generations, consider increasing mutation or injecting diversity. ### Strategy 3: Adjust Mutation Rate Dynamically To avoid loss of variation, adapt the mutation rate based on progress. ``` if (noImprovementGenerations > 30) mutationRate *= 1.2; ``` This keeps the search space open when stuck. ### Strategy 4: Use Elitism Sparingly Elitism ensures the best solutions are preserved. But excessive elitism reduces variation and drives convergence too early. **Rule of thumb:** keep elite count between 1% and 5% of the population. ``` int eliteCount = (int)(populationSize * 0.02); ``` ### Strategy 5: Reevaluate Selection Pressure Tournament selection with large groups or roulette selection with poorly scaled fitness can bias heavily toward a few individuals. **Fixes:** - Reduce tournament size - Use rank-based selection - Normalize fitness scores ``` var ranked = population.OrderByDescending(p => p.Fitness).ToList(); ``` Rank-based selection reduces bias when fitness scores vary widely. ### Strategy 6: Inject Random Individuals To restore diversity, occasionally introduce random individuals into the population. ``` if (generation % 50 == 0) population.Add(GenerateRandomChromosome()); ``` This can help jump out of local optima. ### Strategy 7: Inspect the Fitness Function Ensure your fitness function: - Has meaningful gradients (smooth transitions between fitness levels) - Penalizes invalid solutions properly - Is not overly harsh or sparse Too many ties in fitness will make selection ineffective. **Example of a poor fitness function:** ``` return isValid ? 1 : 0; ``` **Better version:** ``` return isValid ? CalculateObjectiveScore() : 0.01; ``` ### Strategy 8: Step-Through Debugging For subtle logic bugs, step through: - Chromosome creation - Crossover and mutation logic - Fitness calculation Use `Debugger.Break()` or write test cases to validate each operator. ### Strategy 9: Enable Determinism for Testing Use a fixed seed for your `Random` instance: ``` Random rng = new Random(42); ``` This helps you replicate behavior and track changes when tuning. ### Final Advice Debugging a GA is not just about fixing code. It involves understanding evolutionary dynamics, tuning parameters, and ensuring the design reflects the nature of your problem. Track diversity, visualize progress, adapt your mutation, and inject variation when needed. Next, we’ll explore how to extend GAs with memory, co-evolution, and historical tracking. Evolution is powerful but needs your guidance to stay on course. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 31: Best Practices for Tuning Genetic Algorithm Parameters](https://www.woodruff.dev/day-31-best-practices-for-tuning-genetic-algorithm-parameters/) **Published:** September 12, 2025 **Author:** Chris Woodruff **Content:** Genetic Algorithms (GAs) are flexible and powerful tools for solving optimization problems. However, their effectiveness relies heavily on the correct tuning of parameters. Population size, mutation rate, crossover rate, selection pressure, and generation limits all affect convergence, solution quality, and performance. In today’s post, we will explore best practices for tuning these parameters to get the most out of your genetic algorithm in C#. ### Why Tuning Matters Poor parameter choices can lead to: - Premature convergence to suboptimal solutions - Slow evolution and wasted compute cycles - Loss of genetic diversity and stagnation Good tuning balances exploration and exploitation and adapts to the complexity and domain of your problem. ### Population Size The population size determines how many candidate solutions are evaluated per generation. A larger population increases diversity but also increases computation time per generation. **Guideline**: - Small problems (e.g. evolving short strings): 20 to 100 - Complex combinatorial problems (e.g. TSP): 100 to 1000 **Code Example**: ``` int populationSize = 200; ``` ### Mutation Rate Mutation introduces randomness and prevents the population from getting stuck in local optima. **Typical values**: 0.001 to 0.1 Higher mutation rates improve exploration but may disrupt convergence. For binary or boolean chromosomes, consider 1 / chromosome length. **Code Example**: ``` double mutationRate = 0.05; ``` ### Crossover Rate Crossover mixes genetic material from parents to create offspring. If set too low, evolution slows. If set too high, useful traits may get broken. **Typical values**: 0.6 to 0.9 **Code Example**: ``` double crossoverRate = 0.8; ``` ### Selection Pressure Selection pressure is controlled through the selection strategy. Tournament size, roulette scaling, or elitism level can bias toward fitter solutions. **Tips**: - Use tournament selection for controllable pressure. - Preserve 1 to 5 percent of elite individuals. **Example**: ``` var selection = new TournamentSelection(tournamentSize: 5); var elitism = new ElitismStrategy(eliteCount: 2); ``` ### Generation Limit and Termination Set a sensible generation limit or a convergence threshold to stop when the population stabilizes. **Guideline**: - Use `maxGenerations` in early development to debug quickly. - Add a convergence check: if fitness does not improve over N generations, terminate early. **Example**: ``` int maxGenerations = 1000; int stagnantGenerationsLimit = 100; ``` ### Fitness Scaling Scaling fitness can help when raw fitness values have skewed distributions. **Techniques**: - Rank-based fitness - Sigma scaling - Log scaling ### Adaptive Parameter Tuning Dynamic GAs adjust mutation and crossover rates during execution based on progress. Example approach: ``` if (generationsWithoutImprovement > 50) mutationRate *= 1.5; ``` This can boost exploration when evolution stalls. ### Experimental Tuning Strategy 1. Start with defaults: mutation = 0.05, crossover = 0.8, population = 100 2. Run GA with fixed seed to make runs comparable 3. Change one parameter at a time 4. Track best fitness and diversity metrics over generations 5. Use logging and visualization to detect stagnation ### Benchmarking Tools In .NET, you can use `BenchmarkDotNet` to measure how parameter changes affect performance: ``` [Benchmark] public void RunGA() => geneticAlgorithm.Run(); ``` ### Final Thoughts There is no one-size-fits-all parameter set. Optimal tuning requires domain knowledge, experimentation, and proper diagnostics. Build your GA framework to allow easy configuration and rerunability. With careful tuning, even a basic GA can become a robust problem solver for your C# applications. Next time we’ll focus on how to make your GAs smarter using problem-specific knowledge. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 30: Unit Testing Your Evolution: Making Genetic Algorithms Testable and Predictable](https://www.woodruff.dev/day-30-unit-testing-your-evolution-making-genetic-algorithms-testable-and-predictable/) **Published:** September 11, 2025 **Author:** Chris Woodruff **Content:** Genetic Algorithms are inherently stochastic. Mutation introduces randomness. Crossover combines genes in unpredictable ways. Selection strategies often rely on probabilities. While this is essential to their power, it presents a challenge when it comes to unit testing. How can you reliably test behavior when the outcome changes on every run? The answer lies in isolating deterministic logic and controlling randomness with test seams. Today’s post focuses on making your GA codebase testable using clean design principles and writing effective unit tests in C# with xUnit. ### Why Unit Test Genetic Algorithms? A typical GA involves: - Initialization of chromosomes - Evaluation of fitness - Parent selection - Crossover and mutation - Iterative execution across generations Not all of these need to be tested with assertions on evolutionary outcomes. Instead, we focus on: - Verifying mutation and crossover logic behave as expected - Ensuring fitness evaluation returns consistent values - Validating selection strategies pick high-fitness parents - Confirming GA loops honor configuration and convergence rules ### Strategy 1: Isolate and Abstract Randomness Use dependency injection to control random number generation. ``` public interface IRandomProvider { int Next(int minValue, int maxValue); double NextDouble(); } ``` A default implementation using `System.Random`: ``` public class DefaultRandomProvider : IRandomProvider { private readonly Random _random = new(); public int Next(int minValue, int maxValue) => _random.Next(minValue, maxValue); public double NextDouble() => _random.NextDouble(); } ``` In your GA components: ``` public class BitFlipMutation : IMutationStrategy { private readonly IRandomProvider _random; public BitFlipMutation(IRandomProvider random) { _random = random; } public void Mutate(IChromosome chromosome) { if (chromosome is BinaryChromosome bc) { int index = _random.Next(0, bc.Genes.Length); bc.Genes[index] = !bc.Genes[index]; bc.EvaluateFitness(); } } } ``` Now in tests you can mock the randomness. ### Strategy 2: Use Deterministic Chromosomes for Testing You can define a chromosome with a fixed outcome: ``` public class TestChromosome : IChromosome { public double Fitness { get; private set; } public int[] Genes { get; private set; } public TestChromosome(int[] genes) { Genes = genes; EvaluateFitness(); } public void EvaluateFitness() { Fitness = Genes.Sum(); // predictable } public IChromosome Crossover(IChromosome partner) { return Clone(); // no actual crossover for test } public void Mutate() { } // no-op for testing public IChromosome Clone() => new TestChromosome((int[])Genes.Clone()); } ``` ### Example: Testing a Selection Strategy Here’s an xUnit test that verifies `TournamentSelection` chooses the fittest chromosome: ``` [Fact] public void TournamentSelection_PrefersFitterChromosomes() { var population = new List { new TestChromosome(new[] { 1, 1, 1 }), // Fitness = 3 new TestChromosome(new[] { 1, 1, 0 }), // Fitness = 2 new TestChromosome(new[] { 1, 0, 0 }) // Fitness = 1 }; var selector = new TournamentSelection(3); var parents = selector.SelectParents(population).ToList(); Assert.All(parents, p => Assert.True(p.Fitness >= 2)); } ``` ### Example: Testing the Mutation Logic Using a fixed random provider: ``` public class FixedRandomProvider : IRandomProvider { private readonly int _fixedIndex; public FixedRandomProvider(int index) { _fixedIndex = index; } public int Next(int minValue, int maxValue) => _fixedIndex; public double NextDouble() => 0.0; } ``` And the test: ``` [Fact] public void Mutation_FlipsSpecificGene() { var chromosome = new BinaryChromosome(new[] { false, false, false }); var mutator = new BitFlipMutation(new FixedRandomProvider(1)); mutator.Mutate(chromosome); Assert.False(chromosome.Genes[0]); Assert.True(chromosome.Genes[1]); Assert.False(chromosome.Genes[2]); } ``` ### Testing the GA Loop You can test if the GA loop evolves a population to reach a known fitness threshold within a fixed number of generations: ``` [Fact] public void GA_ImprovesFitnessOverTime() { var ga = new GeneticAlgorithm( chromosomeFactory: () => new BinaryChromosome(RandomGeneArray(10)), selection: new TournamentSelection(), crossover: new OnePointCrossover(), mutation: new BitFlipMutation(new DefaultRandomProvider()), populationSize: 50, generations: 100); ga.Run(); Assert.True(ga.Best.Fitness >= 8); } ``` For full reliability, use a fixed seed or inject test-friendly randomness. ### Final Thoughts With the right abstractions and test seams, you can write meaningful, repeatable tests for your genetic algorithm components. You won’t test randomness itself, but you can ensure that your logic responds correctly to it. This gives you the confidence to scale up, refactor, or add features without fearing regression. Tomorrow we’ll look at injecting domain knowledge through heuristics and external data to guide evolution more intelligently. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 29: Defining Interfaces for Genetic Algorithms Components: Fitness, Selection, and Operators](https://www.woodruff.dev/day-29-defining-interfaces-for-genetic-algorithms-components-fitness-selection-and-operators/) **Published:** September 10, 2025 **Author:** Chris Woodruff **Content:** To build flexible and maintainable genetic algorithm solutions in C#, a modular architecture is critical. Yesterday, we focused on designing a pluggable GA framework. Today, we take a deeper dive into how to structure the interfaces that allow different GA strategies to be easily swapped, tested, and reused. By defining clear contracts for fitness evaluation, selection, crossover, and mutation, your GA becomes both extensible and future-proof. This post introduces and explains the interfaces that represent the core behavioral components of any GA engine. These abstractions allow you to treat the GA pipeline as interchangeable pieces while keeping your problem domain logic cleanly separated from evolutionary mechanics. ### Why Interfaces Matter Without well-defined interfaces, your GA quickly becomes rigid. For example, if your selection logic is hard-coded to tournament selection, you will need invasive changes to experiment with roulette selection. With interfaces, each strategy becomes just another implementation behind a common contract. This promotes testing, reusability, and fast iteration. Let’s define the essential contracts that form the foundation of a pluggable GA system. --- ### IChromosome: The Evolutionary Unit The `IChromosome` interface represents an individual solution. It encapsulates data (genes), fitness, and evolutionary behavior. ``` public interface IChromosome { double Fitness { get; } void EvaluateFitness(); IChromosome Crossover(IChromosome partner); void Mutate(); IChromosome Clone(); } ``` Key benefits: - Keeps fitness tightly coupled to solution state - Supports crossover and mutation as object behaviors - Enables copying solutions without mutation side effects A concrete implementation might look like this: ``` public class BinaryChromosome : IChromosome { private static readonly Random _random = new(); public bool[] Genes { get; private set; } public double Fitness { get; private set; } public BinaryChromosome(bool[] genes) { Genes = genes; EvaluateFitness(); } public void EvaluateFitness() { Fitness = Genes.Count(g => g); // simple count of 'true' genes } public IChromosome Crossover(IChromosome partner) { var other = (BinaryChromosome)partner; bool[] childGenes = new bool[Genes.Length]; for (int i = 0; i < Genes.Length; i++) { childGenes[i] = _random.NextDouble() < 0.5 ? Genes[i] : other.Genes[i]; } return new BinaryChromosome(childGenes); } public void Mutate() { int index = _random.Next(Genes.Length); Genes[index] = !Genes[index]; EvaluateFitness(); } public IChromosome Clone() { return new BinaryChromosome((bool[])Genes.Clone()); } } ``` --- ### ISelectionStrategy: Choosing Parents Selection controls which individuals get to reproduce. Different strategies yield different convergence behaviors. ``` public interface ISelectionStrategy { IEnumerable SelectParents(List population); } ``` You might implement it with tournament logic: ``` public class TournamentSelection : ISelectionStrategy { private readonly Random _random = new(); private readonly int _tournamentSize; public TournamentSelection(int tournamentSize = 3) { _tournamentSize = tournamentSize; } public IEnumerable SelectParents(List population) { return Enumerable.Range(0, 2).Select(_ => { var contestants = population.OrderBy(_ => _random.Next()) .Take(_tournamentSize); return contestants.OrderByDescending(c => c.Fitness).First(); }); } } ``` --- ### ICrossoverStrategy: Mixing Genes Crossover combines two parents into a child solution. ``` public interface ICrossoverStrategy { IChromosome Crossover(IChromosome parent1, IChromosome parent2); } ``` One-point crossover example: ``` public class OnePointCrossover : ICrossoverStrategy { private readonly Random _random = new(); public IChromosome Crossover(IChromosome parent1, IChromosome parent2) { var p1 = (BinaryChromosome)parent1; var p2 = (BinaryChromosome)parent2; int point = _random.Next(p1.Genes.Length); bool[] childGenes = new bool[p1.Genes.Length]; for (int i = 0; i < point; i++) childGenes[i] = p1.Genes[i]; for (int i = point; i < p1.Genes.Length; i++) childGenes[i] = p2.Genes[i]; return new BinaryChromosome(childGenes); } } ``` --- ### IMutationStrategy: Injecting Diversity Mutation introduces variability to help escape local optima. ``` public interface IMutationStrategy { void Mutate(IChromosome chromosome); } ``` Simple binary mutation: ``` public class BitFlipMutation : IMutationStrategy { public void Mutate(IChromosome chromosome) { chromosome.Mutate(); } } ``` The strategy delegates mutation to the chromosome’s logic, though advanced frameworks may separate gene-specific mutation into its own service. --- ### Composing It All With interfaces in place, your GA engine becomes a coordinator: ``` var ga = new GeneticAlgorithm( chromosomeFactory: () => new BinaryChromosome(RandomGeneArray()), selection: new TournamentSelection(), crossover: new OnePointCrossover(), mutation: new BitFlipMutation(), populationSize: 100, generations: 200); ga.Run(); ``` This approach decouples every behavior. You can test each strategy in isolation, swap them to compare results, or inject new operators without changing the engine. --- ### Final Thoughts Defining interfaces for genetic algorithm components unlocks long-term maintainability and experimentation. As you continue building more complex applications, multi-objective optimization, hybrid methods, or real-time logging, you will benefit from this modularity. Tomorrow, we will refactor and extend this architecture to support real-world features, such as tracking progress and injecting domain knowledge through heuristics. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 28: Building a Pluggable Genetic Algorithms Framework in C#](https://www.woodruff.dev/day-28-building-a-pluggable-genetic-algorithms-framework-in-c/) **Published:** September 9, 2025 **Author:** Chris Woodruff **Content:** As you reach the final week of our Genetic Algorithms series, it is time to shift from experimentation to engineering. Instead of writing one-off implementations tailored to specific problems, the focus now turns to creating a flexible and pluggable genetic algorithm (GA) framework. This architecture allows developers to reuse core evolutionary components across different problem domains. In this post, we will walk through the structure of a modular GA framework in C# that supports swappable implementations for selection, crossover, mutation, and fitness evaluation. By abstracting core logic into interfaces and strategy classes, we can build highly adaptable systems without rewriting boilerplate GA logic. ### Why Build a GA Framework? Most GA codebases begin as monolithic scripts with tightly coupled logic. But as you scale and tackle diverse problems like string evolution, TSP, or class scheduling, this approach becomes a liability. A framework helps to: - Reuse generic GA logic - Plug in different operators with minimal code changes - Separate concerns (selection vs. mutation vs. fitness) - Support testing and maintainability ### Core Design: Strategy Interfaces We begin by defining a set of interfaces that abstract each key component. ``` public interface IChromosome { double Fitness { get; } void EvaluateFitness(); IChromosome Crossover(IChromosome partner); void Mutate(); IChromosome Clone(); } ``` This represents an individual in the population. It is problem-specific, so users of the framework will implement their own chromosome logic. ``` public interface ISelectionStrategy { IEnumerable SelectParents(List population); } public interface IMutationStrategy { void Mutate(IChromosome chromosome); } public interface ICrossoverStrategy { IChromosome Crossover(IChromosome parent1, IChromosome parent2); } ``` Each interface defines a contract for how the GA should handle evolution. Now we can define a generic `GeneticAlgorithm` engine. ### The GA Engine ``` public class GeneticAlgorithm( Func chromosomeFactory, ISelectionStrategy selection, ICrossoverStrategy crossover, IMutationStrategy mutation, int populationSize, int generations) { private List _population = Enumerable.Range(0, populationSize) .Select(_ => chromosomeFactory()).ToList(); public void Run() { for (int gen = 0; gen < generations; gen++) { foreach (var individual in _population) individual.EvaluateFitness(); _population = _population.OrderByDescending(c => c.Fitness).ToList(); Console.WriteLine($"Gen {gen}: Best = {_population.First().Fitness}"); var newPopulation = new List(); while (newPopulation.Count < populationSize) { var parents = selection.SelectParents(_population).ToList(); var child = crossover.Crossover(parents[0], parents[1]); mutation.Mutate(child); newPopulation.Add(child); } _population = newPopulation; } } } ``` ### Implementing a Problem Let’s say you want to evolve the string “HELLO WORLD”. Your chromosome might look like this: ``` public class StringChromosome : IChromosome { private static readonly Random _random = new(); private const string Target = "HELLO WORLD"; private const string Charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "; private string Genes { get; private set; } public double Fitness { get; private set; } private StringChromosome(string genes) { Genes = genes; EvaluateFitness(); } public static StringChromosome CreateRandom() { var genes = new string(Enumerable.Range(0, Target.Length) .Select(_ => Charset[_random.Next(Charset.Length)]).ToArray()); return new StringChromosome(genes); } public void EvaluateFitness() { int score = 0; for (int i = 0; i < Target.Length; i++) if (Genes[i] == Target[i]) score++; Fitness = score; } public IChromosome Crossover(IChromosome partner) { var p2 = (StringChromosome)partner; char[] childGenes = new char[Genes.Length]; for (int i = 0; i < Genes.Length; i++) { childGenes[i] = _random.NextDouble() < 0.5 ? Genes[i] : p2.Genes[i]; } return new StringChromosome(new string(childGenes)); } public void Mutate() { char[] chars = Genes.ToCharArray(); int index = _random.Next(chars.Length); chars[index] = Charset[_random.Next(Charset.Length)]; Genes = new string(chars); EvaluateFitness(); } public IChromosome Clone() { return new StringChromosome(Genes); } } ``` ### Sample Strategies ``` public class TournamentSelection : ISelectionStrategy { private readonly Random _random = new(); public IEnumerable SelectParents(List population) { return Enumerable.Range(0, 2) .Select(_ => population .OrderBy(_ => _random.Next()) .Take(5) .OrderByDescending(c => c.Fitness) .First()); } } public class DefaultMutation : IMutationStrategy { public void Mutate(IChromosome chromosome) => chromosome.Mutate(); } public class DefaultCrossover : ICrossoverStrategy { public IChromosome Crossover(IChromosome p1, IChromosome p2) => p1.Crossover(p2); } ``` ### Running the Framework ``` var ga = new GeneticAlgorithm( chromosomeFactory: StringChromosome.CreateRandom, selection: new TournamentSelection(), crossover: new DefaultCrossover(), mutation: new DefaultMutation(), populationSize: 100, generations: 50); ga.Run(); ``` ### Final Thoughts By abstracting the components of a Genetic Algorithm into swappable interfaces, you now have a reusable, maintainable, and testable GA framework. You can create new problems by simply implementing a new `IChromosome`, and swap out operators without rewriting your loop. In future posts, we will extend this framework to support logging, NSGA-II, and hybrid techniques like memetic search. If you’re solving optimization problems across domains, a pluggable design like this accelerates experimentation and enhances production readiness. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 7: Putting It Together: Simulating Your First Genetic Algorthm Cycle in .NET](https://www.woodruff.dev/day-7-putting-it-together-simulating-your-first-ga-cycle-in-net/) **Published:** June 10, 2025 **Author:** Chris Woodruff **Excerpt:** By now, you’ve learned the foundational components of genetic algorithms: chromosomes, genes, fitness functions, mutation, crossover, and selection. Today, it’s time to bring those elements together and run your first complete GA cycle using C# and .NET. This post walks you through the structure of a single evolutionary loop—initialization, evaluation, selection, reproduction, and mutation—so you can simulate a working genetic algorithm and evolve real solutions. **Content:** By now, you’ve learned the foundational components of genetic algorithms: chromosomes, genes, fitness functions, mutation, crossover, and selection. Today, it’s time to bring those elements together and run your first complete GA cycle using C# and .NET. This post walks you through the structure of a single evolutionary loop, initialization, evaluation, selection, reproduction, and mutation, so you can simulate a working genetic algorithm and evolve real solutions. ## Recap: The Genetic Algorithm Workflow Each generation in a genetic algorithm follows this pattern: 1. **Initialize** a population of chromosomes 2. **Evaluate** their fitness 3. **Select** parents based on fitness 4. **Crossover** to produce offspring 5. **Mutate** offspring to introduce variation 6. **Replace** the old population with a new generation 7. Repeat for a fixed number of generations or until a solution is found ## Setting Up the Population Let’s start with a simple goal: evolve a population of strings to match the phrase `"HELLO WORLD"`. We’ll use a basic `Chromosome` class like the one we’ve developed in previous posts. ### Initializing the Population ``` const string target = "HELLO WORLD"; const int populationSize = 100; const int generations = 1000; const double mutationRate = 0.01; var population = new List(); for (int i = 0; i < populationSize; i++) { population.Add(new Chromosome(target.Length)); } ``` ## Evaluating Fitness Each chromosome receives a fitness score based on its similarity to the target. ``` foreach (var chromosome in population) { chromosome.FitnessScore = chromosome.GetFitness(target); } ``` Make sure `FitnessScore` is a public property or field in your `Chromosome` class. ## Selection and Reproduction We’ll use elitism and tournament selection to build the next generation. ``` List Evolve(List currentPopulation) { var nextGeneration = new List(); // Elitism: preserve the top 2 var elites = currentPopulation.OrderByDescending(c => c.FitnessScore).Take(2).ToList(); nextGeneration.AddRange(elites); while (nextGeneration.Count < currentPopulation.Count) { var parent1 = TournamentSelection(currentPopulation, 5); var parent2 = TournamentSelection(currentPopulation, 5); var child = parent1.Crossover(parent2); child.Mutate(mutationRate); nextGeneration.Add(child); } return nextGeneration; } ``` ## Running the Full Evolution Cycle Now we’ll loop over multiple generations and evolve toward the target. ``` for (int gen = 0; gen < generations; gen++) { foreach (var chromosome in population) { chromosome.FitnessScore = chromosome.GetFitness(target); } var best = population.OrderByDescending(c => c.FitnessScore).First(); Console.WriteLine($"Generation {gen}: {best.GetPhrase()} (Fitness: {best.FitnessScore})"); if (best.FitnessScore == target.Length) { Console.WriteLine("Solution found!"); break; } population = Evolve(population); } ``` ## Example Output ``` Generation 0: KELXO ZRLAQ (Fitness: 2) Generation 50: HELTO WORHD (Fitness: 10) Generation 104: HELLO WORLD (Fitness: 11) Solution found! ``` The algorithm gradually evolves closer to the target string as the population improves. ## Wrap-Up You’ve now built a full genetic algorithm loop in C#. It may be simple, but it models all the essential behaviors of evolution: variation, selection, and survival of the fittest. From this foundation, you can scale the algorithm to solve more advanced problems, such as route planning, resource scheduling, game AI, and more. Next week, we’ll explore **crossover techniques** in more detail and look at how different strategies influence genetic diversity. Your evolutionary engine is running. Now it’s time to refine how traits are passed along. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 19: Scheduling with DNA: Using Genetic Algorthms for Class and Work Timetables](https://www.woodruff.dev/day-19-scheduling-with-dna-using-gas-for-class-and-work-timetables/) **Published:** August 4, 2025 **Author:** Chris Woodruff **Excerpt:** Scheduling is a classic example of a constraint satisfaction problem that often becomes too complex for brute-force or greedy solutions. Whether you're designing class timetables for a university or shift schedules for employees, the number of constraints quickly increases, making traditional methods inefficient. Genetic Algorithms provide a flexible and powerful approach to finding feasible and optimized schedules by evolving solutions that balance multiple constraints over time. In a scheduling problem, the chromosome represents a potential timetable. Each gene can represent a time-slot assignment for a course, instructor, room, or shift. Unlike simple optimization problems, scheduling must respect both hard constraints (such as no double-booking of rooms or overlapping classes for a teacher) and soft constraints (such as preferring certain time blocks). **Content:** Scheduling is a classic example of a constraint satisfaction problem that often becomes too complex for brute-force or greedy solutions. Whether you’re designing class timetables for a university or shift schedules for employees, the number of constraints quickly increases, making traditional methods inefficient. Genetic Algorithms provide a flexible and powerful approach to finding feasible and optimized schedules by evolving solutions that balance multiple constraints over time. In a scheduling problem, the chromosome represents a potential timetable. Each gene can represent a time-slot assignment for a course, instructor, room, or shift. Unlike simple optimization problems, scheduling must respect both hard constraints (such as no double-booking of rooms or overlapping classes for a teacher) and soft constraints (such as preferring certain time blocks). Let’s begin by designing a basic chromosome for a simplified class scheduling problem. Assume we need to schedule a set of courses into time slots and rooms, making sure no room or instructor is double-booked. Define a data model for the basic entities: ``` public class Course { public string Id { get; set; } public string Instructor { get; set; } public int DurationSlots { get; set; } } public class TimeSlot { public int Day { get; set; } // 0 = Monday, 1 = Tuesday, ... public int Hour { get; set; } // 0 = 8am, 1 = 9am, ... } public class Room { public string Id { get; set; } public int Capacity { get; set; } } ``` Define the chromosome as a mapping of course assignments to room and time: ``` public class ScheduleGene { public string CourseId { get; set; } public string RoomId { get; set; } public TimeSlot Slot { get; set; } } public class ScheduleChromosome { public List Genes { get; set; } public double FitnessScore { get; set; } public ScheduleChromosome(List courses, List rooms, List timeSlots) { var rand = new Random(); Genes = courses.Select(course => new ScheduleGene { CourseId = course.Id, RoomId = rooms[rand.Next(rooms.Count)].Id, Slot = timeSlots[rand.Next(timeSlots.Count)] }).ToList(); } public ScheduleChromosome DeepCopy() { return new ScheduleChromosome { Genes = Genes.Select(g => new ScheduleGene { CourseId = g.CourseId, RoomId = g.RoomId, Slot = new TimeSlot { Day = g.Slot.Day, Hour = g.Slot.Hour } }).ToList() }; } private ScheduleChromosome() { } } ``` Next, implement a fitness function. Penalize the chromosome for violating constraints such as overlapping classes for instructors or rooms: ``` public void EvaluateFitness(List courses) { int violations = 0; var roomSchedule = new Dictionary(); var instructorSchedule = new Dictionary(); foreach (var gene in Genes) { var key = $"{gene.Slot.Day}-{gene.Slot.Hour}"; var course = courses.First(c => c.Id == gene.CourseId); var instructor = course.Instructor; // Check room conflict string roomKey = $"{gene.RoomId}:{key}"; if (!roomSchedule.TryAdd(roomKey, new HashSet { gene.CourseId })) violations++; // Check instructor conflict string instructorKey = $"{instructor}:{key}"; if (!instructorSchedule.TryAdd(instructorKey, new HashSet { gene.CourseId })) violations++; } FitnessScore = 1.0 / (1 + violations); } ``` Crossover can be implemented by taking half of the assignments from one parent and filling the rest from the other: ``` public static ScheduleChromosome Crossover(ScheduleChromosome parent1, ScheduleChromosome parent2) { var rand = new Random(); var child = parent1.DeepCopy(); for (int i = 0; i < child.Genes.Count; i++) { if (rand.NextDouble() < 0.5) { child.Genes[i].RoomId = parent2.Genes[i].RoomId; child.Genes[i].Slot = new TimeSlot { Day = parent2.Genes[i].Slot.Day, Hour = parent2.Genes[i].Slot.Hour }; } } return child; } ``` Mutation randomly reassigns a course to a new time slot or room: ``` public void Mutate(List rooms, List slots, double mutationRate) { var rand = new Random(); foreach (var gene in Genes) { if (rand.NextDouble() < mutationRate) { gene.RoomId = rooms[rand.Next(rooms.Count)].Id; gene.Slot = slots[rand.Next(slots.Count)]; } } } ``` Now run the genetic algorithm loop as usual: ``` List population = InitializePopulation(); for (int gen = 0; gen < maxGenerations; gen++) { foreach (var chromosome in population) chromosome.EvaluateFitness(courses); var elites = population.OrderByDescending(c => c.FitnessScore) .Take(eliteCount) .Select(e => e.DeepCopy()) .ToList(); var nextGen = new List(elites); while (nextGen.Count < populationSize) { var parent1 = TournamentSelect(population); var parent2 = TournamentSelect(population); var child = ScheduleChromosome.Crossover(parent1, parent2); child.Mutate(rooms, slots, mutationRate); nextGen.Add(child); } population = nextGen; Console.WriteLine($"Gen {gen}: Best Fitness = {population.Max(p => p.FitnessScore):F4}"); } ``` The scheduling problem demonstrates the real power of genetic algorithms. Constraints can be layered and diverse, yet the GA adapts through evolutionary pressure. Unlike handcrafted, rule-based schedulers, your solution can be easily adjusted to new requirements by tweaking the constraint logic or fitness scoring. As the complexity of scheduling scenarios increases, your GA will continue to scale. Add room capacities, student course selections, or instructor preferences. The genetic algorithm doesn’t require re-engineering from scratch. It simply evolves with your definition of fitness. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 26: Running Genetic Algorthms in the Cloud with Azure Batch or Functions](https://www.woodruff.dev/day-26-running-gas-in-the-cloud-with-azure-batch-or-functions/) **Published:** August 19, 2025 **Author:** Chris Woodruff **Excerpt:** As your genetic algorithm workloads grow in complexity, compute-intensive tasks like evaluating large populations or running many generations can exceed what a single machine can handle efficiently. To address this, cloud platforms such as Microsoft Azure offer scalable execution environments where GAs can be deployed and run in parallel. Azure Batch and Azure Functions are two effective approaches to execute genetic algorithms at scale, each suited to different execution patterns. **Content:** As your genetic algorithm workloads grow in complexity, compute-intensive tasks like evaluating large populations or running many generations can exceed what a single machine can handle efficiently. To address this, cloud platforms such as Microsoft Azure offer scalable execution environments where GAs can be deployed and run in parallel. Azure Batch and Azure Functions are two effective approaches to execute genetic algorithms at scale, each suited to different execution patterns. In this post, you will learn how to architect and implement GA workloads on both Azure Batch for long-running parallel tasks and Azure Functions for event-driven or micro-GA executions. ### Why Use Azure for Genetic Algorithms? - **Scalability**: Run many GA simulations in parallel without managing infrastructure. - **Elasticity**: Auto-scale based on demand, ideal for dynamic workloads. - **Cost Efficiency**: Pay per use with Functions or spot pricing in Batch. ### Option 1: Azure Batch for Population-Level Parallelism Azure Batch lets you schedule and run parallel tasks on a pool of virtual machines. You upload your GA logic as an executable or container and let Batch distribute the work. #### Use Case Use Azure Batch when: - You have long-running GA simulations - You want to evolve populations in parallel (e.g., distributed islands model) - You require fine control over compute resources #### Steps to Deploy GA with Azure Batch 1. **Package Your GA as a Console App** Your C# GA should be built as a .NET console application that accepts parameters such as seed, generation count, and output file path. ``` // Program.cs static void Main(string[] args) { int seed = int.Parse(args[0]); int generations = int.Parse(args[1]); string output = args[2]; var ga = new GeneticAlgorithm(seed); var result = ga.Run(generations); File.WriteAllText(output, $"Best fitness: {result.Fitness}"); } ``` 2. **Upload Your App and Inputs to Azure Storage** Use Azure CLI or SDK to upload your compiled app and any input data to a Blob Storage container. 3. **Define the Job and Tasks** Each task can represent a simulation run with different seeds or parameters. ``` { "id": "ga-job", "poolInfo": { "poolId": "ga-pool" }, "tasks": [ { "id": "task1", "commandLine": "dotnet GAProject.dll 42 1000 result1.txt" }, { "id": "task2", "commandLine": "dotnet GAProject.dll 99 1000 result2.txt" } ] } ``` 4. **Retrieve Results** Each task can output results to Azure Files or Blob Storage. Download or aggregate results locally after execution. ### Option 2: Azure Functions for Event-Driven GA Runs Azure Functions allow you to run small-scale GA workloads in a serverless, event-driven model. Ideal for micro GA executions like: - Real-time tuning of parameters - Evolving small populations on-demand - Triggering GA jobs from APIs, queues, or timers #### Use Case Use Azure Functions when: - You have short-running GAs - You want to run GAs in response to events (HTTP requests, timers, queues) - You need quick autoscaling without managing infrastructure #### Example: GA Triggered via HTTP Request ``` [FunctionName("RunGA")] public static async Task RunGA( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req) { int generations = int.Parse(req.Query["generations"]); int seed = int.Parse(req.Query["seed"]); var ga = new GeneticAlgorithm(seed); var result = ga.Run(generations); return new OkObjectResult(new { BestFitness = result.Fitness }); } ``` You can deploy this function using the Azure Functions CLI or Visual Studio and then trigger it via HTTP from a client or CI/CD workflow. ### Comparison: Batch vs Functions FeatureAzure BatchAzure FunctionsBest ForLong-running parallel tasksShort-lived event-driven tasksExecution Time LimitUp to 7 daysDefault 5 mins (can extend)ConcurrencyHigh with parallel VMsHigh with autoscaling instancesCost ModelPer VM timePer executionDeployment FormatConsole app or containerFunction App with HTTP/Timer### Conclusion Deploying genetic algorithms to the cloud unlocks scalability and cost-efficiency for compute-heavy optimization tasks. Azure Batch gives you full control for massive parallelism, while Azure Functions offers lightweight, reactive execution. Choose the model that fits your workload profile and evolve your applications at cloud scale. In the next post, we’ll look at how to log and visualize GA results from distributed runs using Azure Storage and dashboards. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 27: Logging and Monitoring Genetic Algorthms Progress Over Generations](https://www.woodruff.dev/day-27-logging-and-monitoring-genetic-progress-over-generations/) **Published:** August 21, 2025 **Author:** Chris Woodruff **Excerpt:** As your genetic algorithms become more sophisticated, it's no longer enough to simply observe the final output. Monitoring the evolutionary process in real time provides critical insight into convergence behavior, mutation impacts, and solution quality. Logging and monitoring allow you to diagnose performance bottlenecks, identify premature convergence, and validate the impact of parameter changes. **Content:** As your genetic algorithms become more sophisticated, it’s no longer enough to simply observe the final output. Monitoring the evolutionary process in real time provides critical insight into convergence behavior, mutation impacts, and solution quality. Logging and monitoring allow you to diagnose performance bottlenecks, identify premature convergence, and validate the impact of parameter changes. In this post, we will walk through practical strategies to instrument a C# genetic algorithm with logging and metrics to track fitness trends, generation performance, and genetic diversity. You will also see how to export this data for visualization or analysis. ### Why Log the Evolutionary Process? Logging the internal behavior of a GA allows you to: - Identify when and why fitness plateaus occur - Validate the effectiveness of crossover, mutation, and selection strategies - Tune parameters like population size or mutation rate based on real data - Provide transparency for reproducibility and auditing in research or production ### Core Metrics to Track The following metrics provide a clear picture of each generation’s state: - **Best fitness**: The highest score in the population - **Average fitness**: Population-wide performance - **Diversity**: How different individuals are (measured using Hamming distance or variance) - **Elapsed time**: Performance monitoring per generation ### Adding Logging to Your GA Loop Assume a basic GA loop that evolves a population of `Chromosome` objects. We will augment this loop with logging. ``` public static void RunGA(int populationSize, int generations) { var population = InitializePopulation(populationSize); var stopwatch = new Stopwatch(); using var writer = new StreamWriter("evolution_log.csv"); writer.WriteLine("Generation,BestFitness,AverageFitness,Diversity,ElapsedMs"); for (int generation = 0; generation < generations; generation++) { stopwatch.Restart(); foreach (var individual in population) individual.Evaluate(); var best = population.Max(c => c.Fitness); var avg = population.Average(c => c.Fitness); var diversity = CalculateDiversity(population); stopwatch.Stop(); writer.WriteLine($"{generation},{best},{avg},{diversity},{stopwatch.ElapsedMilliseconds}"); population = Evolve(population); } } ``` ### Example: Diversity Metric (Hamming Distance) Tracking genetic diversity can show whether your population is stuck in a local optimum. ``` public static double CalculateDiversity(List population) { int totalDistance = 0; int comparisons = 0; for (int i = 0; i < population.Count; i++) { for (int j = i + 1; j < population.Count; j++) { totalDistance += HammingDistance(population[i].Genes, population[j].Genes); comparisons++; } } return comparisons > 0 ? (double)totalDistance / comparisons : 0; } public static int HammingDistance(int[] a, int[] b) { return a.Zip(b, (x, y) => x == y ? 0 : 1).Sum(); } ``` ### Using Real-Time Logging For more advanced scenarios, consider using `Serilog`, `Microsoft.Extensions.Logging`, or outputting metrics to a database or Azure Application Insights. Example using `Serilog`: ``` Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File("ga.log") .CreateLogger(); Log.Information("Generation {Generation}: Best={Best}, Avg={Avg}, Diversity={Diversity}", generation, best, avg, diversity); ``` ### Conclusion By integrating logging and monitoring into your GA, you gain powerful feedback loops for optimization, debugging, and reporting. These insights help drive better decisions about your algorithm’s configuration and structure. Whether for scientific analysis or production optimization, observability is an essential part of building effective genetic systems. In the next post, we’ll explore how to archive and compare GA results over time to support repeatability and long-term tuning strategies. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 25: Scaling Up: Parallelizing Genetic Algorithms Loops in .NET with Parallel.ForEach](https://www.woodruff.dev/day-25-scaling-up-parallelizing-genetic-algorithms-loops-in-net-with-parallel-foreach/) **Published:** August 18, 2025 **Author:** Chris Woodruff **Excerpt:** As problem complexity grows, so does the cost of evaluating and evolving populations in genetic algorithms. When each individual's fitness computation becomes expensive or the population size increases substantially, runtime performance can become a serious bottleneck. Fortunately, .NET makes it easy to scale genetic algorithms with minimal code changes by using Parallel.ForEach. **Content:** As problem complexity grows, so does the cost of evaluating and evolving populations in genetic algorithms. When each individual’s fitness computation becomes expensive or the population size increases substantially, runtime performance can become a serious bottleneck. Fortunately, .NET makes it easy to scale genetic algorithms with minimal code changes by using `Parallel.ForEach`. This post explores how to parallelize the evaluation and evolution stages of a genetic algorithm using the `System.Threading.Tasks.Parallel` library to harness multicore CPUs and accelerate GA performance. ### Why Parallelism Matters in Genetic Algorithms In most GA implementations, each generation includes: - Evaluating the fitness of each individual - Selecting individuals for crossover - Mutating offspring - Replacing the population These operations are usually independent for each individual, especially fitness evaluation, which makes them ideal for data parallelism. By using `Parallel.ForEach`, we can speed up these operations with simple thread-safe logic. ### Parallelizing Fitness Evaluation Let’s assume a standard `Chromosome` class with an `Evaluate()` method. ``` public class Chromosome { public int[] Genes { get; set; } public double Fitness { get; set; } public void Evaluate() { // Simulate expensive fitness computation Fitness = Genes.Sum(); // Replace with real logic } } ``` In a traditional loop, we would evaluate fitness like this: ``` foreach (var individual in population) { individual.Evaluate(); } ``` With `Parallel.ForEach`, this becomes: ``` Parallel.ForEach(population, individual => { individual.Evaluate(); }); ``` If `Evaluate()` It is thread-safe and doesn’t depend on external state; this simple change can dramatically reduce evaluation time on multi-core machines. ### Parallelizing Mutation and Crossover Mutation and crossover can also be parallelized. You must ensure each operation uses a thread-safe random number generator to avoid contention and bias. Here’s an example of safe mutation using `ThreadLocal`: ``` var localRand = new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); Parallel.ForEach(population, individual => { Mutate(individual, mutationRate, localRand.Value); }); ``` Where `Mutate()` is: ``` public static void Mutate(Chromosome c, double rate, Random rand) { for (int i = 0; i < c.Genes.Length; i++) { if (rand.NextDouble() < rate) { c.Genes[i] = 1 - c.Genes[i]; } } } ``` This ensures each thread has an isolated and seeded instance of `Random`, avoiding race conditions or poor randomness due to shared state. ### Parallelizing Next Generation Creation We can even parallelize the generation of offspring. Here’s a simplified example using a concurrent collection: ``` ConcurrentBag nextGen = new(); Parallel.For(0, populationSize, i => { var parent1 = TournamentSelection(population, localRand.Value); var parent2 = TournamentSelection(population, localRand.Value); var child = Crossover(parent1, parent2, localRand.Value); Mutate(child, mutationRate, localRand.Value); child.Evaluate(); nextGen.Add(child); }); ``` Once complete, convert `nextGen` to a list and continue with the evolutionary loop: ``` population = nextGen.ToList(); ``` This approach is handy when generating hundreds or thousands of new individuals per generation. ### Best Practices - Use `ThreadLocal` or `Random.Shared` for safe and performant randomness in parallel code - Avoid shared mutable state inside loops - Profile your fitness evaluation to verify that it’s the actual bottleneck - Be aware of thread contention when logging or updating UI in parallel regions ### Conclusion By leveraging `Parallel.ForEach` and related constructs, you can make your genetic algorithm implementation significantly faster and more scalable. This is especially important for large-scale optimization tasks such as image generation, pathfinding, or feature selection, where fitness evaluation dominates runtime. In the next post, we’ll explore how to use .NET tasks and async patterns to distribute GA workloads across machines and even integrate them with distributed job queues. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 24: Combining Genetic Algorithms with Hill Climbing: The Hybrid Memetic Approach](https://www.woodruff.dev/day-24-combining-genetic-algorithms-with-hill-climbing-the-hybrid-memetic-approach/) **Published:** August 13, 2025 **Author:** Chris Woodruff **Excerpt:** Traditional genetic algorithms (GAs) excel at global exploration across large search spaces. However, they can struggle to fine-tune solutions with high precision due to their stochastic nature. On the other hand, local search techniques like hill climbing are good at refining individual solutions but easily get trapped in local optima. A memetic algorithm combines both approaches, using GAs for exploration and local search for exploitation. This hybrid strategy has proven effective in complex optimization problems where both breadth and depth of search are required. **Content:** Traditional genetic algorithms (GAs) excel at global exploration across large search spaces. However, they can struggle to fine-tune solutions with high precision due to their stochastic nature. On the other hand, local search techniques like hill climbing are good at refining individual solutions but easily get trapped in local optima. A memetic algorithm combines both approaches, using GAs for exploration and local search for exploitation. This hybrid strategy has proven effective in complex optimization problems where both breadth and depth of search are required. In this post, you will learn how to implement a simple memetic algorithm in C# by augmenting a GA with a hill-climbing step applied to each generation’s best individuals. ### Why Hybridize? GAs alone might find “good enough” regions of the solution space, but can leave solutions partially optimized. By adding hill climbing: - You guide elite individuals to higher precision - You improve convergence speed - You reduce the number of generations needed for refinement This approach is particularly valuable in scheduling, layout optimization, and parameter tuning problems. ### Chromosome Design Let’s define a chromosome as we did in earlier posts. We’ll use a simple binary representation for clarity. ``` public class Chromosome { public int[] Genes { get; set; } public double Fitness { get; set; } public static Chromosome Random(int length, Random rand) { return new Chromosome { Genes = Enumerable.Range(0, length).Select(_ => rand.Next(2)).ToArray() }; } public void Evaluate() { Fitness = Genes.Sum(); // Simple objective: maximize 1s } public Chromosome Clone() { return new Chromosome { Genes = (int[])Genes.Clone(), Fitness = Fitness }; } } ``` ### Hill Climbing Implementation We now define a simple local search method that flips each gene and keeps the change if it improves fitness. ``` public static Chromosome HillClimb(Chromosome chromosome) { var current = chromosome.Clone(); current.Evaluate(); for (int i = 0; i < current.Genes.Length; i++) { var neighbor = current.Clone(); neighbor.Genes[i] = 1 - neighbor.Genes[i]; // Flip the gene neighbor.Evaluate(); if (neighbor.Fitness > current.Fitness) { current = neighbor; } } return current; } ``` ### Incorporating Hill Climbing in the GA Cycle The memetic algorithm applies hill climbing to the best individuals in each generation. ``` public static void RunMemeticGA(int geneLength, int populationSize, int generations, double mutationRate) { var rand = new Random(); var population = Enumerable.Range(0, populationSize) .Select(_ => Chromosome.Random(geneLength, rand)) .ToList(); for (int gen = 0; gen < generations; gen++) { foreach (var individual in population) individual.Evaluate(); population = population.OrderByDescending(c => c.Fitness).ToList(); // Apply hill climbing to top N individuals int eliteCount = Math.Min(5, population.Count); for (int i = 0; i < eliteCount; i++) population[i] = HillClimb(population[i]); var nextGen = new List { population[0] }; // Elitism while (nextGen.Count < populationSize) { var parent1 = Tournament(population, rand); var parent2 = Tournament(population, rand); var child = Crossover(parent1, parent2, rand); Mutate(child, mutationRate, rand); child.Evaluate(); nextGen.Add(child); } population = nextGen; Console.WriteLine($"Generation {gen + 1}: Best Fitness = {population[0].Fitness}"); } } ``` ### Helper Methods ``` public static Chromosome Tournament(List pop, Random rand, int size = 3) { return pop.OrderBy(_ => rand.Next()) .Take(size) .OrderByDescending(c => c.Fitness) .First(); } public static Chromosome Crossover(Chromosome a, Chromosome b, Random rand) { var genes = new int[a.Genes.Length]; for (int i = 0; i < genes.Length; i++) genes[i] = rand.NextDouble() < 0.5 ? a.Genes[i] : b.Genes[i]; return new Chromosome { Genes = genes }; } public static void Mutate(Chromosome c, double rate, Random rand) { for (int i = 0; i < c.Genes.Length; i++) { if (rand.NextDouble() < rate) c.Genes[i] = 1 - c.Genes[i]; } } ``` ### Conclusion The memetic algorithm is a powerful hybrid strategy that brings together the global search strength of genetic algorithms with the precision of local search. In problems where refinement is just as important as exploration, this approach offers a clear performance boost. In the next post, we will examine how to scale GA execution using multithreading and parallel execution strategies in .NET. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 23: Introduction to Non-dominated Sorting Genetic Algorithm II (NSGA-II) in C#](https://www.woodruff.dev/day-23-introduction-to-non-dominated-sorting-genetic-algorithm-ii-nsga-ii-in-c/) **Published:** August 12, 2025 **Author:** Chris Woodruff **Excerpt:** As we extend our use of genetic algorithms (GAs) beyond single-objective problems, we enter the realm of multi-objective optimization, where trade-offs must be made between competing goals. The Non-dominated Sorting Genetic Algorithm II (NSGA-II) is one of the most widely used algorithms for solving such problems. Its design maintains a diverse population of high-quality solutions that form what is known as a Pareto front. **Content:** As we extend our use of genetic algorithms (GAs) beyond single-objective problems, we enter the realm of *multi-objective optimization*, where trade-offs must be made between competing goals. The Non-dominated Sorting Genetic Algorithm II (NSGA-II) is one of the most widely used algorithms for solving such problems. Its design maintains a diverse population of high-quality solutions that form what is known as a *Pareto front*. In this post, we will explore the core ideas of NSGA-II and walk through a simplified implementation in C# that demonstrates non-dominated sorting and diversity preservation using crowding distance. ### Why NSGA-II? Traditional GAs work well when you have a single fitness function. But when multiple objectives must be optimized simultaneously, combining them into a single scalar value often leads to suboptimal solutions. NSGA-II addresses this by: - Sorting individuals based on Pareto dominance - Promoting diversity using a crowding distance metric - Preserving elite solutions without duplication or bias The result is a collection of solutions that approximate the optimal trade-offs across objectives. ### Key Components of NSGA-II 1. **Non-dominated Sorting**: Individuals are ranked based on dominance. The first front contains solutions that are not dominated by any others. The second front contains individuals dominated only by those in the first front, and so on. 2. **Crowding Distance**: Within each front, individuals are scored based on how far they are from others in objective space. This ensures that the final population maintains diversity. 3. **Selection**: When forming the next generation, individuals are selected based on front rank and crowding distance. ### Chromosome Structure We define a multi-objective chromosome with two objectives: ``` public class Chromosome { public int[] Genes { get; set; } public double Objective1 { get; set; } public double Objective2 { get; set; } public int Rank { get; set; } public double CrowdingDistance { get; set; } public void Evaluate() { Objective1 = Genes.Sum(); // Dummy objective Objective2 = Genes.Length - Objective1; // Second objective as complement } } ``` ### Non-dominated Sorting We group individuals into fronts using Pareto dominance. ``` public static List NonDominatedSort(List population) { var fronts = new List(); var dominationCounts = new Dictionary(); var dominated = new Dictionary(); var front = new List(); foreach (var p in population) { dominated[p] = new List(); dominationCounts[p] = 0; foreach (var q in population) { if (Dominates(p, q)) dominated[p].Add(q); else if (Dominates(q, p)) dominationCounts[p]++; } if (dominationCounts[p] == 0) { p.Rank = 1; front.Add(p); } } fronts.Add(front); int rank = 1; while (fronts[rank - 1].Count > 0) { var nextFront = new List(); foreach (var p in fronts[rank - 1]) { foreach (var q in dominated[p]) { dominationCounts[q]--; if (dominationCounts[q] == 0) { q.Rank = rank + 1; nextFront.Add(q); } } } fronts.Add(nextFront); rank++; } return fronts; } public static bool Dominates(Chromosome a, Chromosome b) { bool betterInAny = false; if (a.Objective1 < b.Objective1) betterInAny = true; else if (a.Objective1 > b.Objective1) return false; if (a.Objective2 < b.Objective2) betterInAny = true; else if (a.Objective2 > b.Objective2) return false; return betterInAny; } ``` ### Crowding Distance Calculation After sorting, we calculate the crowding distance within each front: ``` public static void AssignCrowdingDistance(List front) { int count = front.Count; if (count == 0) return; foreach (var c in front) c.CrowdingDistance = 0; foreach (var objectiveSelector in new Func[] { c => c.Objective1, c => c.Objective2 }) { var sorted = front.OrderBy(objectiveSelector).ToList(); sorted[0].CrowdingDistance = double.PositiveInfinity; sorted[^1].CrowdingDistance = double.PositiveInfinity; double min = objectiveSelector(sorted[0]); double max = objectiveSelector(sorted[^1]); double range = max - min; if (range == 0) continue; for (int i = 1; i < count - 1; i++) { double prev = objectiveSelector(sorted[i - 1]); double next = objectiveSelector(sorted[i + 1]); sorted[i].CrowdingDistance += (next - prev) / range; } } } ``` ### Selection for the Next Generation Individuals are selected from fronts, starting from the best (lowest rank). If a front overflows the population limit, we use crowding distance to fill the remaining slots. ``` public static List SelectNextGeneration(List fronts, int populationSize) { var newPopulation = new List(); foreach (var front in fronts) { AssignCrowdingDistance(front); if (newPopulation.Count + front.Count c.CrowdingDistance); newPopulation.AddRange(sorted.Take(populationSize - newPopulation.Count)); break; } } return newPopulation; } ``` ### Conclusion NSGA-II brings structure and power to multi-objective genetic algorithms by combining dominance-based ranking and diversity preservation. Implementing NSGA-II in C# enables developers to tackle real-world problems with conflicting goals and identify a set of trade-off solutions that would be missed by single-objective approaches. In the next post, we will look at scaling these algorithms across threads and machines to make them production-ready. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 22: Multi-Objective Optimization: When One Fitness Function Isn't Enough](https://www.woodruff.dev/day-22-multi-objective-optimization-when-one-fitness-function-isnt-enough/) **Published:** August 11, 2025 **Author:** Chris Woodruff **Excerpt:** In many real-world problems, a single fitness function is insufficient to capture the complexity of the solution space. Applications in engineering, logistics, finance, and machine learning often involve trade-offs among competing objectives. For example, minimizing cost while maximizing performance, or reducing power consumption without sacrificing accuracy. In these cases, genetic algorithms (GAs) can be extended to support multi-objective optimization. **Content:** In many real-world problems, a single fitness function is insufficient to capture the complexity of the solution space. Applications in engineering, logistics, finance, and machine learning often involve trade-offs among competing objectives. For example, minimizing cost while maximizing performance, or reducing power consumption without sacrificing accuracy. In these cases, genetic algorithms (GAs) can be extended to support multi-objective optimization. This post introduces the concept of multi-objective GAs, discusses how to represent solutions, and walks through how to score candidates using multiple criteria using a Pareto-based approach. ### Why Multiple Objectives? Let’s consider a scheduling scenario. You might want to: - Minimize the number of schedule conflicts - Maximize the number of preferred assignments met - Ensure fairness in the distribution of work Each of these is a valid and important goal. But how do we evolve solutions that balance all of them? Multi-objective optimization doesn’t attempt to collapse all goals into a single fitness value. Instead, it evaluates trade-offs across objectives and maintains a *Pareto front* of solutions that are non-dominated, meaning no other solution is better in all objectives. ### Modeling Objectives in Code Let’s define a basic structure for multi-objective chromosomes in C#. We’ll assume two objectives for simplicity. ``` public class MultiObjectiveChromosome { public int[] Genes { get; set; } public double Objective1 { get; set; } public double Objective2 { get; set; } public static MultiObjectiveChromosome Random(int length, Random rand) { return new MultiObjectiveChromosome { Genes = Enumerable.Range(0, length).Select(_ => rand.Next(0, 2)).ToArray() }; } public void Evaluate() { Objective1 = CalculateConflicts(); Objective2 = CalculateFairness(); } private double CalculateConflicts() { return Genes.Count(g => g == 1); // dummy example } private double CalculateFairness() { return 1.0 / (1 + Genes.Distinct().Count()); // dummy example } } ``` Each chromosome now has multiple objectives rather than a single fitness value. Our next step is to determine how we compare individuals. ### Pareto Dominance Pareto dominance is the core concept behind comparing individuals in multi-objective optimization. A chromosome A dominates chromosome B if: - A is no worse in all objectives, and - A is better in at least one objective Here’s how to implement a dominance comparison: ``` public static bool Dominates(MultiObjectiveChromosome a, MultiObjectiveChromosome b) { bool betterInAny = false; if (a.Objective1 < b.Objective1) betterInAny = true; else if (a.Objective1 > b.Objective1) return false; if (a.Objective2 < b.Objective2) betterInAny = true; else if (a.Objective2 > b.Objective2) return false; return betterInAny; } ``` With this rule, we can build the Pareto front, the set of individuals not dominated by any other in the population. ### Evolving a Pareto Front A basic version of NSGA-II (Non-dominated Sorting Genetic Algorithm II) would: 1. Sort individuals by rank (Pareto fronts) 2. Use crowding distance to maintain diversity 3. Select individuals based on their rank and crowding distance For simplicity, we’ll select non-dominated individuals as elite survivors. ``` public static List GetParetoFront(List population) { var front = new List(); foreach (var candidate in population) { bool dominated = population.Any(other => other != candidate && Dominates(other, candidate)); if (!dominated) front.Add(candidate); } return front; } ``` This method ensures the selected chromosomes are those that represent the best trade-offs between the objectives. ### Conclusion Single-objective optimization simplifies problem solving but can miss the nuances of real-world decision making. Multi-objective genetic algorithms provide a powerful tool to navigate complex trade-offs, guiding you toward a diverse set of optimal solutions. In the next post, we will explore how to scale your GA to larger populations and longer evolution cycles, balancing performance and resource constraints. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 15: Fitness by Design: How to Shape the Problem to Match Evolution](https://www.woodruff.dev/day-15-fitness-by-design-how-to-shape-the-problem-to-match-evolution/) **Published:** July 28, 2025 **Author:** Chris Woodruff **Excerpt:** In genetic algorithms, the fitness function is not just a scoring system—it is the definition of success. Your entire evolutionary process hinges on how well the fitness function communicates what "better" means in the context of your problem. If the fitness function rewards the right behaviors, your algorithm will evolve meaningful solutions. If not, you may end up optimizing toward the wrong objective or stuck in a plateau of mediocrity. This post focuses on how to design fitness functions that align with your goals, reflect nuanced problem definitions, and promote useful evolution. Whether you are evolving strings, optimizing numbers, or solving real-world configurations, the fitness function is where the problem and solution space meet. **Content:** In genetic algorithms, the fitness function is not just a scoring system. It is the definition of success. Your entire evolutionary process hinges on how well the fitness function communicates what “better” means in the context of your problem. If the fitness function rewards the right behaviors, your algorithm will evolve meaningful solutions. If not, you may end up optimizing toward the wrong objective or stuck in a plateau of mediocrity. This post focuses on how to design fitness functions that align with your goals, reflect nuanced problem definitions, and promote useful evolution. Whether you are evolving strings, optimizing numbers, or solving real-world configurations, the fitness function is where the problem and solution space meet. Let’s revisit a simple example: evolving a string to match “HELLO WORLD”. The most direct fitness function counts the number of characters that match in the correct position. ``` public int GetFitness(string target) { return Genes.Zip(target, (g, t) => g == t ? 1 : 0).Sum(); } ``` While effective, this scoring strategy assumes exact positional matching is the only thing that matters. In more complex problems, this kind of binary scoring creates a flat landscape where many near misses get the same score, offering little direction for improvement. To improve feedback, consider introducing a weighted scoring model. If early characters are more important, or if the structure of the output matters, you can scale the scoring. ``` public int GetFitnessWeighted(string target) { int score = 0; for (int i = 0; i < Genes.Length; i++) { if (Genes[i] == target[i]) { score += (target.Length - i); // earlier matches are more valuable } } return score; } ``` Another technique is to reward proximity rather than exact matches. This softens the fitness landscape and provides gradients that help guide the algorithm toward better solutions. ``` public int GetFitnessSimilarity(string target) { return Genes.Zip(target, (g, t) => 255 - Math.Abs((decimal)(g - t))).Sum(); } ``` This version scores based on ASCII distance. Even if a character is incorrect, it can still contribute a partial score. This makes it easier for the algorithm to detect improvement when characters are close to correct. Sometimes the solution needs to meet multiple objectives. For instance, you may want a solution that is both accurate and compact. This can be achieved by combining multiple factors into a composite score. ``` public double GetCompositeFitness(string target) { int accuracy = GetFitness(target); int diversityPenalty = Genes.Distinct().Count(); // lower diversity = better in this context return accuracy - 0.1 * diversityPenalty; } ``` This composite score favors accuracy while discouraging excessive variation. It is essential to balance the weights carefully so that one metric does not entirely dominate the other. For structured outputs, invalid gene sequences may need to be penalized. You can modify the fitness score according to the constraints. ``` public int GetFitnessWithPenalty(string target) { int baseScore = GetFitness(target); bool containsInvalid = Genes.Any(c => !target.Contains(c)); return containsInvalid ? baseScore - 5 : baseScore; } ``` This approach introduces constraint handling into the fitness logic, steering the population away from infeasible regions of the solution space. Effective fitness design is iterative. Monitor the evolution process and watch how solutions change. If evolution is slow or erratic, analyze whether the fitness function effectively distinguishes between better and worse solutions. Small changes in scoring often produce significant improvements in convergence. Print fitness trends to get insight into algorithm performance. ``` var average = population.Average(c => c.FitnessScore); var best = population.Max(c => c.FitnessScore); Console.WriteLine($"Generation {generation} - Avg: {average}, Best: {best}"); ``` Seeing the delta between average and best fitness can help identify stagnation or dominance in the population. If average fitness is flat while best fitness improves, elitism may be driving most gains. If both are flat, the fitness function may not provide enough gradient. Remember that genetic algorithms do not understand your intent. They only know how to maximize the function you give them. If you want intelligent evolution, the fitness function must clearly, measurably, and reward incremental progress. As you build more advanced applications of GAs, investing time in thoughtful fitness design will yield better solutions, faster convergence, and a more adaptive algorithm. Fitness by design is not optional—it is essential. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 16: Solving the Traveling Salesperson Problem with Genetic Algorithms Permutation Chromosomes](https://www.woodruff.dev/day-16-solving-the-traveling-salesperson-problem-with-genetic-algorithms-permutation-chromosomes/) **Published:** July 29, 2025 **Author:** Chris Woodruff **Excerpt:** The Traveling Salesperson Problem, also known as TSP, is one of the most extensively studied combinatorial optimization problems in computer science. Given a set of cities and the distances between them, the objective is to find the shortest possible route that visits each city exactly once and returns to the starting point. This problem is computationally difficult to solve using brute-force methods because the number of possible routes grows factorially with the number of cities. **Content:** The Traveling Salesperson Problem, also known as TSP, is one of the most extensively studied combinatorial optimization problems in computer science. Given a set of cities and the distances between them, the objective is to find the shortest possible route that visits each city exactly once and returns to the starting point. This problem is computationally difficult to solve using brute-force methods because the number of possible routes grows factorially with the number of cities. Genetic algorithms provide a compelling approach to solving the TSP efficiently by utilizing permutation-based chromosomes to represent city visit sequences. Unlike bitstrings or character arrays, permutation chromosomes maintain the unique ordering required by TSP constraints. In this post, you’ll learn how to model TSP chromosomes in C#, implement crossover and mutation for permutations, and apply a GA to find short routes in a graph of cities. A permutation chromosome represents a tour as an ordered list of city indices. For example, a chromosome with values \[0, 3, 1, 2\] represents a route where the salesperson starts at city 0, travels to city 3, then to city 1, and finally to city 2 before returning to city 0. To begin, define the chromosome: ``` public class PermutationChromosome { public int[] Genes { get; private set; } public double FitnessScore { get; set; } private static readonly Random Random = new(); public PermutationChromosome(int cityCount) { Genes = Enumerable.Range(0, cityCount) .OrderBy(_ => Random.Next()) .ToArray(); } public PermutationChromosome(int[] genes) { Genes = genes.ToArray(); } public double CalculateDistance(double[,] distanceMatrix) { double total = 0; for (int i = 0; i < Genes.Length - 1; i++) { total += distanceMatrix[Genes[i], Genes[i + 1]]; } total += distanceMatrix[Genes[^1], Genes[0]]; // return to start return total; } public void Mutate(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.NextDouble() < mutationRate) { int j = Random.Next(Genes.Length); (Genes[i], Genes[j]) = (Genes[j], Genes[i]); } } } public PermutationChromosome OrderCrossover(PermutationChromosome other) { int length = Genes.Length; int start = Random.Next(length); int end = Random.Next(start, length); var childGenes = new int[length]; Array.Fill(childGenes, -1); for (int i = start; i < end; i++) { childGenes[i] = Genes[i]; } int otherIndex = 0; for (int i = 0; i < length; i++) { if (childGenes[i] == -1) { while (childGenes.Contains(other.Genes[otherIndex])) { otherIndex++; } childGenes[i] = other.Genes[otherIndex]; } } return new PermutationChromosome(childGenes); } public override string ToString() => string.Join(" -> ", Genes); } ``` In this model, the fitness function is based on route length. Lower distances are better, so we often use the inverse distance as the fitness score: ``` public void EvaluateFitness(double[,] distanceMatrix) { double distance = CalculateDistance(distanceMatrix); FitnessScore = 1.0 / distance; } ``` For the GA loop, use a typical configuration: ``` const int cityCount = 10; const int populationSize = 100; const int generations = 500; const double mutationRate = 0.02; const int eliteCount = 2; double[,] distanceMatrix = GenerateRandomDistances(cityCount); var population = Enumerable.Range(0, populationSize) .Select(_ => new PermutationChromosome(cityCount)) .ToList(); for (int gen = 0; gen < generations; gen++) { foreach (var c in population) c.EvaluateFitness(distanceMatrix); var elites = population.OrderByDescending(c => c.FitnessScore) .Take(eliteCount) .Select(e => new PermutationChromosome(e.Genes)) .ToList(); var nextGen = new List(elites); while (nextGen.Count < populationSize) { var parent1 = TournamentSelect(population); var parent2 = TournamentSelect(population); var child = parent1.OrderCrossover(parent2); child.Mutate(mutationRate); nextGen.Add(child); } population = nextGen; var best = population.OrderByDescending(c => c.FitnessScore).First(); Console.WriteLine($"Gen {gen}: {best} Distance: {1 / best.FitnessScore:F2}"); } ``` The TournamentSelect method randomly samples a few chromosomes and returns the fittest: ``` static PermutationChromosome TournamentSelect(List pop, int size = 5) { var group = pop.OrderBy(_ => Guid.NewGuid()).Take(size); return group.OrderByDescending(c => c.FitnessScore).First(); } ``` The GenerateRandomDistances method initializes a symmetric distance matrix between cities: ``` static double[,] GenerateRandomDistances(int size) { var rand = new Random(); var matrix = new double[size, size]; for (int i = 0; i < size; i++) { for (int j = i + 1; j < size; j++) { double dist = rand.Next(10, 100); matrix[i, j] = dist; matrix[j, i] = dist; } } return matrix; } ``` Using permutation chromosomes ensures each gene (city) appears exactly once. Crossover methods, such as Order Crossover, and mutation strategies, like swap mutation, help maintain validity without requiring manual repair. This makes genetic algorithms a strong match for solving problems like TSP, where permutation integrity is critical. Solving TSP with a GA is an ideal application of evolutionary computing. The problem is simple to describe, hard to solve by brute force, and benefits directly from techniques that explore diverse permutations and preserve useful partial solutions. With careful design, your GA will discover routes that surpass random search and provide practical solutions to complex optimization challenges. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 17: Greedy Isn't Always Bad: Heuristics in Genetic Algorithms](https://www.woodruff.dev/day-17-greedy-isnt-always-bad-heuristics-in-genetic-algorithms/) **Published:** July 30, 2025 **Author:** Chris Woodruff **Excerpt:** Genetic algorithms thrive on randomness and gradual improvement, but randomness alone often leads to slow convergence. While global search is essential to explore the full solution space, local improvements can dramatically accelerate progress. That is where heuristics come into play. Specifically, greedy heuristics can guide genetic algorithms by introducing problem-specific knowledge that favors better starting points, smarter offspring, and faster convergence. **Content:** Genetic algorithms thrive on randomness and gradual improvement, but randomness alone often leads to slow convergence. While global search is essential to explore the full solution space, local improvements can dramatically accelerate progress. That is where heuristics come into play. Specifically, **greedy heuristics** can guide genetic algorithms by introducing problem-specific knowledge that favors better starting points, smarter offspring, and faster convergence. Contrary to the purist view that GAs should remain entirely stochastic, adding domain-aware strategies like greedy initialization, greedy mutation, or repair heuristics can significantly boost performance in complex problems such as the Traveling Salesperson Problem, scheduling, or layout optimization. Let’s take a closer look at how greedy logic can complement evolutionary search and where it can be integrated into your C# GA implementations. Start with the **initial population**. Instead of generating every chromosome randomly, you can seed part of the population using a greedy algorithm that constructs a decent solution based on immediate cost decisions. For TSP, this might involve starting from a random city and always choosing the nearest unvisited neighbor: ``` public static int[] GreedyTour(double[,] distances, int startCity) { int cityCount = distances.GetLength(0); var visited = new bool[cityCount]; var tour = new int[cityCount]; tour[0] = startCity; visited[startCity] = true; for (int i = 1; i < cityCount; i++) { int last = tour[i - 1]; double minDist = double.MaxValue; int nextCity = -1; for (int j = 0; j < cityCount; j++) { if (!visited[j] && distances[last, j] < minDist) { minDist = distances[last, j]; nextCity = j; } } tour[i] = nextCity; visited[nextCity] = true; } return tour; } ``` Use this function to seed a portion of your population during initialization: ``` for (int i = 0; i < populationSize; i++) { int[] genes = i < populationSize / 4 ? GreedyTour(distanceMatrix, i % cityCount) : RandomTour(cityCount); population.Add(new PermutationChromosome(genes)); } ``` This hybrid approach creates a diverse population with a few high-quality initial candidates that help guide early generations. You can also use greedy logic within **mutation** operations. Suppose you mutate a chromosome and want to improve its local structure. After mutation, you can run a simple 2-opt local search to remove obvious route inefficiencies: ``` public void TwoOptLocalSearch(double[,] distances) { bool improvement = true; while (improvement) { improvement = false; for (int i = 1; i < Genes.Length - 2; i++) { for (int j = i + 1; j < Genes.Length; j++) { double before = distances[Genes[i - 1], Genes[i]] + distances[Genes[j], Genes[(j + 1) % Genes.Length]]; double after = distances[Genes[i - 1], Genes[j]] + distances[Genes[i], Genes[(j + 1) % Genes.Length]]; if (after < before) { Array.Reverse(Genes, i, j - i + 1); improvement = true; } } } } } ``` Use this after crossover or mutation on a small percentage of chromosomes to clean up inefficient gene segments. This type of local optimization is greedy because it always takes the best immediate improvement, but it works extremely well when integrated into global search. Another opportunity to apply greedy heuristics is in **repairing infeasible solutions**. In problems like scheduling, mutations can cause constraint violations. You can use greedy logic to detect and fix these violations efficiently while preserving the core of the chromosome. For example, if a job is scheduled twice, you could replace the duplicate with the next available job in a greedy manner. Greedy operators can also guide **crossover**. Instead of randomly mixing parent genes, you can construct a child by greedily selecting the next best gene from either parent, based on the shortest edge or lowest cost: ``` public static int[] GreedyCrossover(int[] parent1, int[] parent2, double[,] distances) { var remaining = new HashSet(parent1); var tour = new List { parent1[0] }; remaining.Remove(parent1[0]); while (remaining.Count > 0) { int last = tour[^1]; int next1 = GetNextCity(parent1, last, remaining); int next2 = GetNextCity(parent2, last, remaining); double d1 = next1 >= 0 ? distances[last, next1] : double.MaxValue; double d2 = next2 >= 0 ? distances[last, next2] : double.MaxValue; int next = d1 < d2 ? next1 : next2; tour.Add(next); remaining.Remove(next); } return tour.ToArray(); } private static int GetNextCity(int[] parent, int current, HashSet remaining) { int index = Array.IndexOf(parent, current); for (int i = index + 1; i < parent.Length; i++) { if (remaining.Contains(parent[i])) return parent[i]; } for (int i = 0; i < index; i++) { if (remaining.Contains(parent[i])) return parent[i]; } return -1; } ``` This greedy crossover combines useful subsequences from both parents while maintaining feasibility. Greedy methods are especially powerful when applied selectively. Use them to generate better starting populations, refine children, or maintain valid solutions. The key is not to replace global search with greedy logic, but to complement it. Evolution needs randomness to discover new ideas and greedy heuristics to refine them efficiently. By integrating greedy decisions at critical points, your genetic algorithm becomes more than just an evolutionary system. It becomes a strategic search engine that balances global exploration with local exploitation. When used carefully, greedy isn’t just acceptable. It’s smart. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 20: Constraint Handling in Fitness Functions: Penalizing Bad Solutions](https://www.woodruff.dev/day-20-constraint-handling-in-fitness-functions-penalizing-bad-solutions/) **Published:** August 5, 2025 **Author:** Chris Woodruff **Excerpt:** Genetic algorithms are powerful optimization tools, but real-world problems often involve constraints that cannot be ignored. In scheduling, routing, resource allocation, and layout optimization, constraints like resource limits, timing conflicts, and exclusivity rules define what makes a solution valid. Without a mechanism to enforce or guide adherence to these rules, a genetic algorithm may evolve highly fit but completely infeasible solutions. One effective way to handle this is by integrating constraint penalties directly into the fitness function. By penalizing violations, we make sure that invalid solutions are less likely to survive and propagate in future generations. This method requires designing fitness functions that are both accurate evaluators and adaptive enforcers of your problem's rules. **Content:** Genetic algorithms are powerful optimization tools, but real-world problems often involve constraints that cannot be ignored. In scheduling, routing, resource allocation, and layout optimization, constraints like resource limits, timing conflicts, and exclusivity rules define what makes a solution valid. Without a mechanism to enforce or guide adherence to these rules, a genetic algorithm may evolve highly fit but completely infeasible solutions. One effective way to handle this is by integrating **constraint penalties directly into the fitness function**. By penalizing violations, we make sure that invalid solutions are less likely to survive and propagate in future generations. This method requires designing fitness functions that are both accurate evaluators and adaptive enforcers of your problem’s rules. Let’s explore this in the context of a scheduling problem. A chromosome represents assignments of classes to time slots and rooms. Hard constraints might include: - No room can be assigned to more than one class at the same time - No instructor can teach more than one class at the same time - No student group can attend more than one class at a time We begin with a base fitness function that counts the number of complex constraints violated. The fewer the violations, the higher the fitness. ``` public double EvaluateFitness(List courses, List groups) { int violations = 0; var roomOccupancy = new Dictionary(); var instructorSchedule = new Dictionary(); var studentGroupSchedule = new Dictionary(); foreach (var gene in Genes) { var key = $"{gene.Slot.Day}-{gene.Slot.Hour}"; // Room conflicts string roomKey = $"{gene.RoomId}:{key}"; if (!roomOccupancy.TryAdd(roomKey, new HashSet { gene.CourseId })) violations++; // Instructor conflicts var course = courses.First(c => c.Id == gene.CourseId); string instructorKey = $"{course.Instructor}:{key}"; if (!instructorSchedule.TryAdd(instructorKey, new HashSet { gene.CourseId })) violations++; // Student group conflicts foreach (var groupId in course.StudentGroupIds) { string groupKey = $"{groupId}:{key}"; if (!studentGroupSchedule.TryAdd(groupKey, new HashSet { gene.CourseId })) violations++; } } // Convert violations to fitness return 1.0 / (1 + violations); } ``` This simple rule ensures that perfect schedules (with zero violations) have the highest fitness of 1.0, while infeasible schedules score lower as the number of violations increases. You can make this more flexible by assigning different weights to different constraints: ``` public double EvaluateWeightedFitness(List courses) { int roomConflicts = 0; int instructorConflicts = 0; var roomMap = new Dictionary(); var instructorMap = new Dictionary(); foreach (var gene in Genes) { var key = $"{gene.Slot.Day}-{gene.Slot.Hour}"; var course = courses.First(c => c.Id == gene.CourseId); if (!roomMap.TryAdd($"{gene.RoomId}:{key}", new HashSet { gene.CourseId })) roomConflicts++; if (!instructorMap.TryAdd($"{course.Instructor}:{key}", new HashSet { gene.CourseId })) instructorConflicts++; } double penalty = roomConflicts * 10 + instructorConflicts * 5; return 1.0 / (1 + penalty); } ``` This scoring system reflects the severity of each type of violation. A room conflict is more expensive than an instructor overlap. This helps the GA learn which rules are most critical and prioritize fixing those first. Another powerful technique is **adaptive penalty scaling**. This involves increasing the severity of penalties over generations. Early in the process, diversity and exploration are more important, so minor violations are tolerated. As evolution progresses, constraints are enforced more strictly to fine-tune solutions. ``` public double EvaluateAdaptiveFitness(List courses, int generation) { int violations = CountViolations(courses); double penaltyWeight = 1.0 + generation / 50.0; // Increase over time return 1.0 / (1 + penaltyWeight * violations); } ``` Adaptive penalties help prevent premature convergence by allowing exploratory solutions in early generations, then tightening enforcement as the population stabilizes. Penalties can also be combined with **soft preferences**. A class may prefer certain times or instructors may prefer fewer late sessions. These preferences are not hard constraints, but satisfying them improves overall quality. ``` public double EvaluateWithPreferences(List courses) { int violations = CountHardViolations(courses); int preferenceScore = CountSatisfiedPreferences(); double baseFitness = 1.0 / (1 + violations); return baseFitness + 0.01 * preferenceScore; } ``` Soft constraints can drive improvements once feasibility is reached, encouraging the GA to optimize for quality beyond just validity. When dealing with constraints in GAs, the goal is not only to reject poor solutions, but also to gently guide the population toward valid, high-performing regions of the search space. Well-designed fitness functions serve as both a compass and a filter, rewarding desirable traits and discouraging undesirable ones. Constraint handling via fitness penalties is an elegant and effective way to mold the evolutionary process. It provides developers with the flexibility to encode complex rules directly into the natural selection mechanism, allowing them to evolve solutions that are not only optimized but also compliant with the problem’s domain logic. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 21: Genetic Algorithms vs. Brute Force: A Benchmark Comparison](https://www.woodruff.dev/day-21-genetic-algorithms-vs-brute-force-a-benchmark-comparison/) **Published:** August 6, 2025 **Author:** Chris Woodruff **Excerpt:** To conclude Week 3, let’s address one of the most common questions developers ask when learning about genetic algorithms: How do they perform compared to brute-force solutions? This is especially relevant when working on combinatorial problems, such as the Traveling Salesperson Problem (TSP) or scheduling tasks. Genetic algorithms promise that they offer reasonable solutions in a fraction of the time it takes brute-force methods to find optimal ones. But how does this actually play out? Today, we’ll benchmark a simple scenario using both approaches and compare execution time and solution quality. We’ll use the TSP with 8 cities as our problem space. This size is large enough to make brute-force non-trivial, but still solvable within a reasonable time. **Content:** To conclude Week 3, let’s address one of the most common questions developers ask when learning about genetic algorithms: How do they perform compared to brute-force solutions? This is especially relevant when working on combinatorial problems, such as the Traveling Salesperson Problem (TSP) or scheduling tasks. Genetic algorithms promise that they offer reasonable solutions in a fraction of the time it takes brute-force methods to find optimal ones. But how does this actually play out? Today, we’ll benchmark a simple scenario using both approaches and compare execution time and solution quality. We’ll use the TSP with 8 cities as our problem space. This size is large enough to make brute-force non-trivial, but still solvable within a reasonable time. Let’s start by defining our city coordinates and computing the distance matrix: ``` public static PointF[] GenerateCities() { return new[] { new PointF(0, 0), new PointF(1, 5), new PointF(5, 1), new PointF(2, 2), new PointF(3, 6), new PointF(6, 3), new PointF(7, 1), new PointF(4, 4) }; } public static double[,] ComputeDistanceMatrix(PointF[] cities) { int n = cities.Length; var matrix = new double[n, n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { var dx = cities[i].X - cities[j].X; var dy = cities[i].Y - cities[j].Y; matrix[i, j] = Math.Sqrt(dx * dx + dy * dy); } } return matrix; } ``` ### Brute Force A brute-force approach evaluates every possible permutation of city visits and returns the one with the lowest total distance: ``` public static (int[] bestRoute, double bestDistance) BruteForceTSP(double[,] distanceMatrix) { int n = distanceMatrix.GetLength(0); var cities = Enumerable.Range(0, n).ToArray(); var permutations = GetPermutations(cities); double bestDistance = double.MaxValue; int[] bestRoute = null; foreach (var route in permutations) { double dist = 0; for (int i = 0; i < n - 1; i++) dist += distanceMatrix[route[i], route[i + 1]]; dist += distanceMatrix[route[n - 1], route[0]]; if (dist < bestDistance) { bestDistance = dist; bestRoute = route.ToArray(); } } return (bestRoute, bestDistance); } // Helper for generating permutations public static IEnumerable GetPermutations(int[] items) { return Permute(items, 0, items.Length - 1); IEnumerable Permute(int[] array, int l, int r) { if (l == r) yield return array.ToArray(); else { for (int i = l; i 8 -> 3 -> 9 -> 7 -> 2 -> 10 -> 1 -> 5 -> 0 -> 6GA Distance: 275.25GA Time: 321msRunning Brute Force...Brute Best Path: 0 -> 5 -> 1 -> 10 -> 2 -> 7 -> 9 -> 3 -> 8 -> 4 -> 6Brute Distance: 275.25Brute Time: 10832ms ``` ### Conclusion Genetic algorithms do not guarantee an optimal solution, but they are exceptionally good at producing efficient solutions, especially when the solution space becomes too large for exhaustive methods. For small problems, brute force may still be feasible. But for real-world applications involving dozens or hundreds of variables and constraints, genetic algorithms offer a practical and scalable alternative. In many scenarios, near-optimal is more than good enough when it comes with significant time savings. Genetic algorithms excel by guiding the search process through randomness, selection, and evolutionary pressure, producing high-quality solutions with a fraction of the computational cost. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 14: Evolving Text: Solving the "Hello World" Puzzle with a C# Genetic Algorithm](https://www.woodruff.dev/day-14-evolving-text-solving-the-hello-world-puzzle-with-a-c-genetic-algorithm/) **Published:** June 30, 2025 **Author:** Chris Woodruff **Excerpt:** Now that you’ve built the complete set of genetic algorithm components, chromosomes, fitness functions, mutation, crossover, selection, and a configurable loop, it’s time to apply everything in a hands-on project. In today’s post, we’ll use a genetic algorithm to evolve a string toward a target phrase: "HELLO WORLD". This classic exercise helps demonstrate how genetic algorithms work in a tangible, visual way. You’ll see the population of strings gradually improve, letter by letter, until they match the target. It’s a powerful example of emergent behavior through selection and variation. **Content:** Now that you’ve built the complete set of genetic algorithm components, chromosomes, fitness functions, mutation, crossover, selection, and a configurable loop, it’s time to apply everything in a hands-on project. In today’s post, we’ll use a genetic algorithm to evolve a string toward a target phrase: `"HELLO WORLD"`. This classic exercise helps demonstrate how genetic algorithms work in a tangible, visual way. You’ll see the population of strings gradually improve, letter by letter, until they match the target. It’s a powerful example of emergent behavior through selection and variation. ## The Problem We want to evolve a population of randomly generated character sequences so that over time, one of them becomes the exact string `"HELLO WORLD"`. Each solution (chromosome) is a sequence of characters. Our fitness function evaluates how closely the chromosome aligns with the target. Over successive generations, we use crossover, mutation, and selection to create better solutions. ## Setting Up the Project Start by defining the configuration for the algorithm: ``` var config = new GAConfig { PopulationSize = 200, MaxGenerations = 1000, MutationRate = 0.01, EliteCount = 2, Target = "HELLO WORLD" }; ``` Create an instance of the engine and run it: ``` csharpCopyEdit``` var ga = new GeneticAlgorithm(config);var best = ga.Run();Console.WriteLine($"Final Result: {best} (Fitness: {best.FitnessScore})"); ``` ``` ## The Chromosome Class ``` public class Chromosome { public char[] Genes { get; private set; } public int FitnessScore { get; set; } private static readonly string GenePool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !"; private static readonly Random Random = new(); public Chromosome(int length) { Genes = Enumerable.Range(0, length) .Select(_ => RandomGene()) .ToArray(); } public Chromosome(char[] genes) { Genes = genes; } public int GetFitness(string target) { return Genes.Zip(target, (g, t) => g == t ? 1 : 0).Sum(); } public Chromosome UniformCrossover(Chromosome partner) { char[] childGenes = new char[Genes.Length]; for (int i = 0; i < Genes.Length; i++) { childGenes[i] = Random.NextDouble() < 0.5 ? Genes[i] : partner.Genes[i]; } return new Chromosome(childGenes); } public void Mutate(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.NextDouble() < mutationRate) { Genes[i] = RandomGene(); } } } private static char RandomGene() { return GenePool[Random.Next(GenePool.Length)]; } public override string ToString() => new string(Genes); } ``` ## Visualizing the Evolution Inside the GA loop, log each generation’s best candidate: ``` for (int generation = 0; generation < _config.MaxGenerations; generation++) { EvaluatePopulation(population); var best = population.OrderByDescending(c => c.FitnessScore).First(); Console.WriteLine($"Gen {generation}: {best} (Fitness: {best.FitnessScore})"); if (best.FitnessScore == _config.Target.Length) break; population = Evolve(population); } ``` The output will look something like this: ``` Gen 0: kELlX TnzvM (Fitness: 2)Gen 25: HELLp WORmD (Fitness: 10)Gen 38: HELLO WORLD (Fitness: 11) ``` The algorithm slowly improves from gibberish to the target phrase. Each generation gets closer, proving the effectiveness of selection and variation. ## Why This Works This experiment illustrates key strengths of genetic algorithms: - The search space is enormous (95^11 possibilities), but GAs narrow it quickly. - Mutation maintains diversity and avoids local optima. - Crossover reuses strong gene segments to improve solutions. - Elitism protects breakthroughs and accelerates convergence. All of this comes together through a simple, elegant loop. ## Extending the Idea Now that you’ve evolved a string, try: - Evolving multiple target phrases - Scoring by edit distance instead of exact character matches - Adding adaptive mutation rates - Visualizing average population fitness over time These variations prepare you to evolve more complex structures like code, schedules, or routes. ## Conclusion Evolving text is a classic introduction to genetic algorithms because it reveals the inner mechanics so clearly. Each generation brings better results, not because the algorithm knows the answer, but because it selects and reuses what works. This is evolution in action, driven by code. ## Up Next Tomorrow, we begin solving practical optimization problems using GAs in C#. We’ll start with pathfinding and scheduling, building on the tools and techniques you’ve already developed. Evolution doesn’t stop here. It adapts to every domain. You can find the code demos for the GA series at **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 18: Mapping Cities: Visualizing TSP Evolution in .NET](https://www.woodruff.dev/day-18-mapping-cities-visualizing-tsp-evolution-in-net/) **Published:** July 31, 2025 **Author:** Chris Woodruff **Excerpt:** One of the most effective ways to understand the progress of a genetic algorithm is to visualize its evolution. When solving the Traveling Salesperson Problem, a well-designed visualization can clearly show how random routes evolve into efficient paths over time. In .NET, we can use simple drawing libraries like System.Drawing to generate visual output during the evolution process. Today, we will walk through how to integrate a visual component into your genetic algorithm for TSP using permutation chromosomes. You will generate images that show the route taken by the best chromosome of each generation, allowing you to see improvement over time. **Content:** One of the most effective ways to understand the progress of a genetic algorithm is to visualize its evolution. When solving the Traveling Salesperson Problem, a well-designed visualization can clearly show how random routes evolve into efficient paths over time. In .NET, we can use simple drawing libraries like `System.Drawing` to generate visual output during the evolution process. Today, we will walk through how to integrate a visual component into your genetic algorithm for TSP using permutation chromosomes. You will generate images that show the route taken by the best chromosome of each generation, allowing you to see improvement over time. Start by defining city positions as a list of points. These positions will be used both to compute distances and to draw the city map. ``` public static PointF[] GenerateCities(int count, int width, int height) { var rand = new Random(); return Enumerable.Range(0, count) .Select(_ => new PointF(rand.Next(width), rand.Next(height))) .ToArray(); } ``` To calculate distances for fitness evaluation, use the Euclidean formula: ``` public static double[,] ComputeDistanceMatrix(PointF[] cities) { int n = cities.Length; var matrix = new double[n, n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { var dx = cities[i].X - cities[j].X; var dy = cities[i].Y - cities[j].Y; matrix[i, j] = Math.Sqrt(dx * dx + dy * dy); } } return matrix; } ``` To visualize the best route of a generation, use `System.Drawing` to render the cities and draw lines connecting them in order. ``` public static void DrawRoute(string filename, PointF[] cities, int[] route) { int width = 800; int height = 800; using var bmp = new Bitmap(width, height); using var g = Graphics.FromImage(bmp); g.Clear(Color.White); var pen = new Pen(Color.Blue, 2); var cityBrush = Brushes.Red; // Draw cities foreach (var city in cities) { g.FillEllipse(cityBrush, city.X - 4, city.Y - 4, 8, 8); } // Draw route for (int i = 0; i < route.Length - 1; i++) { var a = cities[route[i]]; var b = cities[route[i + 1]]; g.DrawLine(pen, a, b); } // Close the loop g.DrawLine(pen, cities[route[^1]], cities[route[0]]); bmp.Save(filename); } ``` Integrate this into your genetic algorithm loop so that it saves a frame every N generations. This provides a sequence of images that shows the optimization in action. ``` if (generation % 10 == 0) { var best = population.OrderByDescending(c => c.FitnessScore).First(); DrawRoute($"output/gen_{generation}.png", cities, best.Genes); } ``` Ensure the output folder exists before saving the frames. The result is not just a working genetic algorithm, but a visual insight into how it thinks. You will be able to observe how early generations explore random routes and how selection, crossover, and mutation push the population toward more efficient paths. Over time, city connections become tighter and smoother as the algorithm converges. This feedback loop is not only educational, but it is also helpful in debugging and tuning. Visual artifacts can reveal stagnation, lack of diversity, or local optima. If progress stalls or routes oscillate without improvement, it may signal a need to adjust mutation rate, crossover strategy, or selection pressure. Visualization bridges the gap between algorithm and understanding. It transforms the abstract concept of evolution into something tangible and clear. For problems like TSP that exist in a spatial domain, there is no better way to validate your GA than watching its path to success unfold on a map. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 13: Configuring the Genetic Algorithm Loop in C#](https://www.woodruff.dev/day-13-configuring-the-genetic-algorithm-loop/) **Published:** June 27, 2025 **Author:** Chris Woodruff **Excerpt:** A genetic algorithm is only as effective as the loop that drives it. While selection, crossover, mutation, and elitism form the backbone of a genetic algorithm (GA), it is the configuration of the evolution loop that determines how the algorithm behaves over time. Today’s focus is on designing and implementing the loop that runs your genetic algorithm. We will examine how to parameterize the loop with values such as population size, mutation rate, elite count, and maximum generations, and how to structure your logic to support reusable, testable, and adaptable evolutionary flows in C#. **Content:** A genetic algorithm is only as effective as the loop that drives it. While selection, crossover, mutation, and elitism form the backbone of a genetic algorithm (GA), it is the configuration of the **evolution loop** that determines how the algorithm behaves over time. Today’s focus is on designing and implementing the loop that runs your genetic algorithm. We will examine how to parameterize the loop with values such as population size, mutation rate, elite count, and maximum generations, and how to structure your logic to support reusable, testable, and adaptable evolutionary flows in C#. ## The GA Loop Structure At a high level, every GA loop looks like this: 1. Initialize population 2. Evaluate fitness 3. Select parents 4. Apply crossover and mutation 5. Preserve elite individuals 6. Replace the old population with the new 7. Repeat for N generations or until a stopping condition is met By exposing configuration options and encapsulating the loop logic, we can create a flexible engine that adapts to multiple problem domains. ![GA Loop](https://woodruff.dev/wp-content/uploads/2025/06/GA-loop.png)## Configuration Parameters Before implementing the loop, define your key GA parameters: ``` public class GAConfig { public int PopulationSize { get; set; } = 100; public int MaxGenerations { get; set; } = 500; public double MutationRate { get; set; } = 0.01; public int EliteCount { get; set; } = 2; public string Target { get; set; } = "HELLO WORLD"; } ``` This `GAConfig` class allows you to configure your algorithm externally or via a UI or script. ## Setting Up the Evolution Engine Let’s encapsulate the core evolutionary logic into a method: ``` public class GeneticAlgorithm { private readonly GAConfig _config; public GeneticAlgorithm(GAConfig config) { _config = config; } public Chromosome Run() { var population = InitializePopulation(); for (int generation = 0; generation < _config.MaxGenerations; generation++) { EvaluatePopulation(population); var best = population.OrderByDescending(c => c.FitnessScore).First(); Console.WriteLine($"Gen {generation}: {best} (Fitness: {best.FitnessScore})"); if (best.FitnessScore == _config.Target.Length) return best; population = Evolve(population); } return population.OrderByDescending(c => c.FitnessScore).First(); } private List InitializePopulation() { return Enumerable.Range(0, _config.PopulationSize) .Select(_ => new Chromosome(_config.Target.Length)) .ToList(); } private void EvaluatePopulation(List population) { foreach (var c in population) { c.FitnessScore = c.GetFitness(_config.Target); } } private List Evolve(List current) { var nextGen = new List(); var elites = current.OrderByDescending(c => c.FitnessScore) .Take(_config.EliteCount) .Select(c => new Chromosome((char[])c.Genes.Clone())) .ToList(); nextGen.AddRange(elites); while (nextGen.Count < _config.PopulationSize) { var parent1 = TournamentSelection(current, 5); var parent2 = TournamentSelection(current, 5); var child = parent1.UniformCrossover(parent2); child.Mutate(_config.MutationRate); nextGen.Add(child); } return nextGen; } private Chromosome TournamentSelection(List population, int size) { var group = population.OrderBy(_ => Guid.NewGuid()).Take(size); return group.OrderByDescending(c => c.FitnessScore).First(); } } ``` ## Tuning the Parameters ParameterRoleTypical RangePopulationSizeNumber of chromosomes per generation50–500 depending on problem sizeMaxGenerationsLimits evolutionary time100–1000 or moreMutationRateControls variation injection0.005–0.05EliteCountPreserves top individuals per generation1–5 for small populationsExperiment with these values depending on problem complexity, gene size, and diversity needs. ## Logging and Monitoring To debug or visualize performance over time, add tracking: - Best fitness per generation - Average population fitness - Diversity metrics (e.g., unique gene sequences) These help determine whether your configuration promotes healthy evolution or premature convergence. ## Conclusion Configuring the GA loop isn’t just about wiring steps together. It is about balancing forces—mutation versus selection, exploration versus exploitation, and speed versus stability. By making your loop modular and parameter-driven, you gain the flexibility to evolve a wide variety of solutions in C#. ## Up Next With all core mechanics in place, we next begin solving real-world problems using genetic algorithms, starting with the classic “evolving text” challenge. You’ve built the engine. Now it’s time to drive it. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Evolve Your C# Code with AI: A 5-Week Genetic Algorithms Bootcamp for Developers](https://www.woodruff.dev/evolve-your-c-code-with-ai-a-5-week-genetic-algorithms-bootcamp-for-developers/) **Published:** May 24, 2025 **Author:** Chris Woodruff **Excerpt:** What if your code could evolve like life itself—adapting, optimizing, and learning over time? Welcome to the AI-inspired world of Genetic Algorithms, where we blend evolution with code to solve complex problems cleverly. Starting this week, I'm launching a 42-day blog series—a 4-week bootcamp—designed to teach C# and .NET developers how to build, run, and scale Genetic Algorithms. From foundational concepts to solving real-world optimization problems, this series is your guide to coding like Darwin meant it. Using clean, testable C# code, we'll simulate survival of the fittest with fitness functions, crossover operations, mutations, and elite selection. This isn't theoretical fluff—it's practical, hands-on AI for your everyday dev life. Whether you're optimizing routes, building smarter schedules, or just curious how to make your software think, this series is for you. **Content:** What if your code could evolve like life itself, adapting, optimizing, and learning over time? Welcome to the **AI-inspired world of Genetic Algorithms**, where we blend evolution with code to solve complex problems cleverly. Starting the week of June 2, I’m launching a **37-day blog series**—a **5-week bootcamp**—designed to teach C# and .NET developers how to build, run, and scale Genetic Algorithms. From foundational concepts to solving real-world optimization problems, this series is your guide to coding like Darwin meant it. Using clean, testable C# code, we’ll simulate survival of the fittest with fitness functions, crossover operations, mutations, and elite selection. This isn’t theoretical fluff—it’s practical, hands-on AI for your everyday dev life. Whether you’re optimizing routes, building smarter schedules, or just curious how to make your software *think*, this series is for you. ## Here’s Your Day-by-Day Bootcamp Lineup: 1. **[The Survival of the Fittest Code: Why Learn Genetic Algorithms in C#?](https://woodruff.dev/day-1-the-survival-of-the-fittest-code-why-learn-genetic-algorithms-in-c/)** 2. **[What Are Genetic Algorithms? A Developer’s Guide to Evolutionary Logic](https://woodruff.dev/day-2-evolution-in-code-the-core-concepts/)** 3. **[Understanding Chromosomes, Genes, and DNA in Code](https://woodruff.dev/day-3-understanding-chromosomes-genes-and-dna-in-code/)** 4. **[Designing Your First Chromosome Class in C#](https://woodruff.dev/day-4-designing-your-first-chromosome-class-in-c/)** 5. **[Natural Selection in Software: Implementing Fitness Functions](https://woodruff.dev/day-5-natural-selection-in-software-implementing-fitness-functions/)** 6. **[Roulette, Tournaments, and Elites: Exploring Selection Strategies](https://woodruff.dev/day-6-roulette-tournaments-and-elites-exploring-selection-strategies/)** 7. **[Putting It Together: Simulating Your First GA Cycle in .NET](https://woodruff.dev/day-7-putting-it-together-simulating-your-first-ga-cycle-in-net/)** 8. [**One Point or Two? How Crossover Shapes Genetic Diversity**](https://woodruff.dev/day-8-one-point-or-two-how-crossover-shapes-genetic-diversity/) 9. **[Uniform Crossover in C#: Combining Chromosomes with Balance](https://woodruff.dev/day-9-using-genetic-algorithms-uniform-crossover-in-c/)** 10. [**Mutation Matters: How Small Changes Spark Big Improvements**](https://woodruff.dev/day-10-mutation-matters-in-c-genetic-algorithms/) 11. **[Implementing a Mutation Operator with Randomness in Mind](https://woodruff.dev/day-11-implementing-a-c-mutation-operator-for-genetic-algorithms/)** 12. **[Elitism in Evolution: Preserving the Best Code](https://woodruff.dev/day-12-genetic-algorithms-elitism-for-evolution-survival-of-the-fittest/)** 13. Configuring the GA Loop: Population, Generations, and Mutation Rates 14. Evolving Text: Solving the “Hello World” Puzzle with a GA 15. Fitness by Design: How to Shape the Problem to Match Evolution 16. Solving the Traveling Salesperson Problem with Permutation Chromosomes 17. Greedy Isn’t Always Bad: Heuristics in Genetic Algorithms 18. Mapping Cities: Visualizing TSP Evolution in .NET 19. Scheduling with DNA: Using GAs for Class and Work Timetables 20. Constraint Handling in Fitness Functions: Penalizing Bad Solutions 21. Genetic Algorithms vs. Brute Force: A Benchmark Comparison 22. Multi-Objective Optimization: When One Fitness Function Isn’t Enough 23. Introduction to NSGA-II in C# 24. Combining GAs with Hill Climbing: The Hybrid Memetic Approach 25. Scaling Up: Parallelizing GA Loops in .NET with Parallel.ForEach 26. Running GAs in the Cloud with Azure Batch or Functions 27. Logging and Monitoring Genetic Progress Over Generations 28. Visualizing Evolution: Building a Real-Time Chart of Fitness 29. Building a Pluggable GA Framework in C# 30. Defining Interfaces for GA Components: Fitness, Selection, and Operators 31. Unit Testing Your Evolution: Making GAs Testable and Predictable 32. Best Practices for Tuning Genetic Algorithm Parameters 33. When GAs Go Wrong: Debugging Poor Performance and Premature Convergence 34. Case Study: Using a GA to Optimize Hyperparameters in a Neural Network 35. GA vs. Other Optimization Techniques: A Developer’s Perspective 36. Evolution Beyond Biology: Using GAs for Creative Art and Design 37. Final Reflections: 37 Days of Evolutionary Coding ## What You’ll Learn Along the Way - How to model evolution in code with fitness functions, crossover, and mutation - How to build a fully functional GA engine in C# and .NET - How to apply GAs to real-world challenges like TSP, scheduling, and hyperparameter tuning - How to refactor your experiments into reusable, testable frameworks - How to visualize and debug AI-like behavior in your applications ## Join the Experiment Follow the series here on the blog or subscribe for updates via [RSS](https://woodruff.dev/feed/) or [GitHub](https://github.com/cwoodruff/genetic-algorithms-blog-series). You’ll get daily doses of insight, complete code walkthroughs, and challenges to try independently. Whether you’re a .NET pro or just curious about AI, this is your chance to build something truly evolutionary. **Let’s evolve some code.** **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 12: Genetic Algorithms' Elitism for Evolution Survival of the Fittest](https://www.woodruff.dev/day-12-genetic-algorithms-elitism-for-evolution-survival-of-the-fittest/) **Published:** June 26, 2025 **Author:** Chris Woodruff **Excerpt:** Natural selection favors the survival of the fittest, but evolution in the wild is not always efficient. In genetic algorithms, we can bias the process toward faster convergence by deliberately preserving top-performing individuals across generations. This technique is known as elitism, and it is one of the simplest yet most effective strategies for enhancing GA performance. Today’s post focuses on applying elitism in a C# genetic algorithm to ensure that the best solutions are never lost. We will define elitism, explain its impact on the evolutionary process, and demonstrate how to implement it cleanly and effectively. **Content:** Natural selection favors the survival of the fittest, but evolution in the wild is not always efficient. In genetic algorithms, we can bias the process toward faster convergence by deliberately preserving top-performing individuals across generations. This technique is known as **elitism**, and it is one of the simplest yet most effective strategies for enhancing GA performance. Today’s post focuses on applying elitism in a C# genetic algorithm to ensure that the best solutions are never lost. We will define elitism, explain its impact on the evolutionary process, and demonstrate how to implement it cleanly and effectively. ## What Is Elitism? Elitism is a selection method that guarantees a specific number of the fittest chromosomes (called elites) are carried over unchanged from one generation to the next. These elite individuals bypass crossover and mutation to preserve their exact structure and performance. Without elitism, there’s always a chance that recombination or mutation degrades even the best solutions. Elitism avoids this risk by protecting a small number of high-quality chromosomes. ## Why Use Elitism? - **Preserves High Fitness**: Ensures top solutions are passed down through the gene pool. - **Accelerates Convergence**: Reduces the time to reach acceptable solutions. - **Stabilizes Evolution**: Maintains a performance baseline during exploration. However, overuse of elitism can reduce diversity and lead to premature convergence. It’s best used in moderation, typically preserving 1 to 5 of the best individuals per generation. ## Implementing Elitism in C# Assume you have a `List` representing the current population. Here’s how to apply elitism to extract the top-performing chromosomes: ``` public List ApplyElitism(List population, int eliteCount) { return population .OrderByDescending(c => c.FitnessScore) .Take(eliteCount) .Select(c => new Chromosome((char[])c.Genes.Clone())) .ToList(); } ``` This implementation: - Sorts the population by fitness - Takes the top `eliteCount` chromosomes - Clone them to avoid reference issues when evolving the next generation You can then insert these elites directly into the next generation before filling the remaining slots via selection and crossover. ## Integrating Elitism in the GA Loop Below is a simplified structure of how elitism fits into your main evolution logic: ``` var newPopulation = new List(); // Step 1: Elitism var elites = ApplyElitism(currentPopulation, eliteCount); newPopulation.AddRange(elites); // Step 2: Fill the rest of the population while (newPopulation.Count < populationSize) { var parent1 = TournamentSelection(currentPopulation); var parent2 = TournamentSelection(currentPopulation); var child = parent1.Crossover(parent2); child.Mutate(mutationRate); newPopulation.Add(child); } ``` This ensures that elite individuals are always preserved, while the rest of the population is subject to natural selection or evolutionary pressure. ## Choosing an Elitism Strategy Population SizeTypical Elite Count501–21002–5500+5–10You can also apply a **dynamic elitism** strategy, where the number of elites changes based on population diversity or progress in generations. ## Caution: Elitism vs. Diversity Elitism introduces exploitation pressure. If used excessively, it can crowd out diversity and lead to genetic stagnation. To avoid this: - Keep elite count low - Combine with mutation or diversity-preserving selection - Monitor convergence rates and diversity metrics ## Conclusion Elitism is a powerful addition to your genetic algorithm, ensuring that hard-won solutions are not lost in the randomness of evolution. Used properly, it stabilizes progress and improves your algorithm’s performance with reduced volatility. ## Up Next In the next post, we’ll integrate all operator components —selection, crossover, mutation, and elitism —into a configurable GA loop. You’ll learn how to set population size, mutation rate, elite count, and generation limits to adapt your GA to different problem domains. You’ve built the parts. Now it’s time to engineer the machine. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 11: Implementing a C# Mutation Operator for Genetic Algorithms](https://www.woodruff.dev/day-11-implementing-a-c-mutation-operator-for-genetic-algorithms/) **Published:** June 23, 2025 **Author:** Chris Woodruff **Excerpt:** In yesterday’s post, we explored the importance of mutation in genetic algorithms. Mutation helps maintain genetic diversity, prevent premature convergence, and enable the discovery of better solutions through small, random changes. Today, we shift from theory to implementation. Our goal is to code a mutation operator in C# that is both configurable and adaptable to different types of chromosomes. This operator will be a core component of your genetic algorithm loop, introducing the right level of randomness into your evolutionary process. **Content:** In yesterday’s post, we explored the importance of mutation in genetic algorithms. Mutation helps maintain genetic diversity, prevent premature convergence, and enable the discovery of better solutions through small, random changes. Today, we shift from theory to implementation. Our goal is to code a **mutation operator** in C# that is both configurable and adaptable to different types of chromosomes. This operator will be a core component of your genetic algorithm loop, introducing the right level of randomness into your evolutionary process. ## Mutation Revisited A mutation is applied to each gene in a chromosome with a small probability, called the **mutation rate**. For character-based chromosomes, this means replacing a character with another from the gene pool. For numeric or binary chromosomes, the operation might involve flipping a bit or adjusting a value. The implementation should allow flexibility so you can control how aggressive or conservative the mutation is. ## Base Mutation Operator for Character Chromosomes Let’s define a mutation operator for a chromosome where each gene is a character. Assume we already have a `Chromosome` class with a `Genes` array and a method for generating a random gene. ### Full C# Implementation ``` public class Chromosome { public char[] Genes { get; private set; } private static readonly string GenePool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ,.!"; private static readonly Random Random = new(); public Chromosome(int length) { Genes = new char[length]; for (int i = 0; i < length; i++) { Genes[i] = RandomGene(); } } public Chromosome(char[] genes) { Genes = genes; } private static char RandomGene() { return GenePool[Random.Next(GenePool.Length)]; } public void Mutate(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.NextDouble() < mutationRate) { Genes[i] = RandomGene(); } } } public override string ToString() { return new string(Genes); } } ``` This method walks through each gene and mutates it with a probability of `mutationRate`. ## Enhancing the Operator with Customization You may want to vary the mutation strategy based on the problem domain. For example: ### 1. **Fixed-point Mutation** Target a specific number of mutations per chromosome, regardless of length. ``` public void FixedMutation(int mutationCount) { for (int i = 0; i < mutationCount; i++) { int index = Random.Next(Genes.Length); Genes[index] = RandomGene(); } } ``` ### 2. **Adaptive Mutation** Modify the mutation rate based on the population’s diversity or the progress over time. This approach often requires tracking population statistics externally; however, the mutation method can accommodate a dynamic rate. ## Logging Mutations for Debugging Add optional logging to monitor mutation frequency during execution: ``` public void MutateWithLogging(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.NextDouble() < mutationRate) { char oldGene = Genes[i]; Genes[i] = RandomGene(); Console.WriteLine($"Gene at index {i} mutated from '{oldGene}' to '{Genes[i]}'"); } } } ``` This can help you diagnose if mutation is too rare or too aggressive. ## Mutation as a Reusable Strategy For maintainability, consider defining a delegate-based mutation strategy: ``` public delegate void MutationStrategy(Chromosome chromosome); public static void ApplyMutation(Chromosome chromosome, MutationStrategy strategy) { strategy(chromosome); } ``` This allows you to inject different mutation strategies during runtime without modifying your genetic algorithm (GA) loop. ## Final Thoughts The mutation operator may seem small, but it plays a significant role in shaping the long-term dynamics of your genetic algorithm. The right mutation strategy can make the difference between fast convergence and evolutionary stagnation. Make mutation adjustable, test it under different rates, and use it strategically in combination with selection and crossover. ## Up Next In the next post, we’ll focus on **elitism**, a mechanism for preserving top-performing chromosomes across generations to accelerate convergence without sacrificing genetic diversity. Even the best genes need protection. Elitism ensures they survive. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 10: Mutation Matters in C# Genetic Algorithms](https://www.woodruff.dev/day-10-mutation-matters-in-c-genetic-algorithms/) **Published:** June 22, 2025 **Author:** Chris Woodruff **Excerpt:** In biological evolution, mutations are rare, random changes in DNA that introduce new traits. While many mutations are neutral or even harmful, some spark evolutionary leaps. In genetic algorithms, mutation serves the same purpose: injecting fresh variations into the population to avoid stagnation and premature convergence. Without mutation, a genetic algorithm can easily fall into local optima—improving early on but plateauing before reaching the best solution. Mutation helps keep the algorithm dynamic, ensuring exploration continues even when the population becomes homogeneous. Today, we explore how mutation works, its importance in the evolutionary process, and how to implement it in C#. **Content:** In biological evolution, mutations are rare, random changes in DNA that introduce new traits. While many mutations are neutral or even harmful, some spark evolutionary leaps. In genetic algorithms, **mutation serves the same purpose**: injecting fresh variations into the population to avoid stagnation and premature convergence. Without mutation, a genetic algorithm can easily fall into local optima—improving early on but plateauing before reaching the best solution. Mutation helps keep the algorithm dynamic, ensuring exploration continues even when the population becomes homogeneous. Today, we explore how mutation works, its importance in the evolutionary process, and how to implement it in C#. ## Why Mutation Is Essential Crossover alone cannot generate new genes—it only recombines existing ones. If all chromosomes in the population become too similar, crossover will help shuffle away the sameness. Mutation ensures the genetic pool retains diversity over time. ### Key Benefits of Mutation - **Prevents Premature Convergence**: Avoids getting stuck in local maxima or minima. - **Maintains Diversity**: Introduces new alleles into the population. - **Supports Exploration**: Enables discovery of parts of the search space not reachable by crossover. However, too many mutations can make the algorithm behave like a random search. The key is to strike the right balance. ## Mutation in Practice In C#, mutation is typically applied to each gene with a small probability (e.g., 0.5% to 5%). If the condition is met, the gene is replaced with a randomly selected alternative from the gene pool. ### Example: Character-Based Chromosome Assume we are evolving strings. Each gene is a character from a predefined gene pool. ``` public void Mutate(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.Shared.NextDouble() < mutationRate) { Genes[i] = RandomGene(); } } } ``` Here, `RandomGene()` returns a random character from the allowed gene pool: ``` private static readonly string GenePool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ,.!"; private static char RandomGene() { return GenePool[Random.Shared.Next(GenePool.Length)]; } ``` A mutation rate of 0.01 means each gene has a 1% chance of being replaced. ![](https://woodruff.dev/wp-content/uploads/2025/06/IMG_0001.png)## Best Practices for Mutation ### Use Low Rates Mutation is meant to fine-tune the population, not dominate the search. Recommended mutation rates: - **Binary strings**: 1% to 2% - **Character sequences**: 0.5% to 1% - **Complex objects**: custom logic, typically sparse changes ### Combine with Crossover Mutation alone is a random walk. It is most effective when used after crossover to introduce slight deviations in the offspring. ``` var child = parent1.Crossover(parent2); child.Mutate(0.01); ``` ### Monitor Diversity Track population diversity to adjust the mutation rate dynamically. If diversity drops below a threshold, slightly increase the rate to reintroduce variability. ## Impact of Mutation on Convergence Without mutation: - The population converges quickly - The risk of suboptimal results increases - Search space coverage shrinks over time With mutation: - The algorithm retains the ability to explore - It may take longer to converge, but with better global performance - Solutions can recover from early missteps Mutation is what gives the algorithm its evolutionary edge. It enables occasional leaps in solution space, which are crucial for escaping the gravitational pull of local optima. ## Up Next Tomorrow, we’ll write the mutation operator with randomness in mind, making sure it integrates seamlessly into our algorithm and allows for flexible tuning. You’ll build a reusable component that can be customized for your domain-specific chromosomes. Small changes can lead to significant improvements. Mutation is proof of that. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 9: Using Genetic Algorithm's Uniform Crossover in C#](https://www.woodruff.dev/day-9-using-genetic-algorithms-uniform-crossover-in-c/) **Published:** June 20, 2025 **Author:** Chris Woodruff **Excerpt:** So far, we’ve explored one-point and two-point crossover strategies, which split chromosomes at predefined positions. These methods are effective for maintaining gene sequence structure, but they can be limiting when diversity is crucial. Enter uniform crossover—a technique that treats each gene position independently, offering greater mixing and a finer-grained approach to recombination. Today, we’ll implement uniform crossover in C#, compare it with other strategies, and explore when and why you should use it. **Content:** So far, we’ve explored one-point and two-point crossover strategies, which split chromosomes at predefined positions. These methods are effective for maintaining gene sequence structure, but they can be limiting when diversity is crucial. Enter **uniform crossover**—a technique that treats each gene position independently, offering greater mixing and a finer-grained approach to recombination. Today, we’ll implement uniform crossover in C#, compare it with other strategies, and explore when and why you should use it. ## What Is Uniform Crossover? Uniform crossover is a recombination method that operates on a **per-gene basis**. Instead of copying a block of genes from one parent and then the rest from another, the algorithm flips a coin (or generates a random number) for each gene to decide which parent contributes that gene to the child. This strategy maximizes variation and balances inheritance across the entire chromosome. ## Benefits of Uniform Crossover - **High Diversity**: Each gene is a 50/50 decision, promoting exploration. - **Fine-Grained Control**: No dependence on contiguous gene segments. - **Stable Mixing**: Reduces bias in gene inheritance across generations. It is especially effective when genes are **independent** or when the **order** of genes is not critical (e.g., in binary encodings or feature selection problems). ## Implementing Uniform Crossover in C# Here is a simple and efficient implementation in C#: ``` public Chromosome UniformCrossover(Chromosome partner) { int length = Genes.Length; char[] childGenes = new char[length]; for (int i = 0; i < length; i++) { bool takeFromThis = Random.Shared.NextDouble() < 0.5; childGenes[i] = takeFromThis ? Genes[i] : partner.Genes[i]; } return new Chromosome(childGenes); } ``` This method uses a random value at each index to decide which parent’s gene to use. Over many offspring, this produces a well-mixed gene pool. ## Parameterized Uniformity You can introduce a **crossover probability** to control how often genes are exchanged. For example, a 0.7 uniformity rate would take genes from the first parent 70 percent of the time. ``` public Chromosome UniformCrossover(Chromosome partner, double uniformityRate) { int length = Genes.Length; char[] childGenes = new char[length]; for (int i = 0; i < length; i++) { bool takeFromThis = Random.Shared.NextDouble() < uniformityRate; childGenes[i] = takeFromThis ? Genes[i] : partner.Genes[i]; } return new Chromosome(childGenes); } ``` This gives you the flexibility to adjust the balance between exploitation (using the stronger parent more often) and exploration (mixing aggressively). ## Example Comparison Let’s say you have: - `Parent A: HELLO WORLD` - `Parent B: YXLQR PZKMG` One-point crossover at index 5 might yield: - `Child: HELLO PZKMG` Uniform crossover might result in: - `Child: HXLLO PRKLD` Notice how uniform crossover mixes genes from all parts of both parents, not just contiguous sections. This increases the genetic variety across generations, which is crucial for escaping local optima. ## When to Use Uniform Crossover Uniform crossover is especially useful when: - Genes are loosely ordered or unordered - You want to avoid gene dominance - Maintaining specific sequences is not critical However, it can disrupt valuable gene combinations (called **building blocks**) if the gene sequence has structural importance, such as in routing or scheduling problems. In those cases, one- or two-point crossover may be safer. ## Up Next Now that you’ve seen how crossover affects diversity, tomorrow we’ll focus on **mutation**—the small but powerful tweak that helps escape evolutionary dead ends and ensures your population keeps improving. Recombination brings the big moves, but mutation provides the spark. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 8: One Point or Two? How Crossover Shapes Genetic Diversity](https://www.woodruff.dev/day-8-one-point-or-two-how-crossover-shapes-genetic-diversity/) **Published:** June 17, 2025 **Author:** Chris Woodruff **Excerpt:** In the evolutionary process, crossover is the mechanism by which parents pass on their traits to offspring. In genetic algorithms, crossover plays the same role, combining genes from two parent chromosomes to produce new solutions. How you implement crossover significantly impacts the algorithm's ability to explore the search space and avoid premature convergence. Today, we dive into crossover methods in C#, comparing one-point, two-point, and uniform crossover, and how each influences genetic diversity. **Content:** In the evolutionary process, crossover is the mechanism by which parents pass on their traits to offspring. In genetic algorithms, crossover plays the same role, combining genes from two parent chromosomes to produce new solutions. How you implement crossover significantly impacts the algorithm’s ability to explore the search space and avoid premature convergence. Today, we dive into crossover methods in C#, comparing **one-point**, **two-point**, and **uniform crossover**, and how each influences genetic diversity. ## The Role of Crossover Crossover mimics sexual reproduction. It selects two parents and mixes their genes to create one or more children. While mutation introduces random variations, crossover is responsible for combining high-quality traits to form even better solutions, potentially. Practical crossover promotes **diversity** without destroying structure. If done poorly, it can dilute strong solutions or lead to population stagnation. ## One-Point Crossover One-point crossover selects a single cut point in the gene sequence. The child inherits genes from the first parent up to the cut point, and from the second parent beyond it. ### Implementation in C# ``` public Chromosome OnePointCrossover(Chromosome other) { int length = Genes.Length; int crossoverPoint = Random.Shared.Next(1, length - 1); char[] childGenes = new char[length]; for (int i = 0; i < length; i++) { childGenes[i] = i < crossoverPoint ? Genes[i] : other.Genes[i]; } return new Chromosome(childGenes); } ``` ### Benefits - Maintains order and structure from both parents - Simple and efficient ### Drawbacks - Consistently uses the same type of split - Can bias inheritance patterns over time ## Two-Point Crossover Two-point crossover uses two cut points and swaps the gene segments between them. ### Implementation ``` public Chromosome TwoPointCrossover(Chromosome other) { int length = Genes.Length; int point1 = Random.Shared.Next(0, length - 2); int point2 = Random.Shared.Next(point1 + 1, length); char[] childGenes = new char[length]; for (int i = 0; i < length; i++) { if (i < point1 || i >= point2) childGenes[i] = Genes[i]; else childGenes[i] = other.Genes[i]; } return new Chromosome(childGenes); } ``` ### Benefits - More variation than one-point crossover - Preserves the middle sections from the second parent ### Drawbacks - More complex to implement - Can disrupt functional building blocks in some representations ## Uniform Crossover (Preview) In uniform crossover, each gene is chosen randomly from either parent with a fixed probability. We’ll dive deeper into this method in tomorrow’s post. ## Choosing a Strategy There is no one-size-fits-all crossover. The right choice depends on your problem space: MethodStrengthsBest ForOne-PointFast, preserves sequencesProblems where gene order mattersTwo-PointGreater mixing, more explorationComplex encoding schemesUniformMaximum mixing, high diversityBinary or loosely-ordered genesIn most GA implementations, one-point or two-point crossover is the starting point. If your population stagnates or converges prematurely, switching to a different crossover method is often a good first step. ## Up Next Tomorrow, we’ll explore **uniform crossover** in depth and implement it in C#. It’s a powerful alternative that doesn’t rely on fixed breakpoints, and it brings high diversity into your gene pool. Understanding crossover is the key to generating smarter offspring. Let’s evolve intelligently. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 6: Roulette, Tournaments, and Elites: Exploring Selection Strategies](https://www.woodruff.dev/day-6-roulette-tournaments-and-elites-exploring-selection-strategies/) **Published:** June 9, 2025 **Author:** Chris Woodruff **Excerpt:** Once you’ve calculated the fitness of each chromosome in your population, the next step in the genetic algorithm lifecycle is selection—deciding which chromosomes get to reproduce and which are left behind. Selection strategies play a crucial role in balancing exploration (searching new areas of the solution space) and exploitation (refining known good solutions). Choosing the right strategy affects the convergence speed and the overall effectiveness of your genetic algorithm. Today, we’ll explore the most common selection strategies: roulette wheel selection, tournament selection, and elitism. We’ll compare them and implement each one in C#. **Content:** Once you’ve calculated the fitness of each chromosome in your population, the next step in the genetic algorithm lifecycle is **selection**—deciding which chromosomes get to reproduce and which are left behind. Selection strategies play a crucial role in balancing **exploration** (searching new areas of the solution space) and **exploitation** (refining known good solutions). Choosing the right strategy affects the convergence speed and the overall effectiveness of your genetic algorithm. Today, we’ll explore the most common selection strategies: **roulette wheel selection**, **tournament selection**, and **elitism**. We’ll compare them and implement each one in C#. ## 1. Roulette Wheel Selection (Fitness Proportional) In roulette wheel selection, also known as fitness-proportionate selection, the probability of a chromosome being selected is directly proportional to its fitness. Think of it as a weighted lottery. Chromosomes with higher fitness get more “space” on the wheel. ### C# Implementation ``` public Chromosome RouletteSelection(List population) { int totalFitness = population.Sum(c => c.FitnessScore); int spin = Random.Shared.Next(0, totalFitness); int cumulative = 0; foreach (var chromosome in population) { cumulative += chromosome.FitnessScore; if (spin < cumulative) return chromosome; } return population[^1]; // fallback } ``` ### Pros - Easy to implement - Scales with population fitness ### Cons - Can become unstable if one chromosome dominates early - Low-performing chromosomes may never get selected ## 2. Tournament Selection Tournament selection picks a random subset of the population and selects the fittest among them. You can configure the tournament size to control selective pressure. ### C# Implementation ``` public Chromosome TournamentSelection(List population, int tournamentSize = 3) { var tournament = population.OrderBy(_ => Guid.NewGuid()) .Take(tournamentSize) .ToList(); return tournament.OrderByDescending(c => c.FitnessScore).First(); } ``` ### Pros - Simple and efficient - Robust against fitness scaling issues - Encourages diversity (when tournament size is small) ### Cons - Parameter-sensitive - Too much pressure can lead to premature convergence ## 3. Elitism Elitism ensures that the best chromosomes from the current generation are always preserved in the next one. This guarantees that your population never loses the most fit individuals due to random chance. ### C# Implementation ``` public List ApplyElitism(List population, int eliteCount) { return population.OrderByDescending(c => c.FitnessScore) .Take(eliteCount) .ToList(); } ``` You typically combine elitism with another selection method. For example, carry the top 2 chromosomes forward, then use tournament or roulette to fill the rest of the population. ### Pros - Preserves progress - Accelerates convergence ### Cons - Can reduce diversity if overused ## Combining Strategies Most production-level genetic algorithms combine these techniques: - Use **elitism** to preserve the top performers - Use **tournament** or **roulette** to fill the remaining population Here’s a high-level strategy: ``` var elites = ApplyElitism(population, 2); var children = new List(elites); while (children.Count < population.Count) { var parent1 = TournamentSelection(population); var parent2 = TournamentSelection(population); var child = parent1.Crossover(parent2); child.Mutate(0.01); children.Add(child); } ``` This approach keeps your evolutionary process guided and steady, without falling into stagnation or chaos. ## Choosing the Right Strategy StrategyBest When…RouletteFitness is well distributed across the populationTournamentYou want simplicity and diversityElitismYou need to preserve peak performanceExperimentation is key. What works well for a string evolution problem may not work for route optimization or machine learning parameter tuning. ## Up Next In tomorrow’s post, we’ll tie everything together and implement the full GA loop—initializing the population, running the fitness evaluation, applying selection, performing crossover and mutation, and evolving over generations. This is where your C# code starts to breathe and evolve on its own. Let the fittest rise. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 5: Natural Selection in Software: Implementing Fitness Functions](https://www.woodruff.dev/day-5-natural-selection-in-software-implementing-fitness-functions/) **Published:** June 7, 2025 **Author:** Chris Woodruff **Excerpt:** In the natural world, organisms survive and reproduce based on their ability to adapt to their environment. This principle of natural selection is central to the effectiveness of genetic algorithms. In software, our analog to survival is fitness—a quantitative measurement of how well a solution performs. Today, we focus on the role of fitness functions in guiding evolutionary progress in a genetic algorithm, and we’ll implement them in C# to evaluate and score candidate solutions effectively. **Content:** In the natural world, organisms survive and reproduce based on their ability to adapt to their environment. This principle of natural selection is central to the effectiveness of genetic algorithms. In software, our equivalent of survival is **fitness**, a quantitative measure of how well a solution performs. Today, we focus on the role of **fitness functions** in guiding evolutionary progress in a genetic algorithm, and we’ll implement them in C# to evaluate and score candidate solutions effectively. ## What Is a Fitness Function? A fitness function evaluates how “good” a chromosome is relative to the goal of your problem. It provides the only feedback loop the algorithm uses to determine which solutions to preserve, recombine, or discard. In other words, fitness functions define the environment in which natural selection occurs. In C# terms, the fitness function is typically implemented as a method that returns an `int` or `double`, with higher values indicating better solutions (though some problems invert this). ## Key Characteristics of a Good Fitness Function - **Objective**: Clearly aligned with the problem’s goals. - **Gradual**: Provides continuous feedback rather than binary success/failure. - **Discriminative**: Can meaningfully differentiate between competing solutions. - **Efficient**: Should execute quickly, since it’s called repeatedly across generations. ## Case Study: Matching a Target Phrase Let’s build a fitness function for the “evolve a string” example, where the goal is to transform a random string into a target like `"HELLO WORLD"`. ``` public int GetFitness(string target) { int score = 0; for (int i = 0; i < Genes.Length; i++) { if (Genes[i] == target[i]) { score++; } } return score; } ``` This function assigns one point for every character that matches the corresponding character in the target string. A perfect match receives the highest fitness. ## Using Levenshtein Distance (Advanced) For more robust fitness scoring in string problems, you could use **Levenshtein distance**, which measures how many edits are needed to transform one string into another. ``` public int LevenshteinDistance(string a, string b) { var costs = new int[b.Length + 1]; for (int j = 0; j **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 4: Designing Your First Chromosome Class in C#](https://www.woodruff.dev/day-4-designing-your-first-chromosome-class-in-c/) **Published:** June 6, 2025 **Author:** Chris Woodruff **Excerpt:** Now that we’ve explored the concept of genes and chromosomes in the context of genetic algorithms, it’s time to write some real code. Today’s goal is to design a reusable, extensible Chromosome class in C# that can serve as the foundation for solving optimization problems using genetic algorithms. We will not only model the chromosome itself, but also lay the groundwork for operations such as initialization, crossover, mutation, and evaluation. Think of this class as the central actor in your evolutionary simulation. **Content:** Now that we’ve explored the concept of genes and chromosomes in the context of genetic algorithms, it’s time to write some real code. Today’s goal is to design a reusable, extensible `Chromosome` class in C# that can serve as the foundation for solving optimization problems using genetic algorithms. We will not only model the chromosome itself, but also lay the groundwork for operations such as initialization, crossover, mutation, and evaluation. Think of this class as the central actor in your evolutionary simulation. ## Defining the Purpose A chromosome represents a candidate solution. It needs to encapsulate the data that defines that solution and provide mechanisms to modify and evaluate itself. For our initial implementation, we’ll assume that each gene is a `char` selected from a fixed gene pool, and the chromosome is trying to evolve toward a target string. This mirrors a common GA example: string evolution. It’s simple but powerful enough to illustrate all the necessary components. ## First Iteration: Basic Structure Let’s begin with a basic `Chromosome` class: ``` public class Chromosome { private static readonly string GenePool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ,!"; private static readonly Random Random = new Random(); public char[] Genes { get; private set; } public Chromosome(int length) { Genes = new char[length]; for (int i = 0; i < length; i++) { Genes[i] = RandomGene(); } } private char RandomGene() { return GenePool[Random.Next(GenePool.Length)]; } public string GetPhrase() { return new string(Genes); } public override string ToString() { return GetPhrase(); } } ``` This class gives us a randomly generated chromosome of a given length. Each gene is a character picked from a configurable gene pool. This is the genetic “DNA” of our solution. ## Adding Fitness Evaluation To guide the selection process, each chromosome must be evaluated for fitness. For string evolution, fitness can be defined as the number of characters that match the target string in the correct position. ``` public int GetFitness(string target) { int score = 0; for (int i = 0; i < Genes.Length; i++) { if (Genes[i] == target[i]) { score++; } } return score; } ``` This allows us to score and rank the population. The higher the score, the closer the chromosome is to the target. ## Supporting Crossover Next, we implement a crossover method to produce a child chromosome by mixing genes from two parents: ``` public Chromosome Crossover(Chromosome partner) { char[] childGenes = new char[Genes.Length]; int midpoint = Genes.Length / 2; for (int i = 0; i < Genes.Length; i++) { childGenes[i] = i < midpoint ? Genes[i] : partner.Genes[i]; } return new Chromosome(childGenes); } public Chromosome(char[] genes) { Genes = genes; } ``` This simple one-point crossover splits the genes at the midpoint. More advanced techniques, like two-point or uniform crossover, can be added later for more diversity. ## Adding Mutation Support Mutation prevents the population from becoming too homogenous and getting stuck in local optima. We add a method to randomly change some genes based on a mutation rate: ``` public void Mutate(double mutationRate) { for (int i = 0; i < Genes.Length; i++) { if (Random.NextDouble() < mutationRate) { Genes[i] = RandomGene(); } } } ``` Mutation rates are typically kept low—around 1 to 5 percent—to maintain stability while still exploring the solution space. ## Wrapping It Up Our final `Chromosome` class now supports: - Initialization with random genes - Conversion to string for display - Fitness scoring against a target - Crossover between parents - Mutation of random genes This is the engine behind your evolving code. Everything else in your genetic algorithm is the population, selection, and reproduction loop. We will build around this foundation. ## Up Next Tomorrow we will dive into fitness functions in more detail. You will learn how to craft scoring mechanisms that accurately reflect the goals of your problem and drive evolution in the right direction. With a chromosome class in place, your code is ready to evolve. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Day 3: Understanding Chromosomes, Genes, and DNA in Code](https://www.woodruff.dev/day-3-understanding-chromosomes-genes-and-dna-in-code/) **Published:** June 5, 2025 **Author:** Chris Woodruff **Excerpt:** At the heart of every genetic algorithm lies the concept of evolution, and at the heart of evolution lies DNA. For software developers, the equivalent building blocks are chromosomes and genes. If we want our applications to evolve solutions over time, we need a reliable way to encode, manipulate, and assess those building blocks in our C# programs. Today, we’ll take a closer look at how we can represent chromosomes and genes in C#, how to choose the right data structures, and how to build a model that is both flexible and performant. **Content:** At the heart of every genetic algorithm lies the concept of evolution, and at the heart of evolution lies DNA. For software developers, the equivalent building blocks are chromosomes and genes. If we want our applications to evolve solutions over time, we need a reliable way to encode, manipulate, and assess those building blocks in our C# programs. Today, we’ll take a closer look at how we can represent chromosomes and genes in C#, how to choose the right data structures, and how to build a model that is both flexible and performant. --- ## From Biology to Bytes In biology: - **Genes** encode traits like eye color or height. - **Chromosomes** are sequences of genes that together define an organism. - **DNA** is the underlying material, composed of sequences of base pairs. In genetic algorithms: - A **gene** is the smallest unit of information, usually a single value or decision. - A **chromosome** is a collection of genes representing one candidate solution. - The **DNA** of a solution is its full representation in code, often as a string, array, or object structure. Let’s take an example. Suppose we want to evolve a solution that generates the phrase “HELLO”. One possible chromosome might be a string of 5 characters. Each character represents a gene. --- ## Designing the Gene and Chromosome in C# While you can model a chromosome directly as a `string`, it is more powerful to create a dedicated `Chromosome` class. This enables encapsulation of behavior such as mutation, crossover, and fitness evaluation. Here’s a simple model: ``` public class Chromosome { public char[] Genes { get; private set; } private static Random _random = new Random(); private const string GenePool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ,!"; public Chromosome(int length) { Genes = new char[length]; for (int i = 0; i < length; i++) { Genes[i] = RandomGene(); } } public Chromosome(char[] genes) { Genes = genes; } private char RandomGene() { return GenePool[_random.Next(GenePool.Length)]; } public string GetPhrase() { return new string(Genes); } } ``` This structure represents our DNA as an array of characters. Each gene is a character selected from a gene pool. The constructor ensures that when a chromosome is created, it starts with a randomized set of genes. --- ## Alternative Representations Depending on the problem domain, the internal representation of genes may vary: - **Binary Arrays**: For low-level problems like circuit design or optimization, genes might be `bool[]`. - **Integers**: For numeric problems or ordering problems like the Traveling Salesperson Problem (TSP), `int[]` can represent cities or weights. - **Custom Objects**: For complex domains, each gene could be a class or struct with its own properties. Here is a numeric version for route optimization: ``` public class NumericChromosome { public int[] Genes { get; private set; } public NumericChromosome(int[] geneSequence) { Genes = geneSequence; } // Shuffle for random initialization public static NumericChromosome CreateRandom(int length) { var genes = Enumerable.Range(0, length).ToArray(); return new NumericChromosome(genes.OrderBy(_ => Guid.NewGuid()).ToArray()); } } ``` This approach is ideal when the order of genes matters, such as in scheduling or routing problems. --- ## Key Design Considerations When building chromosome and gene structures in C#, consider the following: - **Mutability**: Are genes fixed, or do you expect them to change often? Immutable structures make tracking changes easier, but mutable ones can improve performance. - **Fitness Evaluation**: Ensure the gene structure facilitates easy calculation of fitness. - **Cloning and Copying**: Each generation will involve duplicating chromosomes. Optimize for performance and correctness when copying gene sequences. You may also want to override `ToString()` to make logging and debugging easier: ``` public override string ToString() { return new string(Genes); } ``` --- ## Up Next Tomorrow, we’ll introduce the concept of fitness, how we measure which chromosomes are worth keeping and which need to be discarded. You’ll learn to implement a fitness function that can evaluate solutions and guide the evolutionary process in your C# codebase. Our digital DNA is now in place. Time to teach it what “fit” means. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Mechanical Minds and Human Folly: Why Fearin' AI is More Foolish Than Fruitful](https://www.woodruff.dev/mechanical-minds-and-human-folly-why-fearin-ai-is-more-foolish-than-fruitful/) **Published:** June 4, 2025 **Author:** Chris Woodruff **Excerpt:** It has come to my attention recently that many good folks have grown anxious over this new contrivance known as Artificial Intelligence. They eye these ingenious machines with suspicion, fearing we might soon become servants to mechanical overlords. Please permit me to offer a few thoughts on this subject, not to dispel your apprehensions outright but rather to restore a measure of good sense and clear perspective. **Content:** It has come to my attention recently that many good folks have grown anxious over this new contrivance known as Artificial Intelligence. They eye these ingenious machines with suspicion, fearing we might soon become servants to mechanical overlords. Please permit me to offer a few thoughts on this subject, not to dispel your apprehensions outright but rather to restore a measure of good sense and clear perspective. Artificial Intelligence, as far as I comprehend, which I must confess isn’t exceedingly far, is somewhat akin to a sophisticated calculator or perhaps a player piano that can produce music without a human hand to guide the keys. It can undoubtedly perform marvelous feats, crafting poetry, rendering art, and solving arithmetic quicker than a fleeing schoolboy. Yet, despite its remarkable abilities, it remains as devoid of true inventiveness as a wind-up bird repeating charming phrases without understanding a single syllable. You see, humanity’s real strength, and I declare this with utmost confidence, is not found in our ability to perform calculations or to regurgitate known facts; indeed, even an accountant’s ledger can achieve such tasks. No, our genuine talent, our true worth, resides in our limitless capacity for innovation and our insatiable desire to dream beyond established boundaries. Can Artificial Intelligence spontaneously conceive grand visions or design ingenious contraptions from nothing but necessity and determination? I think not. And what about humor, wit, and irony? Can an automated intellect craft a spontaneous joke that sends a crowd into fits of laughter, eyes watering and sides aching? It may imitate, certainly, but genuine originality is the sole dominion of humanity. We, dear friends, are the masters of surprise, ambassadors of absurdity, and peddlers of peculiar ingenuity. No array of gears, circuits, or digital magic can duplicate the unpredictable spark of human whimsy or those intuitive leaps that propel us into uncharted territories. Thus, let us not shrink in fear from these clever mechanical creations. Instead, let us boldly welcome them, as we have welcomed every remarkable invention before them, from the printing press to the steam engine. They may carry out our instructions swiftly and precisely, but true creativity remains a wild, elusive beast uniquely suited to human pursuit. Rather than worrying over obsolescence, let us stride confidently forward, forming new expectations and pursuing grander ideas far beyond even the most advanced contraptions’ abilities. Let us innovate courageously, create audaciously, and above all, dream extravagantly, for it is within our dreams, not within our fears, that humanity’s true greatness eternally resides. **Categories:** AI **Tags:** ai --- ### [Day 2: Evolution in Code: The Core Concepts](https://www.woodruff.dev/day-2-evolution-in-code-the-core-concepts/) **Published:** June 4, 2025 **Author:** Chris Woodruff **Content:** At their core, genetic algorithms are built on five foundational principles that closely resemble biological evolution: ### 1. **Genes and Chromosomes** In biology, genes are units of information, and chromosomes are structured collections of those genes. In GAs, a **chromosome** is a single candidate solution, typically represented as an array, list, or string. Each **gene** in the chromosome represents one aspect of the solution. For instance, in a string-matching problem, each character is a gene, and the entire string is the chromosome. ### 2. **Population** A population is simply a collection of chromosomes. Rather than working on one solution, a GA evaluates a diverse set of solutions in parallel. This diversity is what enables GAs to avoid local optima and explore the solution space more thoroughly. ### 3. **Fitness Function** The fitness function is the lens through which the algorithm views the world. It measures how “good” a solution is. It does not need to know the perfect answer, only how to rank the quality of candidate solutions. In C#, this is typically implemented as a method returning an integer or floating-point score. ``` public int Fitness(string target) { return Genes.Zip(target, (g, t) => g == t ? 1 : 0).Sum(); } ``` This simple example scores a string based on how many characters match the target. The higher the score, the more “fit” the chromosome. ### 4. **Selection** Once you’ve scored all chromosomes in the population, you need to decide which ones will pass their genes to the next generation. Selection mimics survival of the fittest: better solutions have a higher chance of being chosen to breed. Common strategies include: - **Roulette Wheel Selection** (probability weighted) - **Tournament Selection** (random subset competitions) - **Elitism** (carry over the best chromosomes unchanged) We will implement these in detail later in the series. ### 5. **Crossover and Mutation** Crossover is the process of combining genes from two parent chromosomes to create a new child. Mutation introduces randomness by slightly altering one or more genes in a chromosome. Together, these operations drive both **exploration** (mutation) and **exploitation** (crossover) of the solution space. Here is a quick crossover example: ``` public Chromosome Crossover(Chromosome partner) { int midpoint = Genes.Length / 2; string childGenes = Genes.Substring(0, midpoint) + partner.Genes.Substring(midpoint); return new Chromosome(childGenes); } ``` And a simple mutation implementation: ``` public Chromosome Mutate(double mutationRate) { var mutated = Genes.Select(c => _random.NextDouble() < mutationRate ? GenePool[_random.Next(GenePool.Length)] : c); return new Chromosome(new string(mutated.ToArray())); } ``` Mutation rates are typically small, ranging from 0.1% to 5%, depending on the domain. --- ## The GA Lifecycle Every genetic algorithm runs through a loop that looks like this: 1. **Initialize** a random population of chromosomes 2. **Evaluate** each chromosome using the fitness function 3. **Select** the best chromosomes to become parents 4. **Crossover** parents to produce children 5. **Mutate** the children randomly 6. **Replace** the old population with the new one 7. **Repeat** until a stopping condition is met (e.g., max generations or fitness threshold) This loop is both simple and powerful. Over time, poor solutions are discarded, decent solutions evolve into better ones, and the population converges on high-quality answers—even when the shape of the solution is not known in advance. --- ## Why It Works GAs are not guaranteed to find the perfect solution, but they often find good solutions when traditional methods fail. Their power comes from combining local search (mutation) with global recombination (crossover), guided by a measurable goal (fitness function). In domains where the search space is discontinuous, high-dimensional, or has many local optima, GAs offer a viable and surprisingly effective alternative to brute-force or greedy algorithms. --- ## Up Next In the next post, we will dive into modeling chromosomes and genes in C#. We’ll create a flexible `Chromosome` class that supports fitness evaluation, crossover, and mutation. By the end of the week, you’ll have a functioning genetic algorithm ready to solve a basic optimization problem. Evolution is coming to your codebase. Get ready. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [Rust Scalar and Compound Types: Where Are My C# Classes?](https://www.woodruff.dev/rust-scalar-and-compound-types-where-are-my-c-classes/) **Published:** April 16, 2025 **Author:** Chris Woodruff **Excerpt:** When I first started exploring Rust, one of my instincts was to reach for a class. You know the drill. Need to model some data? Write a class, slap a few properties on it, and maybe add a constructor or two. But Rust had other plans. On Day 6, I dug into Rust’s scalar and compound types and quickly realized that Rust doesn’t want you to start with classes. Instead, it hands you a small set of powerful primitives and says, “Let’s build something lean.” **Content:** When I first started exploring Rust, one of my instincts was to reach for a class. You know the drill. Need to model some data? Write a class, slap a few properties on it, and maybe add a constructor or two. But Rust had other plans. On Day 6, I dug into Rust’s scalar and compound types and quickly realized that Rust doesn’t *want* you to start with classes. Instead, it hands you a small set of powerful primitives and says, “Let’s build something lean.” Let’s take a look at how that plays out, especially through the eyes of a C# developer. ### Scalars: The Simple Stuff In C#, we have our familiar scalar types: ``` int x = 42; float pi = 3.14f; char c = 'A'; bool isReady = true; ``` Rust matches us almost one-for-one but requires more explicitness upfront. ``` let x: i32 = 42; let pi: f32 = 3.14; let c: char = 'A'; let is_ready: bool = true; ``` Rust supports: - Signed/unsigned integers (`i8` to `i128`, `u8` to `u128`) - Floating points (`f32`, `f64`) - `char` (which is a Unicode scalar, not a byte!) - `bool` Rust can also infer types most of the time: ``` let count = 10; // i32 by default let ratio = 0.5; // f64 by default ``` But if you’re coming from C#’s flexible `var`, Rust’s inference feels stricter; you’ll find yourself annotating types more often than not, especially in more complex scenarios. ### Tuples: Lightweight Data Bundles Here’s a quick reality check: no `Tuple` or deconstruction syntax required here. Rust tuples are first-class citizens: ``` let person = ("Chris", 42); ``` Want to access the values? ``` let name = person.0; let age = person.1; ``` Or destructure them (which feels very modern C#): ``` let (name, age) = person; ``` There’s no class, no struct, just a tuple carrying multiple values with different types. Quick, dirty, and highly useful when you don’t need a full data model. Compare that to C#: ``` var person = ("Chris", 42); var name = person.Item1; var age = person.Item2; ``` C#’s support for value tuples is decent. In Rust, it’s built right in and super ergonomic. ### Arrays and Slices: Familiar with a Twist C# arrays are flexible and garbage-collected: ``` int[] scores = new int[] { 10, 20, 30 }; ``` Rust’s arrays are fixed-size by default: ``` let scores: [i32; 3] = [10, 20, 30]; ``` The `[i32; 3]` type means “an array of three `i32`s.” You can also initialize them with repetition: ``` let zeros = [0; 5]; // same as [0, 0, 0, 0, 0] ``` To get the slice (similar to `Span` in .NET), you can reference it like this: ``` let part = &scores[1..3]; // gets [20, 30] ``` And yes, Rust will panic if you index out of bounds. But it does this at runtime with a very detailed error, which honestly still feels nicer than IndexOutOfRangeExceptions in C#. ### Wait, No Classes Yet? Nope. Not even structs at this point (that’s tomorrow). Rust really wants you to get comfortable with simple, efficient types before moving into more abstract modeling. And you know what? It works. C# makes it easy to jump to classes for everything. But Rust pushes you to start small—model your data with tuples or arrays, and only reach for structs when the pattern demands it. It’s a different mindset, and I kind of love it. ### Final Thoughts: Minimal Types, Maximum Power Rust’s scalar and compound types feel simple but there’s a surprising amount of power in that simplicity. You’re not juggling `object` or `dynamic`. There’s no `null`. You just get clean, lean, explicit values. Tomorrow, we graduate to *structs* and talk about how Rust models data without the ceremony of C# classes. Spoiler: No `public`, no `get; set;`, no inheritance, and I’m not even mad. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Day 1: The Survival of the Fittest Code: Why Learn Genetic Algorithms in C#?](https://www.woodruff.dev/day-1-the-survival-of-the-fittest-code-why-learn-genetic-algorithms-in-c/) **Published:** June 3, 2025 **Author:** Chris Woodruff **Excerpt:** What if you could write code that evolves? Not just code that runs, but code that iteratively improves its own solutions to complex problems without requiring you to handcraft every edge case. That’s the promise of genetic algorithms (GAs), an AI-inspired method rooted in Darwinian evolution, and it fits surprisingly well in the world of modern C# development. **Content:** What if you could write code that evolves? Not just code that runs, but code that iteratively improves its own solutions to complex problems without requiring you to handcraft every edge case. That’s the promise of genetic algorithms (GAs), an AI-inspired method rooted in Darwinian evolution, and it fits surprisingly well in the world of modern C# development. In this opening post of our [**5+ week (37 days) journey**](https://woodruff.dev/evolve-your-c-code-with-ai-a-5-week-genetic-algorithms-bootcamp-for-developers/), we’ll explore what makes genetic algorithms such a compelling tool for C# developers and why you might want to add this evolutionary technique to your problem-solving arsenal. --- ## Why Genetic Algorithms Matter Traditional programming excels when the solution path is well defined. You know the rules, you code them, and the system behaves predictably. But what about when the search space is vast, nonlinear, or not well understood? What if the problem is so complex that brute-force or greedy algorithms are too slow or ineffective? That’s where genetic algorithms shine. Inspired by natural selection, GAs simulate the process of evolution to find high-quality solutions to optimization and search problems, rather than relying on deterministic logic, GAs work by iterating over generations of possible solutions, mutating, recombining, and selecting the fittest until the population converges on a strong answer. This technique isn’t just a theoretical exercise. GAs are widely used in: - Route optimization (e.g., traveling salesperson problem) - Scheduling systems (e.g., employee shifts, task queues) - Game AI and procedural content generation - Hyperparameter tuning in machine learning - Financial forecasting and risk modeling C# and .NET provide the power, performance, and expressiveness needed to implement genetic algorithms cleanly and effectively, especially with features like strong typing, generics, and the Task Parallel Library (TPL) for parallelism. --- ## A Mental Model for Developers Think of genetic algorithms not as a black-box AI technique, but as a customizable framework for search and improvement. At its core, a genetic algorithm consists of five key parts: 1. **Chromosomes** – Representations of possible solutions (typically encoded as strings, arrays, or objects). 2. **Population** – A group of chromosomes representing diverse solutions. 3. **Fitness Function** – A function that evaluates how “good” a solution is. 4. **Selection** – A process to choose the best chromosomes for breeding. 5. **Crossover & Mutation** – Techniques to mix and slightly alter chromosomes to introduce diversity. Let’s say you’re evolving strings to match the phrase “Hello, World!”. Each chromosome might be a string of 13 characters, and your fitness function would score them based on how many characters match the target phrase. Here’s a conceptual example in C#: ``` public class Chromosome { public string Genes { get; private set; } private static Random _random = new Random(); private const string GenePool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ,!"; public Chromosome(int length) { Genes = new string(Enumerable.Range(0, length) .Select(_ => GenePool[_random.Next(GenePool.Length)]) .ToArray()); } public int Fitness(string target) { return Genes.Zip(target, (g, t) => g == t ? 1 : 0).Sum(); } public Chromosome Crossover(Chromosome other) { int midpoint = Genes.Length / 2; var childGenes = Genes.Substring(0, midpoint) + other.Genes.Substring(midpoint); return new Chromosome(childGenes); } public Chromosome(string genes) { Genes = genes; } public Chromosome Mutate(double mutationRate) { var mutated = Genes.Select(c => _random.NextDouble() < mutationRate ? GenePool[_random.Next(GenePool.Length)] : c); return new Chromosome(new string(mutated.ToArray())); } } ``` This isn’t production-ready, but it’s a foundation. Over the next few weeks, we’ll evolve this codebase into a full-fledged GA engine, complete with selection strategies, configurable parameters, and real-world problem solving. --- ## Why Now? Why C#? The .NET ecosystem has matured into a first-class platform for AI and high-performance computing. With C# 12, Span, parallelization via `Parallel.For`, and the reach of .NET across platforms, implementing performance-sensitive algorithms like GAs is more accessible than ever. You’re no longer constrained to using C++ or Python to work with advanced algorithms. With C#, you get: - High performance with just-in-time optimizations - Rich tooling and debugging support - Expressive syntax for modeling complex objects - Access to powerful libraries and cross-platform deployment So if you’ve ever felt that AI and evolutionary programming were out of reach, this series is your on-ramp, no PhD required. --- ## Coming Up Next In tomorrow’s post, we’ll break down what genetic algorithms are from a developer’s point of view and start designing our first real Chromosome class in C#. It’ll be hands-on and incremental, perfect for building your confidence and your codebase as we go. Let’s start evolving. **Categories:** Genetic Algorithms **Tags:** .NET, ai, C#, dotnet, genetic algorithms, programming --- ### [From C# to Rust: A 42-Day Developer Challenge](https://www.woodruff.dev/from-c-to-rust-a-42-day-developer-challenge/) **Published:** April 10, 2025 **Author:** Chris Woodruff **Excerpt:** I’ve spent over a decade writing C# and building solutions on .NET. But for six weeks, we will step outside the managed world of garbage collection and runtime JIT to dive headfirst into Rust—a systems programming language that promises performance, safety, and no nulls. Over the course of 42 days, we will learn something new about Rust every single day. We will fight the borrow checker. We will make mistakes. And I will blog it all from the perspective of a C# developer trying to make sense of it. **Content:** I’ve spent over a decade writing C# and building solutions on .NET. But for six weeks, we will step outside the managed world of garbage collection and runtime JIT to dive headfirst into Rust—a systems programming language that promises performance, safety, and no nulls. Over the course of 42 days, we will learn something new about Rust every single day. We will fight the borrow checker. We will make mistakes. And I will blog it all from the perspective of a C# developer trying to make sense of it. ## What to Expect - Real comparisons between Rust and C# - Lessons on ownership, pattern matching, traits, and lifetimes - Daily reflections from a .NET developer mindset ## Daily Breakdown 1. [Day 1: Why Rust? A C# Developer’s Journey Begins](https://woodruff.dev/why-rust-a-c-developers-journey-begins/) 2. [Day 2: Installing Rust: From dotnet new to cargo new](https://woodruff.dev/dotnet-new-meet-cargo-new-a-tale-of-two-clis/) 3. [Day 3: Hello, World! Rust vs C# Syntax](https://woodruff.dev/hello-rust-hello-world-rust-vs-c-syntax/) 4. [Day 4: Variables in Rust: Let Me Be Immutable](https://woodruff.dev/variables-in-rust-let-me-be-immutable/) 5. [Day 5: Functions in Rust: Familiar Yet Different](https://woodruff.dev/functions-in-rust-familiar-yet-different/) 6. [Day 6: Scalar and Compound Types: Where Are My Classes?](https://woodruff.dev/rust-scalar-and-compound-types-where-are-my-c-classes/) 7. [Day 7: Reflections on Week 1: Rust’s Minimalism Hits Different](https://woodruff.dev/reflections-on-week-1-rusts-minimalism-hits-different/) 8. [Day 8: Ownership: The Most C++-ish Thing I’ve Loved](https://woodruff.dev/ownership-in-rust-the-most-c-ish-thing-ive-loved-and-i-mean-that-in-a-good-way/) 9. [Day 9: Move Semantics: What Just Happened to My Variable?](https://woodruff.dev/move-semantics-in-rust-what-just-happened-to-my-variable/) 10. [Day 10: Borrowing and References: Rust’s Version of ref](https://woodruff.dev/borrowing-and-references-rusts-version-of-ref-but-nicer/) 11. [Day 11: The Borrow Checker: Rust’s Tough-Love Mentor](https://woodruff.dev/the-borrow-checker-rusts-tough-love-mentor/) 12. [Day 12: Slices and Strings: Goodbye StringBuilder?](https://woodruff.dev/slices-and-strings-goodbye-c-stringbuilder/) 13. [Day 13: Shadowing in Rust: Redeclaring with Style](https://woodruff.dev/shadowing-in-rust-redeclaring-with-style/) 14. [Day 14: Week 2 Reflection: Borrow Checker vs Garbage Collector](https://woodruff.dev/week-2-reflections-on-ownership-week-my-brain-hurts-in-a-good-way/) 15. [Day 15: Rust Structs vs C# Classes: Less is More](https://woodruff.dev/rust-structs-vs-c-classes-less-is-more/) 16. [Day 16: Enums: Discriminated Unions Done Right](https://woodruff.dev/enums-discriminated-unions-done-right/) 17. [Day 17: Match: Switch on Steroids](https://woodruff.dev/match-switch-on-steroids/) 18. [Day 18: Destructuring: Pattern Matching’s Power Move](https://woodruff.dev/destructuring-pattern-matchings-power-move/) 19. [Day 19: Option: Where Null Is Not an Option](https://woodruff.dev/optiont-where-null-is-not-an-option/) 20. [Day 20: Result: A Better Way to Fail](https://woodruff.dev/result-a-better-way-to-fail/) 21. [Day 21: Week 3 Wrap-Up: Data Modeling That Fights Back](https://woodruff.dev/week-3-wrap-up-data-modeling-that-fights-back/) 22. [Day 22: Organizing Code: Rust Modules vs C# Namespaces](https://woodruff.dev/organizing-code-rust-modules-vs-c-namespaces/) 23. [Day 23: Crates and Dependencies: NuGet, Meet Cargo](https://woodruff.dev/crates-and-dependencies-nuget-meet-cargo/) 24. [Day 24: Error Propagation with ?: So Simple, So Smar](https://woodruff.dev/error-propagation-with-so-simple-so-smart/)t 25. [Day 25: Panic! vs Exceptions: Stop the World or Handle It?](https://woodruff.dev/panic-vs-exceptions-stop-the-world-or-handle-it/) 26. [Day 26: Custom Errors: From Display to thiserror](https://woodruff.dev/custom-errors-from-display-to-thiserror/) 27. [Day 27: Logging in Rust: Tracing Without Console.WriteLine](https://woodruff.dev/logging-in-rust-tracing-without-console-writeline/) 28. [Day 28: Reflection on Errors and Modules](https://woodruff.dev/week-4-reflecting-on-errors-and-structure/) 29. [Day 29: Traits in Rust: Interfaces That Do More](https://woodruff.dev/traits-in-rust-interfaces-that-do-more/) 30. [Day 30: Trait Objects: Goodbye virtual, Hello dyn](https://woodruff.dev/trait-objects-goodbye-virtual-hello-dyn/) 31. [Day 31: Generics in Rust vs Generics in C#](https://woodruff.dev/generics-in-rust-vs-generics-in-c/) 32. [Day 32: Lifetimes: Surviving the First Encounter](https://woodruff.dev/lifetimes-surviving-the-first-encounter/) 33. [Day 33: Closures in Rust: Functional Vibes with a Twist](https://woodruff.dev/closures-in-rust-functional-vibes-with-a-twist/) 34. [Day 34: Iterators and Functional Combinators](https://woodruff.dev/iterators-and-functional-combinators/) 35. [Day 35: Reflection: Traits & Lifetimes—Power and Pain](https://woodruff.dev/week-5-reflecting-on-traits-lifetimes-power-and-pain/) 36. [Day 36: Building a CLI App in Rust: My First Project](https://woodruff.dev/building-a-cli-app-in-rust-my-first-project/) 37. [Day 37: Parsing Arguments and Writing Logic](https://woodruff.dev/parsing-arguments-and-writing-logic/) 38. [Day 38: Working with Files and the Filesystem](https://woodruff.dev/working-with-files-and-the-filesystem-in-rust/) 39. [Day 39: Writing Tests in Rust: Familiar and Fast](https://woodruff.dev/writing-tests-in-rust-familiar-and-fast/) 40. [Day 40: Packaging and Releasing a Rust CLI Tool](https://woodruff.dev/packaging-and-releasing-a-rust-cli-tool/) 41. [Day 41: Performance Check: Does Rust Really Fly?](https://woodruff.dev/performance-check-does-rust-really-fly/) 42. [Day 42: Final Reflections: What Rust Taught Me as a C# Dev](https://woodruff.dev/final-reflections-what-rust-taught-me-as-a-c-dev/) ## Final Recap Once you’re ready for the whole story, read the full recap: [Rust for the Sharp Mind: 6 Weeks of Learning Rust as a C# Developer](https://woodruff.dev/rust-for-the-sharp-mind-6-weeks-of-learning-rust-as-a-c-developer/). This journey will stretch our brains, challenge our assumptions, and ultimately make us better developers on any platform. I hope you will join me. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Performance Check: Does Rust Really Fly?](https://www.woodruff.dev/performance-check-does-rust-really-fly/) **Published:** May 21, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 41, and we are almost done! Today, we are putting Rust’s performance reputation to the test. Rust has a reputation for being fast. But how fast? If you have been living in the C# world where the JIT and garbage collector handle things for you this is a good chance to see how Rust stacks up when it comes to raw speed. **Content:** Welcome to Day 41, and we are almost done! Today, we are putting Rust’s performance reputation to the test. Rust has a reputation for being fast. But how fast? If you have been living in the C# world where the JIT and garbage collector handle things for you this is a good chance to see how Rust stacks up when it comes to raw speed. ## The Test Scenario Let us keep it simple. We will compare a Rust CLI app and a C# console app that both sum the numbers from 1 to 100 million. ## The C# Version Here is our basic C# example: ``` class Program { static void Main() { long sum = 0; for (long i = 1; i **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Final Reflections: What Rust Taught Me as a C# Dev](https://www.woodruff.dev/final-reflections-what-rust-taught-me-as-a-c-dev/) **Published:** May 22, 2025 **Author:** Chris Woodruff **Excerpt:** Day 42, and here we are. Six weeks of learning Rust from the perspective of a C# developer. We covered the basics, wrestled with ownership, danced with traits and lifetimes, and shipped a working CLI app. Along the way, there were moments of frustration, lightbulb moments, and more than a few “why is this so hard” conversations with the compiler. This final reflection is about stepping back and asking the big questions. What did Rust really teach me? What am I taking back to my C# projects? What might be next? **Content:** Day 42, and here we are. Six weeks of learning Rust from the perspective of a C# developer. We covered the basics, wrestled with ownership, danced with traits and lifetimes, and shipped a working CLI app. Along the way, there were moments of frustration, lightbulb moments, and more than a few “why is this so hard” conversations with the compiler. This final reflection is about stepping back and asking the big questions. What did Rust really teach me? What am I taking back to my C# projects? What might be next? ## Week 1: Getting Our Feet Wet We started with the basics. Hello World, variables, functions, and data types. Coming from C#, most of this felt familiar. But right away, Rust made its philosophy clear. Immutability is the default. You have to opt into mutability by explicitly using `mut`. That small change set the tone for how Rust wants you to think. Be deliberate. Be clear about your intent. C# lets you sprinkle in `readonly` where you feel like it. Rust makes you consider it every time you declare a variable. ## Week 2: Ownership and Borrowing Ownership and borrowing were the first significant mental challenges. As a C# developer, you let the garbage collector handle memory. Rust forced me to confront questions I usually avoid. - Who owns this value? - When does it go away? - Can I safely share it? This week was tough. The borrow checker and I had some heated arguments. But every error message was a gentle (or not so gentle) reminder that Rust is keeping me honest. I came away from this week with a deeper understanding and respect for memory management. Even though C# handles this for me, I now see opportunities to be more mindful about when I allocate, copy, or share data. ## Week 3: Structs, Enums, and Data Modeling This was the week when Rust started to win me over. Structs were straightforward, but enums felt like a superpower. Discriminated unions in Rust are more flexible than C# enums and feel closer to what F# offers. Instead of building class hierarchies with inheritance, Rust lets you model your data directly: ``` enum PaymentMethod { CreditCard { number: String, cvv: String }, Paypal { email: String }, WireTransfer { iban: String }, } ``` Pattern matching with `match` makes the logic safe and straightforward. No casting. No guessing. The compiler makes sure I handle every case. This approach prompted me to reconsider some of the instances where I utilize class hierarchies in C#. Could I use simpler data models with clearer state transitions? Probably. ## Week 4: Modules, Crates, and Error Handling This was the week when I began to appreciate Rust’s emphasis on explicitness. Modules and visibility rules are not just about organization. They enforce API boundaries. Error handling with `Result` felt like a big shift from C#’s try-catch world. Instead of hoping you catch every exception, Rust makes you handle failure right at the function signature: ``` fn read_file(path: &str) -> Result { fs::read_to_string(path) } ``` This approach makes error handling part of the design, not just an afterthought. Back in C#, I am now thinking more about where exceptions should be used and where returning explicit results might lead to clearer code. ## Week 5: Traits, Generics, and Lifetimes This was the heavy week. Traits felt familiar coming from interfaces in C#, but with some extra flexibility. I loved how traits allow shared behavior without inheritance chains. Lifetimes were the real workout. It was uncomfortable at first, but the more I worked with them, the more I understood why they exist. They are not meant to make my life difficult. They are about making sure my code is correct. C# shields me from most of these concerns, but I now have a deeper understanding of what the GC handles for me. It also made me appreciate `ref` and `Span` in a new light. ## Week 6: Building and Shipping We built a CLI app. We packaged it. We benchmarked it. The tooling around Rust is excellent. Cargo handles project setup, dependency management, testing, and release builds all in one place. Compared to the .NET CLI and NuGet, it felt lean and focused. I appreciated how easy it was to write tests right alongside my code and run them with `cargo test`. The performance tests confirmed what I had heard: Rust is fast, like really fast. But they also reinforced that performance comes from thoughtful design, not just the language itself. ## What I’m Taking Back to C# - Be more explicit about mutability - Consider modeling with enums and pattern matching where it makes sense - Rethink error handling and be more deliberate about where exceptions are appropriate - Appreciate what the GC does, but also know when to think about allocations - Write more tests alongside my code, not as an afterthought - Lean into the power of structs and readonly types ## What’s Next? I am not giving up C#. However, I am bringing a bit of the Rust mindset back with me. This journey expanded my perspective on safety, design, and performance. I will probably continue to explore Rust for CLI tools and system code, where it excels. And who knows, maybe a side project or two will graduate from C# to Rust just for fun. Thanks for following along on this journey. Whether you are a die-hard C# fan curious about Rust or already dabbling in both, I hope this series helped bridge the gap between these two awesome languages. See you out there and happy coding! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Packaging and Releasing a Rust CLI Tool](https://www.woodruff.dev/packaging-and-releasing-a-rust-cli-tool/) **Published:** May 20, 2025 **Author:** Chris Woodruff **Excerpt:** Day 40, and today we are looking at how to package and release your Rust CLI app. You have written the code, added argument parsing, handled the logic, and even written tests. Now it is time to get that shiny CLI tool into the hands of others. This process will feel familiar if you have worked with .NET global tools. Rust’s cargo makes it easy to build, release, and share your command-line apps. **Content:** Day 40, and today we are looking at how to package and release your Rust CLI app. You have written the code, added argument parsing, handled the logic, and even written tests. Now it is time to get that shiny CLI tool into the hands of others. This process will feel familiar if you have worked with .NET global tools. Rust’s `cargo` makes it easy to build, release, and share your command-line apps. ## Building Your CLI Tool The first step is to build your project. By default, Cargo builds in debug mode, but you can create an optimized release build like this: ``` cargo build --release ``` This creates the binary in the `target/release/` folder: ``` target/release/my_cli_app ``` The release build is smaller and faster because it includes optimizations. Compare this to C#, where you would run: ``` dotnet build -c Release ``` Same idea. Build your project in release mode for production use. ## Installing Locally with cargo install If you want to install your CLI tool globally on your own machine, you can use: ``` cargo install --path . ``` This installs the binary into Cargo’s global binary directory, usually ~/.cargo/bin. Make sure that the folder is in your system PATH and you can run your CLI tool from anywhere. Compare this to .NET’s global tools: ``` dotnet tool install --global MyTool ``` Both approaches let you install a local or published tool and make it available system-wide. ## Sharing Your Binary with Others Once you have your release binary built, you can share it directly. Just send the binary file to your users. On Linux and macOS, it is ready to go. On Windows, remember to share the `.exe`. If you want to automate building for multiple platforms, check out tools like `cross` or GitHub Actions to build on CI pipelines. You can also publish your CLI app to crates.io if it is a library or to GitHub Releases if you want to distribute prebuilt binaries. ## Adding Metadata for Packaging Make sure your `Cargo.toml` includes helpful metadata like this: ``` [package] name = "my_cli_app" version = "0.1.0" authors = ["Your Name "] edition = "2021" ``` \[dependencies\] clap = { version = “4.0”, features = \[“derive”\] } This information shows up when users run `--version` or `--help` if you set up your CLI with `clap` correctly. ## Example of –version Output ``` my_cli_app --version my_cli_app 0.1.0 ``` Cargo uses this info from your `Cargo.toml`. No extra wiring needed. ## Why This Approach Feels Good - Cargo handles building and installing out of the box - Release builds are optimized and ready for production - Local installs make testing and usage easy - Packaging feels similar to .NET global tools ## Wrapping It Up Releasing a Rust CLI tool feels smooth and intentional. With Cargo, you build, optimize, and install easily. Whether you use direct distribution or automation tools, sharing binaries with others is straightforward. Tomorrow, we will look at benchmarking and performance checking to see if Rust really flies. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Writing Tests in Rust: Familiar and Fast](https://www.woodruff.dev/writing-tests-in-rust-familiar-and-fast/) **Published:** May 19, 2025 **Author:** Chris Woodruff **Excerpt:** Onward to Day 39. Today, we're discussing testing in Rust. If you are a C# developer, you have probably spent time with xUnit, NUnit, or MSTest. You know the usual [TestMethod] or [Fact] attributes and Assert.Equal calls. Rust’s testing system is going to feel pretty familiar with a bit of Rust flair. **Content:** Onward to Day 39. Today, we’re discussing testing in Rust. If you are a C# developer, you have probably spent time with xUnit, NUnit, or MSTest. You know the usual `[TestMethod]` or `[Fact]` attributes and `Assert.Equal` calls. Rust’s testing system is going to feel pretty familiar with a bit of Rust flair. ## The Basic Rust Test In Rust, writing tests is built right into the language and toolchain. No extra test runner needed. Just use the `#[test]` attribute and the `assert_eq!` macro. Example: ``` pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 3), 5); } } ``` The `#[cfg(test)]` block tells Rust to only compile these tests when running tests, not in normal builds. To run your tests: ``` cargo test ``` ## Compare to xUnit or NUnit In C#, your test might look like this with xUnit: ``` public class MathTests { [Fact] public void Add_ReturnsSum() { Assert.Equal(5, Add(2, 3)); } public int Add(int a, int b) => a + b; } ``` The feel is the same. Annotate your test method, assert your expectations, and let the framework run your tests. ## Other Assertions Rust gives you a few handy macros for testing: - `assert!`: Checks that a condition is true - `assert_eq!`: Checks that two values are equal - `assert_ne!`: Checks that two values are not equal Example: ``` #[test] fn test_positive_number() { let value = 10; assert!(value > 0); } ``` ## Testing Error Conditions You can also check for expected panics using `#[should_panic]`: ``` #[test] #[should_panic(expected = "division by zero")] fn test_divide_by_zero() { let _ = 1 / 0; } ``` Compare that to C#’s `[ExpectedException]` in older MSTest or `Assert.Throws` in xUnit: ``` [Fact] public void DivideByZero_ThrowsException() { Assert.Throws(() => { var result = 1 / 0; }); } ``` ## Organizing Tests Rust lets you group tests inside modules. You can also create a `tests` folder at the root of your project for integration tests. Example unit test organization: ``` #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-2, -3), -5); } } ``` Integration test example: ``` my_project/ ├── src/ │ └── lib.rs ├── tests/ │ └── integration_test.rs ``` Inside `integration_test.rs`: ``` use my_project::add; #[test] fn test_add_integration() { assert_eq!(add(5, 7), 12); } ``` Run all tests with: ``` cargo test ``` ## Why Rust’s Testing Feels Smooth - Built into Cargo with zero setup - No extra dependencies required - Fast compile and test cycles - Clear assertion macros ## Wrapping It Up Testing in Rust feels familiar if you come from the .NET world but with the benefit of being built right into the language and its package manager. You get fast feedback and easy organization without extra tooling. Tomorrow, we will discuss packaging and releasing your CLI app, making it easy to share your work. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Working with Files and the Filesystem in Rust](https://www.woodruff.dev/working-with-files-and-the-filesystem-in-rust/) **Published:** May 18, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 38. Today, we're getting our hands dirty with file I/O. Reading and writing files is one of those tasks every app must perform at some point. If you have written C# code using System.IO this is going to feel familiar but with a Rust twist. **Content:** Welcome to Day 38. Today, we’re getting our hands dirty with file I/O. Reading and writing files is one of those tasks every app must perform at some point. If you have written C# code using `System.IO` this is going to feel familiar but with a Rust twist. ## Reading Files in C#: The Classic Approach In C# reading a file might look like this: ``` string content = File.ReadAllText("config.txt"); Console.WriteLine(content); ``` Simple and effective. The `System.IO` namespace gives you helpers like `ReadAllText`, `WriteAllText`, and streams when you need more control. ## Reading Files in Rust: std::fs::read\_to\_string Rust has similar helpers in `std::fs`. Here is how you read a file into a string: ``` use std::fs; use std::io; fn main() -> io::Result { let content = fs::read_to_string("config.txt")?; println!("{}", content); Ok(()) } ``` The `?` operator automatically handles the `Result` returned by `read_to_string`. If the file is missing or there is an error, the function returns early with the error. Compare that to C#, where you need a `try/catch` block to handle the exception. ## Writing Files In C#: ``` File.WriteAllText("output.txt", "Hello World"); ``` In Rust: ``` use std::fs; use std::io; fn main() -> io::Result { fs::write("output.txt", "Hello World")?; println!("File written successfully"); Ok(()) } ``` Again the `?` operator keeps error handling clean and clear. ## Appending to a File Rust gives you `OpenOptions` for more control, just like C# uses `FileStream` and its options. In C#: ``` using (var writer = File.AppendText("log.txt")) { writer.WriteLine("Log entry"); } ``` In Rust: ``` use std::fs::OpenOptions; use std::io::Write; use std::io; fn main() -> io::Result { let mut file = OpenOptions::new() .append(true) .create(true) .open("log.txt")?; writeln!(file, "Log entry")?; Ok(()) } ``` Same idea with clear options for appending or creating the file if it does not exist. ## Checking if a File Exists In C#: ``` if (File.Exists("data.txt")) { Console.WriteLine("File exists"); } ``` In Rust: ``` use std::path::Path; fn main() { if Path::new("data.txt").exists() { println!("File exists"); } else { println!("File not found"); } } ``` Rust uses `Path` to work with file paths and check for existence. ## Creating Directories C# example: ``` Directory.CreateDirectory("logs"); ``` Rust: ``` use std::fs; use std::io; fn main() -> io::Result { fs::create_dir_all("logs")?; println!("Directory created"); Ok(()) } ``` The `create_dir_all` function makes sure the entire directory path exists just like `Directory.CreateDirectory`. ## Why Rust’s Approach Works Well - Compile-time safety with `Result` handling - Clear options for file modes and behaviors - Flexible path handling with `Path` and `PathBuf` - Easy to plug into Rust’s iterator and error-handling systems ## Wrapping It Up Working with the filesystem in Rust feels familiar if you come from C#, but with a focus on safety and explicit error handling. Instead of relying on exceptions, Rust uses the type system to ensure that you handle every case up front. Next time, we will move into writing tests for our Rust projects to keep our code solid and trustworthy. See you there! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Parsing Arguments and Writing Logic in Rust](https://www.woodruff.dev/parsing-arguments-and-writing-logic/) **Published:** May 17, 2025 **Author:** Chris Woodruff **Excerpt:** We are up to Day 37, and today, we are continuing to build out our Rust CLI app. Last time, we set up a simple command-line tool using the clap crate. Now, it is time to dig a little deeper into parsing arguments, handling input validation, and structuring our logic cleanly. If you are coming from the C# world, this is where you would probably set up your Program.cs to parse args[], maybe use a library like CommandLineParser, and then branch out into your application logic. Rust gives you similar tools but with its own flavor. **Content:** We are up to Day 37, and today, we are continuing to build out our Rust CLI app. Last time, we set up a simple command-line tool using the clap crate. Now, it is time to dig a little deeper into parsing arguments, handling input validation, and structuring our logic cleanly. If you are coming from the C# world, this is where you would probably set up your Program.cs to parse args\[\], maybe use a library like CommandLineParser, and then branch out into your application logic. Rust gives you similar tools but with its own flavor. ## Adding More Arguments and Options Here is where we left off last time: ``` use clap::Parser; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// The name of the person to greet name: String, /// Adds excitement #[arg(short, long)] excited: bool, } fn main() { let cli = Cli::parse(); if cli.excited { println!("HELLO, {}!!!", cli.name.to_uppercase()); } else { println!("Hello, {}.", cli.name); } } ``` Now, let us say we want to add a `repeat` option, so the user can specify how many times to print the message. This means adding another argument: ``` use clap::Parser; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { name: String, #[arg(short, long)] excited: bool, /// Number of times to repeat the message #[arg(short, long, default_value_t = 1)] repeat: u8, } fn main() { let cli = Cli::parse(); for _ in 0..cli.repeat { if cli.excited { println!("HELLO, {}!!!", cli.name.to_uppercase()); } else { println!("Hello, {}.", cli.name); } } } ``` Now you can run it like this: ``` cargo run -- Woody --excited --repeat 3 ``` Output: ``` HELLO, WOODY!!! HELLO, WOODY!!! HELLO, WOODY!!! ``` ## Handling Validation Sometimes, you want to ensure that the input makes sense. For example, what if the user enters a negative number or a huge value? `clap` has validation built in. You can restrict the range with `value_parser`: ``` #[arg(short, long, value_parser = clap::value_parser!(u8).range(1..=10))] repeat: u8, ``` This limits the repeat value between 1 and 10. If the user tries anything outside that range, the CLI will reject it with a friendly message. ## Structuring Logic Cleanly Just like in C#, you want to avoid stuffing all your logic into `main`. Break out the work into separate functions. ``` fn print_greeting(name: &str, excited: bool, repeat: u8) { for _ in 0..repeat { if excited { println!("HELLO, {}!!!", name.to_uppercase()); } else { println!("Hello, {}.", name); } } } fn main() { let cli = Cli::parse(); print_greeting(&cli.name, cli.excited, cli.repeat); } ``` This keeps `main` as your entry point and moves the business logic into its own function. Cleaner and easier to test. ## Why This Approach Works Well in Rust - Clear argument parsing with built-in validation - No messy manual parsing or switch statements - Easy to add help output and usage instructions - Logic stays organized and easy to maintain Rust’s type system and `clap` macros work together to make your CLI apps robust and easy to build. ## Wrapping It Up Today, you learned how to go beyond basic argument parsing and handle input validation while keeping your logic structured and readable. Next, we will work with files and the filesystem, adding some real-world usefulness to our CLI project. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Building a CLI App in Rust: My First Project](https://www.woodruff.dev/building-a-cli-app-in-rust-my-first-project/) **Published:** May 16, 2025 **Author:** Chris Woodruff **Excerpt:** It's day 36, and today, we are shifting gears from theory to practice. It is time to roll up our sleeves and build something. If you have ever built a command-line tool in C# using .NET Console apps, you will feel right at home. Rust has the same capability with a few extras that make the experience feel pretty slick. **Content:** It’s day 36, and today, we are shifting gears from theory to practice. It is time to roll up our sleeves and build something. If you have ever built a command-line tool in C# using .NET Console apps, you will feel right at home. Rust has the same capability with a few extras that make the experience feel pretty slick. ## Why Build a CLI Tool? CLI apps are a great way to learn Rust because they let you focus on small functional pieces without worrying about GUIs or frameworks. Plus, typing a command into the terminal and seeing your code respond is deeply satisfying. ## Getting Started with cargo new First up, we create the project using Cargo, Rust’s package manager and build tool: ``` cargo new my_cli_app cd my_cli_app ``` Cargo sets up the directory with a `Cargo.toml` file and a `src/main.rs`. This is your starting point. ## Meet clap: The CLI Toolkit for Rust The `clap` crate is the go-to solution for parsing command-line arguments in Rust. It stands for Command Line Argument Parser and makes setting up CLI interfaces easy and reliable. Add it to your `Cargo.toml`: ``` [dependencies] clap = { version = "4.5.38", features = ["derive"] } ``` Now you can start defining commands and arguments with the `clap` macros. ## Defining Basic Commands Here is an example of a simple CLI that greets the user: ``` use clap::Parser; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// The name of the person to greet name: String, } fn main() { let cli = Cli::parse(); println!("Hello, {}!", cli.name); } ``` When you run this app: ``` cargo run -- Woody ``` It prints: ``` Hello, Woody! ``` That is the basic idea. Define your CLI structure with a struct. Use attributes to set up help text and argument parsing. Let `clap` handle the rest. ## Adding More Options You can easily add flags and options: ``` use clap::Parser; #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// The name of the person to greet name: String, /// Adds excitement #[arg(short, long)] excited: bool, } fn main() { let cli = Cli::parse(); if cli.excited { println!("HELLO, {}!!!", cli.name.to_uppercase()); } else { println!("Hello, {}.", cli.name); } } ``` Run it like this: ``` cargo run -- Woody --excited ``` Output: ``` HELLO, WOODY!!! ``` ## Why Use clap Instead of Manually Parsing? If you have ever written manual `args[]` parsing logic in C#, you know how painful it can be. String arrays, positional arguments, lots of switch statements. Clap does the heavy lifting for you and helps you with output and argument validation out of the box. ``` cargo run -- --help ``` Gives you: ``` Usage: my_cli_app [OPTIONS] Arguments: The name of the person to greet Options: -e, --excited Adds excitement -h, --help Print help -V, --version Print version ``` ## Wrapping It Up Building a CLI app in Rust is a perfect first project. It teaches you how to structure a project, use dependencies like `clap` and get immediate feedback from the terminal. Next time, we will expand on this, explore argument parsing in more depth, and start handling real work with input and output. See you there! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Lifetimes: Surviving the First Encounter](https://www.woodruff.dev/lifetimes-surviving-the-first-encounter/) **Published:** May 12, 2025 **Author:** Chris Woodruff **Excerpt:** Day 32, and today we are stepping into one of Rust’s most talked-about features. If you have heard scary stories about lifetimes, do not worry. This is your first encounter, and we are going to make it approachable. If you are coming from C#, the idea of lifetimes might feel strange at first. In .NET you lean on the garbage collector to handle memory cleanup. You create objects, and when they are no longer needed, the GC comes along and takes out the trash. Rust does not have a garbage collector. It uses ownership and borrowing to manage memory. Lifetimes are how Rust keeps track of how long references are valid. They are the glue that ties Rust’s memory safety model together. **Content:** Day 32, and today we are stepping into one of Rust’s most talked-about features. If you have heard scary stories about lifetimes, do not worry. This is your first encounter, and we are going to make it approachable. If you are coming from C#, the idea of lifetimes might feel strange at first. In .NET you lean on the garbage collector to handle memory cleanup. You create objects, and when they are no longer needed, the GC comes along and takes out the trash. Rust does not have a garbage collector. It uses ownership and borrowing to manage memory. Lifetimes are how Rust keeps track of how long references are valid. They are the glue that ties Rust’s memory safety model together. ## The GC Approach in C#: Forget About It In C# you might write: ``` public string GetFirstChar(string input) { return input.Substring(0, 1); } ``` No need to think about when `input` goes away. The GC handles it. ## The Rust Way: Be Explicit In Rust, you can write a function that returns a reference to part of a string: ``` fn first_char(s: &str) -> &str { &s[0..1] } ``` But Rust wants to make sure that the reference it returns does not outlive the original string. If it did, you would have a dangling reference, which leads to undefined behavior. To tell Rust how the input and output references relate to each other, you use lifetimes: ``` fn first_char &'a str { &s[0..1] } ``` Here `'a` is the lifetime parameter. It says that the output reference lives at least as long as the input reference. ## What Does That Mean in Practice? Think of lifetimes like a library checkout system. If you borrow a book, you can read it, but you have to return it before the library closes. Rust uses lifetimes to ensure that you never end up with a reference to a book that has already been returned to the shelf. In C#, the GC might clean up the book behind your back. In Rust, the compiler will not even let you compile if your reference might outlive its source. ## When Do You See Lifetimes? Lifetimes show up most often when: - You are returning references from functions - You have structs that hold references - You are working with generic functions and references together Here is an example with a struct: ``` struct Book` ties the lifetime of `title` to the lifetime of `name`. Rust ensures that `book` cannot outlive `name`. ## But What About Lifetime Elision? Sometimes Rust is smart enough to figure out lifetimes for you. This is called lifetime elision. In simple functions like: ``` fn first_char(s: &str) -> &str { &s[0..1] } ``` Rust applies lifetime elision rules and inserts the lifetime annotations for you. But when things get more complex or ambiguous you need to spell it out explicitly. ## Why This Is a Good Thing It might feel like extra work up front but lifetimes prevent a whole class of memory bugs at compile time. No null references no dangling pointers no guesswork about whether your object is still alive. In C# you rely on the GC. In Rust the compiler is your safety net. ## Wrapping It Up Lifetimes are not here to scare you. They are here to help you write safe and predictable code. They make sure your references stay valid and your memory stays clean. With a little practice lifetimes become part of the rhythm of writing Rust. Tomorrow we will dig into closures and see how Rust handles function-like behavior with a twist. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Week 5: Reflecting on Traits & Lifetimes Power and Pain](https://www.woodruff.dev/week-5-reflecting-on-traits-lifetimes-power-and-pain/) **Published:** May 15, 2025 **Author:** Chris Woodruff **Excerpt:** You've made it to Day 35, and we're wrapping up Week 5. This week was all about Rust’s generics, contracts, and lifetimes. If your brain feels like it has been doing deadlifts, that means you are doing it right. Traits and lifetimes bring a lot of power to Rus,t but they can also make your head spin if you are used to the more relaxed rules of C#. Let us take a moment to look back on what we covered and how Rust compares to what you might be used to with C#. **Content:** You’ve made it to Day 35, and we’re wrapping up Week 5. This week was all about Rust’s generics, contracts, and lifetimes. If your brain feels like it has been doing deadlifts, that means you are doing it right. Traits and lifetimes bring a lot of power to Rust, but they can also make your head spin if you are used to the more relaxed rules of C#. Let’s take a moment to review what we covered in the last 7 days and compare Rust to what you might be used to with C#. ## Traits vs Interfaces: Rust Brings the Muscle C# gives you interfaces to define shared behavior. Rust gives you traits. On the surface, they feel the same, but Rust’s traits are more flexible because they work without inheritance. You can implement a trait for any type, not just your own. ``` pub trait Logger { fn log(&self, message: &str); } pub struct ConsoleLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("{}", message); } } ``` Compare that to C#: ``` public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } } ``` So far so similar but Rust pushes it further with blanket implementations and trait bounds on generics which are not as easy to pull off in C# without inheritance chains or reflection. ## Trait Objects and dyn: Controlled Flexibility In C#, the default is dynamic dispatch through virtual methods and interfaces. Rust makes you opt into it with `dyn Trait` and trait objects. This gives you explicit control over when you pay the cost of dynamic dispatch. ``` let loggers: Vec = vec![ Box::new(ConsoleLogger), ]; ``` In C# this is just standard interface behavior: ``` List loggers = new List { new ConsoleLogger() }; ``` Rust makes you work a little harder, but rewards you with clear boundaries between static and dynamic dispatch. ## Generics: Constraint Clarity C# generics use constraints like `where T : IDisposable`. Rust uses trait bounds like `T: Display`. Both approaches work, but Rust ties the constraints into the type system with zero runtime overhead thanks to monomorphization. ``` fn print_item(item: T) { println!("{}", item); } ``` In C#: ``` public void PrintItem(T item) where T : IFormattable { Console.WriteLine(item); } ``` Rust gives you predictability at compile time while C# leans on the JIT to specialize generics for value types and erase them for reference types. ## Lifetimes: Brain Benders with a Purpose Thanks to the garbage collector, C# developers get to avoid thinking about how long objects live. Rust says not so fast and introduces lifetimes. It forces you to think about how references relate to each other and how long they can exist. ``` fn longest y.len() { x } else { y } } ``` There is no direct equivalent in C# because the GC handles memory lifetimes. But that also means you cannot catch use-after-free or dangling reference bugs until runtime. Rust catches these at compile time. ## Where Rust Hurts More Than Helps Let us be honest. Lifetimes can feel painful, especially when you are new to Rust. Sometimes the compiler seems like it is speaking in riddles, and you only want to return a reference from a function. There are also moments when trait bounds get tricky, and the need to specify lifetimes or type annotations makes code harder to read, especially in complex generic scenarios. In C#, the GC and dynamic dispatch smooth over these rough edges. In Rust, you trade convenience for safety. ## Wrapping Up Week 5 This week was about leaning into Rust’s strengths while also recognizing where it asks more of you. Traits give you flexible and powerful contracts. Lifetimes protect your memory safety. Generics enforce your type promises at compile time. It is not always easy, but it is safe. And when it clicks, it feels like a superpower. Next week, we will start putting everything together by building real applications. Get ready to shift from learning the tools to using them. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Rust Iterators and Functional Combinators](https://www.woodruff.dev/iterators-and-functional-combinators/) **Published:** May 14, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 34, and today we are taking a stroll through one of the most satisfying parts of Rust. If you're a C# developer who loves LINQ, this will feel like home, with just enough Rust flavor to keep it interesting. **Content:** Welcome to Day 34, and today we are taking a stroll through one of the most satisfying parts of Rust. If you’re a C# developer who loves LINQ, this will feel like home, with just enough Rust flavor to keep it interesting. ## LINQ in C#: The Gold Standard In C#, you probably know and love LINQ. You can chain together operations like `Select`, `Where`, and `ToList` to transform and filter collections elegantly. ``` var numbers = new List { 1, 2, 3, 4, 5 }; var evens = numbers.Where(n => n % 2 == 0).Select(n => n * n).ToList(); ``` LINQ enables you to work with IEnumerable and chain operators without needing to write manual loops. ## Rust’s Iterator Pattern: Same Idea, Different Wrapping Rust has iterators and it offers similar functional combinators like `map`, `filter`, and `collect`. Here is the same example in Rust: ``` let numbers = vec![1, 2, 3, 4, 5]; let evens: Vec = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect(); println!("{:?}", evens); // [4, 16] ``` The vibe is the same. Chain methods transform the collection. The difference is how Rust enforces ownership and borrowing while keeping things fast and safe. ## Iterators in Rust: Lazy Evaluation Just like LINQ Rust iterators are lazy. That means the operations like `map` and `filter` do not actually run until you call something like `collect` or `for_each`. ``` let numbers = vec![1, 2, 3, 4, 5]; let iter = numbers.iter().map(|n| n * n); // Nothing happens yet for n in iter { println!("{}", n); // Now the squares are calculated } ``` ## Collect: Turning Iterators into Collections In LINQ you call `ToList` or `ToArray` to materialize the results. Rust uses `collect` which can create different types of collections depending on what you ask for: ``` let squares: Vec = (1..=5).map(|n| n * n).collect(); println!("{:?}", squares); // [1, 4, 9, 16, 25] ``` The type annotation on `squares` tells Rust what to build from the iterator. ## More Fun with Iterators Here are some other handy methods: - `sum`: Adds up the values - `count`: Counts the elements - `any` and `all`: Check conditions across the elements Example: ``` let numbers = vec![1, 2, 3, 4, 5]; let total: i32 = numbers.iter().sum(); println!("Total: {}", total); // Total: 15 let has_even = numbers.iter().any(|n| *n % 2 == 0); println!("Contains even number: {}", has_even); // true ``` ## Why Rust’s Iterators Are Awesome - Lazy by default - Zero-cost abstractions with no runtime overhead - Ownership and borrowing baked into the design - Powerful chaining without allocating unless you choose to collect Rust iterators give you the functional expressiveness of LINQ while staying true to Rust’s philosophy of safety and performance. ## Wrapping It Up If you enjoy LINQ you will love Rust’s iterators. They let you work with data in a clean readable way without giving up control over performance. Tomorrow we will reflect on the week and talk about the power and challenges of traits and lifetimes. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Closures in Rust: Functional Vibes with a Twist](https://www.woodruff.dev/closures-in-rust-functional-vibes-with-a-twist/) **Published:** May 13, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 33, and today we are jumping into closures. If you have been writing C# for a while, you are no stranger to lambdas, delegates, and maybe even expression trees. Rust has closures, too, and they bring a nice functional flavor to the language with some unique Rust twists. **Content:** Welcome to Day 33, and today we are jumping into closures. If you have been writing C# for a while, you are no stranger to lambdas, delegates, and maybe even expression trees. Rust has closures, too, and they bring a nice functional flavor to the language with some unique Rust twists. ## Lambdas and Delegates in C#: The Familiar Territory In C#, you have probably written something like this a thousand times: ``` Func square = x => x * x; Console.WriteLine(square(5)); // 25 ``` Or passed a lambda into LINQ: ``` var evens = numbers.Where(n => n % 2 == 0); ``` C# lets you define small anonymous functions with lambdas and use them wherever a delegate or expression tree is expected. ## The Rust Way: Closures with Capture In Rust, closures look very similar at first glance: ``` let square = |x: i32| x * x; println!("{}", square(5)); // 25 ``` You use the `| |` syntax to define parameters; the body comes after the arrow or directly if it is an expression. But here is the twist. Rust closures can automatically capture variables from their environment. This makes them feel more like C# lambdas with closures rather than just plain delegates. Example: ``` let factor = 2; let multiply = |x: i32| x * factor; println!("{}", multiply(5)); // 10 ``` The closure grabs `factor` from the surrounding scope without needing you to pass it explicitly. ## Closures and Traits: Fn, FnMut, FnOnce Traits power Rust’s closures. Three main traits describe what kind of access the closure needs to its environment: - `Fn`: Takes arguments by reference, does not mutate captured variables - `FnMut`: Can mutate captured variables - `FnOnce`: Consumes captured variables and can only be called once You do not have to specify these most of the time. The compiler figures it out based on what your closure does. But you can add these traits explicitly when needed, especially in generic functions. Example of a closure that mutates captured state: ``` let mut count = 0; let mut increment = || { count += 1; println!("Count: {}", count); }; increment(); increment(); ``` This closure implements `FnMut` because it modifies `count`. ## Comparing to Expression Trees In C#, expression trees are often used when you need to compile or analyze the structure of a lambda rather than execute it: ``` Expression expr = x => x * x; ``` Rust does not have a direct equivalent of expression trees in the same way. Instead, Rust favors higher-order functions and generics to handle most functional use cases. If you want to build something like an expression tree, you would define your own enums and interpret them manually. ## Returning Closures from Functions In Rust, closures have anonymous types, so if you want to return a closure from a function, you usually have to use `impl Fn` or `Box`. Example: ``` fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 { move |x| x * factor } let multiply_by_3 = make_multiplier(3); println!("{}", multiply_by_3(10)); // 30 ``` Notice the use of `move`. This tells Rust to move ownership of captured variables into the closure. ## Why Closures Feel Familiar Yet Different - You get the same flexibility as C# lambdas - Rust’s capture behavior feels closer to C# closures - You explicitly choose when a closure consumes its environment - Type inference handles most of the heavy lifting for closure types Closures in Rust combine the convenience of functional programming with Rust’s usual safety guarantees. They let you write flexible, composable code without giving up control over memory and lifetimes. ## Wrapping It Up Closures are one of those tools that feel right at home if you have written C# lambdas, but they bring a little extra flavor when you add Rust’s traits and ownership model into the mix. Tomorrow, we will look at iterators and functional combinators, such as map and filter. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Generics in Rust vs Generics in C#](https://www.woodruff.dev/generics-in-rust-vs-generics-in-c/) **Published:** May 11, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 31, and today we are diving into generics. If you are coming from C# this is probably familiar ground. You know and love List, Dictionary, and all the type-safe goodness that generics bring to your code. Rust has generics too but with a twist. The syntax might look similar but Rust’s approach gives you some extra control and a few new things to think about. **Content:** Welcome to Day 31. Today, we’re diving into generics. If you are coming from C# this is probably familiar ground. You know and love List, Dictionary, and all the type-safe goodness that generics bring to your code. Rust has generics too but with a twist. The syntax might look similar but Rust’s approach gives you some extra control and a few new things to think about. ## Generic Basics: The Same but Different Let us start with the basic syntax comparison. In C# you might write something like: ``` public class Box { public T Value { get; set; } public Box(T value) { Value = value; } } ``` In Rust the equivalent looks like this: ``` pub struct Box { value: T, } impl Box { pub fn new(value: T) -> Self { Box { value } } } ``` So far so good. Both languages let you define types and functions that work with any type. ## Adding Constraints: Traits vs where T : Interface In C# you often constrain generics like this: ``` public void Process(T item) where T : IDisposable { item.Dispose(); } ``` In Rust you use trait bounds: ``` pub fn process(item: T) { println!("{}", item); } ``` Rust even lets you write the constraint separately with a `where` clause just like C#: ``` pub fn process(item: T) where T: std::fmt::Display, { println!("{}", item); } ``` The trait system in Rust is roughly equivalent to interfaces in C#. They both define shared behavior that can be enforced through generics. ## Variance and Ownership One of the subtle differences between Rust and C# generics is variance. In C# you can mark interfaces and delegates with `out` and `in` to control covariance and contravariance. ``` public interface IProducer { T Produce(); } ``` Rust handles variance through its ownership model and lifetime system. There is no `out` or `in` keyword. Instead variance is inferred based on how types are used and the borrow checker ensures safety. This makes variance feel a bit more automatic but it also means you sometimes need to think about lifetimes and references when using generics in Rust. ## Type Inference: Helpful in Both Both C# and Rust provide type inference to make using generics easier. In C# you might rely on `var` to avoid specifying the generic type explicitly. ``` var box = new Box(42); ``` In Rust the compiler often infers types for you as well: ``` let box = Box::new(42); ``` If Rust cannot infer the type it will tell you and you can specify it explicitly: ``` let box: Box = Box::new(42); ``` ## Compile Time Guarantees One of Rust’s big strengths is that generic constraints are checked at compile time with no runtime overhead. Because Rust uses monomorphization it generates a concrete version of the code for each type you use. This is similar to how C++ templates work. In C# generics use a type-erased model for reference types and JIT specialization for value types. This makes C# generics flexible but sometimes slower at runtime for certain scenarios. ## Wrapping It Up Generics in Rust feel familiar if you come from C#. The syntax lines up well and the core ideas are the same. The differences come down to how constraints are expressed with traits instead of interfaces and how Rust leans on the borrow checker and lifetimes to manage safety. Tomorrow we will step into lifetimes and see how Rust makes sure your references never outlive their welcome. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Trait Objects: Goodbye virtual, Hello dyn](https://www.woodruff.dev/trait-objects-goodbye-virtual-hello-dyn/) **Published:** May 10, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 30 and today we are going to explore how Rust handles dynamic dispatch with trait objects. If you are used to the world of C# this is the part where you usually reach for abstract classes or virtual methods. Maybe you sprinkle in some interfaces and let polymorphism do the heavy lifting at runtime. In Rust dynamic dispatch works a little differently and it is all thanks to dyn. **Content:** Welcome to Day 30 and today we are going to explore how Rust handles dynamic dispatch with trait objects. If you are used to the world of C# this is the part where you usually reach for `abstract` classes or `virtual` methods. Maybe you sprinkle in some interfaces and let polymorphism do the heavy lifting at runtime. In Rust dynamic dispatch works a little differently and it is all thanks to `dyn`. ## Virtual and Abstract in C#: The Classic Approach In C# you might define an abstract base class like this: ``` public abstract class Animal { public abstract void Speak(); } public class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } } public class Cat : Animal { public override void Speak() { Console.WriteLine("Meow!"); } } ``` Then at runtime you can work with the base class reference and the correct method is called via the virtual table: ``` Animal pet = new Dog(); pet.Speak(); // Woof! ``` ## The Rust Way: Trait Objects and dyn Rust does not have classes or virtual methods but it has traits and something called **trait objects**. A trait object lets you store different types that implement the same trait and call methods on them using dynamic dispatch. Here is the same idea in Rust: ``` pub trait Animal { fn speak(&self); } pub struct Dog; impl Animal for Dog { fn speak(&self) { println!("Woof!"); } } pub struct Cat; impl Animal for Cat { fn speak(&self) { println!("Meow!"); } } ``` Instead of using an abstract base class we can use `dyn Animal`: ``` fn main() { let pets: Vec = vec![ Box::new(Dog), Box::new(Cat), ]; for pet in pets { pet.speak(); } } ``` The key here is `Box`. This tells Rust to store a pointer to a type that implements `Animal` but resolve the actual method calls at runtime. ## Static vs Dynamic Dispatch Rust loves static dispatch because it means the compiler can optimize everything at compile time. But when you need flexibility Rust lets you choose dynamic dispatch explicitly with `dyn`. - **Static dispatch**: Chosen at compile time using generics and trait bounds - **Dynamic dispatch**: Chosen at runtime using `dyn Trait` Compare this to C# where dynamic dispatch is the default when you use virtual methods or interfaces. ## When to Reach for dyn Use `dyn Trait` when: - You need to work with different types that implement the same behavior - You do not know the concrete type at compile time - You want to store a collection of mixed types Stick with generics and trait bounds when: - You want maximum performance - You know the concrete types at compile time - You do not need runtime flexibility ## Trait Objects Cannot Do Everything Not all traits can be turned into trait objects. Traits must be **object safe** to be used with `dyn`. That means: - No generic methods in the trait definition - Methods cannot return `Self` If your trait does not meet these rules Rust will let you know at compile time. ## Wrapping It Up Trait objects and `dyn` give Rust a way to support dynamic dispatch without the overhead of a full object-oriented system. You get just enough flexibility to share behavior across types when you need it but without sacrificing Rust’s usual focus on safety and performance. Next up we will explore generics in Rust and how they compare to generics in C#. See you tomorrow! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Traits in Rust: Interfaces That Do More](https://www.woodruff.dev/traits-in-rust-interfaces-that-do-more/) **Published:** May 9, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 29 where we are stepping into one of Rust’s most powerful features. If you are a C# developer think of traits as interfaces but with some serious upgrades. Traits in Rust define shared behavior just like interfaces do in C#. But Rust’s approach feels more flexible and composable. **Content:** Welcome to Day 29 where we are stepping into one of Rust’s most powerful features. If you are a C# developer think of traits as interfaces but with some serious upgrades. Traits in Rust define shared behavior just like interfaces do in C#. But Rust’s approach feels more flexible and composable. ## Interfaces in C#: The Starting Point In C# you are used to defining interfaces like this: ``` public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } } ``` The interface defines the contract. Any class that implements the interface agrees to provide those methods. This is solid and works well but it can get cumbersome when you start layering on inheritance or trying to compose behavior across unrelated types. ## Enter Traits in Rust Rust’s version of interfaces is called a trait. Here is the same idea in Rust: ``` pub trait Logger { fn log(&self, message: &str); } pub struct ConsoleLogger; impl Logger for ConsoleLogger { fn log(&self, message: &str) { println!("{}", message); } } ``` The syntax feels similar but here is where Rust starts to shine. Traits can be implemented for structs enums or any type. No inheritance chain required. ## Traits and Generics: Power Couple Traits in Rust work beautifully with generics. You can use trait bounds to ensure that a generic type implements certain behavior. ``` fn process(logger: &T) { logger.log("Processing order"); } fn main() { let logger = ConsoleLogger; process(&logger); } ``` This is like adding `where T : ILogger` in C# but with tighter integration into the type system. ``` public void Process(T logger) where T : ILogger { logger.Log("Processing order"); } ``` The difference is that in Rust the trait system is deeply tied to the compiler’s ability to optimize and enforce correctness. ## Default Implementations Another neat feature of traits is that you can provide default implementations for methods. Interfaces in C# gained something similar with default interface methods but Rust has had this baked in from the start. ``` pub trait Logger { fn log(&self, message: &str) { println!("[Default Logger] {}", message); } } ``` This allows you to define common behavior while still letting specific types override it when needed. ## Blanket Implementations Rust lets you implement a trait for any type that meets certain conditions. This is called a blanket implementation. ``` impl Logger for T { fn log(&self, message: &str) { println!("{}: {}", self, message); } } ``` You cannot really do this in C# without reflection or heavy use of base classes. Rust makes it part of the language. ## Why Traits Feel More Flexible - Traits are not tied to inheritance chains - They work with enums structs and more - They compose well with generics and type bounds - The compiler enforces trait usage at compile time with zero runtime cost This approach pushes you toward composition instead of inheritance. It keeps your code modular and lets behavior be shared without forcing a strict object hierarchy. ## Wrapping It Up Traits give Rust a way to share behavior that feels familiar to C# developers but with a lot more flexibility. They pair perfectly with generics and encourage you to think about behavior and capability instead of class hierarchies. Tomorrow we will explore trait objects and how Rust handles dynamic dispatch. See you there! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Week 4: Reflecting on Errors and Structure](https://www.woodruff.dev/week-4-reflecting-on-errors-and-structure/) **Published:** May 8, 2025 **Author:** Chris Woodruff **Excerpt:** Day 28 marks the end of Week 4 and it is time to pause for a quick reflection. We have covered a lot of ground this week from organizing Rust code with modules and crates to handling errors with grace instead of chaos. Coming from a C# background this week might have felt both familiar and refreshingly different. **Content:** Day 28 marks the end of Week 4 and it is time to pause for a quick reflection. We have covered a lot of ground this week from organizing Rust code with modules and crates to handling errors with grace instead of chaos. Coming from a C# background this week might have felt both familiar and refreshingly different. ## Rust Keeps You Honest If there is one theme that runs through Rust’s approach to error handling and code organization it is this: Rust keeps you honest. It makes sure you declare your intentions clearly and handle failure up front rather than hoping everything goes smoothly and catching surprises at runtime. In C# we are used to the flexibility of exceptions. You can throw any object that inherits from `Exception` and catch it wherever you want or not at all. This leads to custom exception hierarchies with classes like `UserNotFoundException` or `InvalidOrderException`. That works but it often leaves error handling as an afterthought. Exceptions bubble up through layers and unless you are deliberate about your catch blocks you might miss important failure points. Rust flips this story. Errors are part of the function signature. They are right there in your face: ``` fn read_file(path: &str) -> Result { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } ``` This is not optional. If something can fail the function signature tells you and the compiler makes sure you handle it. ## Structure Matters When it comes to organizing code C# gives you namespaces and assemblies. You can sprinkle classes across folders and rely on conventions to keep things sane. Rust demands more structure with its module system. Your folder layout directly impacts how you structure your code. ``` // src/main.rs mod services; fn main() { services::process(); } ``` ``` // src/services.rs pub fn process() { println!("Processing service logic"); } ``` You cannot just toss things anywhere and hope the compiler figures it out. Modules and visibility rules make your structure explicit. In C# you might declare something `internal` or `public`. In Rust everything is private by default and you use `pub` to opt into visibility. It nudges you toward deliberate API boundaries. ## Composability Over Inheritance C# leans on object-oriented patterns like inheritance and interfaces to share behavior. Rust takes a more composable approach. Instead of base classes you use traits and generics to define behavior. Instead of class hierarchies you use enums and pattern matching to handle different states. ``` enum OrderStatus { Pending, Shipped, Delivered, Cancelled, } fn print_status(status: OrderStatus) { match status { OrderStatus::Pending => println!("Order is pending"), OrderStatus::Shipped => println!("Order has shipped"), OrderStatus::Delivered => println!("Order delivered"), OrderStatus::Cancelled => println!("Order cancelled"), } } ``` This makes state handling explicit and the compiler helps ensure you do not miss a case. No base classes no inheritance chains just clear data models and behavior. ## Rust Makes You Slow Down In a Good Way One of the takeaways from this week is that Rust asks you to slow down and be intentional. You cannot skip error handling. You cannot hide behind inheritance. You have to design your data flow and error paths clearly. And while that might feel like extra work up front it pays off when you revisit your code months later. The structure is not just for the compiler it is there to help you and your team keep things maintainable and understandable. ## Wrapping Up Week 4 This week highlighted one of Rust’s biggest strengths. It nudges you toward making better choices without forcing you into heavy patterns. It lets you design small focused pieces that compose well together. Next week we will get into traits generics lifetimes and closures. Buckle up because we are about to take the flexibility of Rust’s type system for a spin. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Logging in Rust: Tracing Without Console.WriteLine](https://www.woodruff.dev/logging-in-rust-tracing-without-console-writeline/) **Published:** May 7, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 27 and let’s talk about how Rust handles logging the smart way. If you have been in the .NET world for any amount of time, you are probably used to ILogger. You know the drill: inject a logger, use it throughout your code, and stay away from the quick and dirty Console.WriteLine scattershot approach. Rust takes a similar path but with its own flavor. It gives you the log and tracing crates, both designed to keep your code clean while still giving you rich, structured log output when you need it. **Content:** Welcome to Day 27, and let’s talk about how Rust handles logging the smart way. If you have been in the .NET world for any amount of time, you are probably used to `ILogger`. You know the drill: inject a logger, use it throughout your code, and stay away from the quick and dirty `Console.WriteLine` scattershot approach. Rust takes a similar path but with its own flavor. It gives you the `log` and `tracing` crates, both designed to keep your code clean while still giving you rich, structured log output when you need it. ## Why Not Just println! Sure, you could just sprinkle some `println!` statements all over your Rust code: ``` println!("Processing order: {}", order_id); ``` It works fine for quick debugging. But the moment your project grows or you want different log levels or outputs, you are going to wish you had something better. This is where structured logging comes in. ## Meet the log Crate The `log` crate provides a standard API for logging. It defines macros like `trace!`, `debug!`, `info!`, `warn!`, and `error!`. But it does not decide where the logs go. You can plug in whatever logging backend you like. Example: ``` use log::{info, warn}; fn main() { env_logger::init(); // Set up a basic logger backend info!("Application started"); warn!("This is your warning log"); } ``` To control the log level, just set an environment variable when running your app: ``` RUST_LOG=info cargo run ``` Compare that to .NET: ``` public class OrderService { private readonly ILogger _logger; public OrderService(ILogger logger) { _logger = logger; } public void ProcessOrder(int orderId) { _logger.LogInformation("Processing order {OrderId}", orderId); } } ``` The concepts match up nicely. Both give you level-based logging and keep the log configuration separate from the code that generates the logs. ## Leveling Up with tracing For more advanced logging needs, Rust offers the `tracing` crate. Think of it as the structured logging and observability solution. It supports things like spans, fields, and subscribers. It is built for async and multithreaded environments where you need to track context across function calls. Example using `tracing`: ``` use tracing::{info, instrument}; use tracing_subscriber; #[instrument] fn process_order(order_id: u32) { info!(order_id, "Processing order"); } fn main() { tracing_subscriber::fmt::init(); process_order(42); } ``` The `#[instrument]` macro automatically captures function arguments as structured log fields. This gives you much richer log data without any extra typing. Compare this to .NET’s structured logging with scopes: ``` using (_logger.BeginScope("OrderId: {OrderId}", orderId)) { _logger.LogInformation("Processing order"); } ``` Both approaches help add meaningful context to your logs so they are not just strings floating around in the dark. ## Separation of Concerns One of the best parts of Rust’s logging ecosystem is how it separates log generation from log handling. Your code focuses on generating events and data. The logging backend handles where those logs go. You can log to the console today and switch to writing JSON logs to a file tomorrow without touching your core logic. In .NET, you get a similar benefit through dependency injection and logger providers. Rust accomplishes this through the flexibility of its logger implementations. ## Wrapping It Up Rust makes it easy to avoid the bad habit of littering your code with println statements. With `log` and `tracing`, you get structured, scalable logging that feels right at home if you are coming from the .NET world. Whether you are just spinning up a CLI tool or building a production-grade service, Rust’s logging approach gives you the power to keep your logs clean and your debugging stress-free. Tomorrow we will reflect on error handling and the structure we have built so far. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Custom Errors: From Display to thiserror](https://www.woodruff.dev/custom-errors-from-display-to-thiserror/) **Published:** May 6, 2025 **Author:** Chris Woodruff **Excerpt:** Look at this! Day 26, and today we’re talking about one of my favorite things to not ignore: error messages. Because when things go sideways (and they will), your future self and your users will appreciate error handling that’s clear, structured, and helpful. **Content:** Look at this! Day 26, and today we’re talking about one of my favorite things to *not* ignore: error messages. Because when things go sideways (and they will), your future self and your users will appreciate error handling that’s clear, structured, and helpful. If you’re coming from C#, you’re used to building custom exception classes when the built-in ones just don’t cut it: ``` public class UserNotFoundException : Exception { public UserNotFoundException(string username) : base($"User '{username}' was not found.") { } } ``` In Rust, we do something similar, but instead of throwing exceptions, we define **custom error types** that work with the `Result` system. And with a little help from the fantastic `thiserror` crate, it’s easier than you might expect. ## Custom Error Types: The Basics Here’s how you might define a basic error type in Rust: ``` use std::fmt; #[derive(Debug)] pub enum MyError { NotFound(String), InvalidInput(String), } impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MyError::NotFound(item) => write!(f, "{} not found", item), MyError::InvalidInput(reason) => write!(f, "Invalid input: {}", reason), } } } impl std::error::Error for MyError {} ``` This gives you structured, meaningful errors that play nicely with `Result`. But writing `Display` and `Error` by hand every time? A bit tedious. ## Enter `thiserror`: The Easy Button The `thiserror` crate automates most of the boilerplate, keeping your error definitions clean and readable: ``` use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error("{0} not found")] NotFound(String), #[error("Invalid input: {0}")] InvalidInput(String), } ``` That’s it. No manual `impl Display`. No manual `impl Error`. `thiserror` handles it all. ## Using Custom Errors in Practice Here’s how you might use that custom error in a function: ``` fn find_user(username: &str) -> Result { if username == "woodydev" { Ok(String::from("User found!")) } else { Err(MyError::NotFound(username.to_string())) } } fn main() { match find_user("unknown") { Ok(message) => println!("{}", message), Err(e) => println!("Error: {}", e), } } ``` ## Compared to C# Exception Hierarchies In C#, custom exceptions are about creating class hierarchies and relying on inheritance: ``` try { throw new UserNotFoundException("woodydev"); } catch (UserNotFoundException ex) { Console.WriteLine(ex.Message); } ``` The downside? Exceptions are runtime beasts, and they need to be caught explicitly and forgetting to catch the right one can lead to unpredictable behavior. In Rust, because errors are values, you can: - Compose them. - Wrap them. - Chain them. - Handle them *at compile time*. It feels more like working with actual data instead of handling side effects. ## Chaining Errors with `#[from]` Another slick feature of `thiserror` is easy error conversion with the `#[from]` attribute: ``` use std::io; use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error("IO error: {0}")] Io(#[from] io::Error), #[error("Invalid data provided")] InvalidData, } ``` Now, anytime a function returns an `io::Error`, Rust can automatically convert it into your `MyError::Io` variant using the `?` operator. No glue code needed. ## Wrapping It Up Rust’s approach to custom errors is all about making your error handling explicit, structured, and, dare I say, pleasant. With enums, pattern matching, and handy tools like `thiserror`, you get the flexibility of detailed error types without the pain of hand-rolling exception hierarchies. Next up, we’re looking at logging in Rust because when things go wrong, it’s nice to leave a paper trail. See you there! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Panic! vs Exceptions: Stop the World or Handle It?](https://www.woodruff.dev/panic-vs-exceptions-stop-the-world-or-handle-it/) **Published:** May 5, 2025 **Author:** Chris Woodruff **Excerpt:** Okay, we're on Day 25, and today we’re stepping into the world of failure again. But this time, it’s the catastrophic kind. We’re not talking about the "file didn’t open" kind of error. Nope, we’re talking about "game over, stop everything, hit the eject button" failure. In .NET, you’re familiar with exceptions. In Rust, there’s something called panic!. But these two aren’t quite the same thing. Rust draws a hard line between recoverable errors and unrecoverable failures, and understanding that line is a big mindset shift for anyone coming from C#. **Content:** ``` fn main() { panic!("Something went terribly wrong!"); } ``` Okay, we’re on Day 25, and today we’re stepping into the world of failure again. But this time, it’s the catastrophic kind. We’re not talking about the “file didn’t open” kind of error. Nope, we’re talking about “game over, stop everything, hit the eject button” failure. In .NET, you’re familiar with exceptions. In Rust, there’s something called **panic!**. But these two aren’t quite the same thing. Rust draws a hard line between **recoverable errors** and **unrecoverable failures**, and understanding that line is a big mindset shift for anyone coming from C#. ## C# Exceptions: One-Size-Fits-All Failure In C#, exceptions handle just about everything that goes wrong: ``` try { var number = int.Parse("not a number"); } catch (FormatException ex) { Console.WriteLine($"Oops: {ex.Message}"); } ``` Whether it’s a minor issue or a catastrophic failure, C# throws an exception. You decide if and where to catch it. But because *everything* is an exception, from a file not found to an out-of-memory error, it can be hard to tell whether you should handle it or let the app crash. That’s where Rust takes a different approach. ## Rust’s Two Types of Failure In Rust, failure comes in two flavors: 1. **Recoverable Errors:** Things that might go wrong, but you can handle (e.g., file not found, bad input). This is where `Result` shines. 2. **Unrecoverable Errors:** Things that should never happen (e.g., index out of bounds, logic bug, critical assumptions violated). This is where **panic!** steps in. ## Meet panic! Panic in Rust is an intentional, immediate crash. It’s like Rust throwing its hands up and saying, “Nope, this is not okay, I’m out.” Example: ``` fn main() { let numbers = [1, 2, 3]; println!("Number: {}", numbers[5]); // panic! here: index out of bounds } ``` Or explicitly: When panic happens, Rust **unwinds the stack** by default, cleaning up resources as it goes. You can also configure Rust to abort immediately if you prefer. ## But Wait! Isn’t That Like Throwing an Exception? Yes… but with clearer intent. In Rust: - If it returns `Result`, you’re expected to handle it. - If it panics, it’s because the program hit a state that should *never* happen. Compare that to C#, where the difference between something like `IOException` and `OutOfMemoryException` can feel blurry unless you’re intentionally splitting catch blocks: ``` try { // risky work } catch (IOException ex) { // recoverable } catch (OutOfMemoryException ex) { // probably shouldn't even try to recover throw; } ``` In Rust, that separation is built into the design. ## Should You Ever Catch a Panic? Technically, yes, Rust does provide a way to catch panics with `std::panic::catch_unwind`. But the philosophy is: **don’t try to recover from panics unless you absolutely have to** (like in a test harness or maybe an embedded system). In .NET terms, think of panics more like `Environment.FailFast()` than exceptions. They’re the “nuke it from orbit” option. ## Fault Domains and Failure Boundaries C# developers often talk about **fault domains,** sections of your system that can fail without bringing down the whole app. You might catch exceptions at a service boundary and return a graceful error response. Rust’s equivalent is pushing you to use `Result` at those boundaries. Panics are reserved for true logic errors, not for I/O failure or bad user input. ## Wrapping It Up Rust’s philosophy is pretty clear: - **Recoverable problems? Use** `Result`**.** - **Unrecoverable issues? Let it panic.** By keeping these two paths separate, Rust helps you write code that’s safer, easier to reason about, and less likely to blow up in production from unexpected “oops, we forgot to catch that” moments. Next up, we’ll look at crafting your own custom error types in Rust. Because when things *do* go wrong, let’s at least make the error messages helpful! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Error Propagation with ?: So Simple, So Smart](https://www.woodruff.dev/error-propagation-with-so-simple-so-smart/) **Published:** May 4, 2025 **Author:** Chris Woodruff **Excerpt:** Day 24... today we’re digging into one of my favorite "why doesn’t every language have this?" features in Rust: the ? operator. If you've spent any time in C#, you're no stranger to the good ol’ try/catch flow. You wrap some code in a try, you catch the exception, and you hope you didn’t forget to check for null or some unexpected state. Rust takes a different approach with Result and the magic of the ? operator. It keeps your error handling clean, readable, and safe without the overhead (or drama) of exceptions. **Content:** Day 24… today we’re digging into one of my favorite “why doesn’t every language have this?” features in Rust: the `?` operator. If you’ve spent any time in C#, you’re no stranger to the good old `try/catch` flow. You wrap some code in a `try`, you catch the exception, and you hope you didn’t forget to check for null or some unexpected state. Rust takes a different approach with `Result` and the magic of the `?` operator. It keeps your error handling clean, readable, and safe without the overhead (or drama) of exceptions. ## The Usual Suspects: C# try/catch Let’s start with what you know: ``` try { var data = File.ReadAllText("config.txt"); Console.WriteLine(data); } catch (IOException ex) { Console.WriteLine($"Failed to read file: {ex.Message}"); } ``` It works. But it’s noisy. And if you’re calling multiple methods that could throw, your `try/catch` blocks can quickly become a tangle of indentation and exception juggling. ## Rust’s Way: Result + ? = Clean In Rust, instead of exceptions, functions return `Result`. And instead of wrapping every call in a match or if statement, you can use the `?` operator to automatically return early if there’s an error. Here’s a function that reads a file and propagates errors up the call stack: ``` use std::fs::File; use std::io::{self, Read}; fn read_file_contents(path: &str) -> Result { let mut file = File::open(path)?; // if this fails, the error is returned immediately let mut contents = String::new(); file.read_to_string(&mut contents)?; // same here Ok(contents) } fn main() { match read_file_contents("config.txt") { Ok(data) => println!("File contents: {}", data), Err(e) => println!("Failed to read file: {}", e), } } ``` No try, no catch, no drama. The `?` says: “If this operation fails, bail out and return the error. Otherwise, keep going.” ## Why This Is So Smart The brilliance of the `?` operator is in its simplicity: - **It reduces boilerplate.** No need for repetitive match statements. - **It’s explicit.** You can see exactly where errors might occur. - **It’s enforced by the type system.** Rust won’t let you forget to handle an error. Compare that with chaining methods in C#, where you either: - Hope nothing throws, or - Wrap everything in `try/catch`, or - Check every return value manually if you’re avoiding exceptions. ## Even Cleaner: Using `thiserror` and Custom Errors Rust makes it easy to define your own error types and still use `?`. Example: ``` use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error("IO error: {0}")] Io(#[from] io::Error), #[error("Invalid input!")] InvalidInput, } fn read_file(path: &str) -> Result { let mut file = File::open(path)?; // auto-converts io::Error into MyError::Io let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } ``` This keeps your error types meaningful without sacrificing the ease of `?`. ## Wrapping It Up Error handling is one of those areas where Rust’s philosophy really shines: catch problems early, handle them clearly, and avoid surprise runtime crashes. The `?` operator makes error propagation *so* simple that once you get used to it, going back to nested `try/catch` blocks feels like using a fax machine. Tomorrow, we’ll talk about panic and how Rust handles those truly unrecoverable situations. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Crates and Dependencies: NuGet, Meet Cargo](https://www.woodruff.dev/crates-and-dependencies-nuget-meet-cargo/) **Published:** May 3, 2025 **Author:** Chris Woodruff **Excerpt:** Day 23... let’s talk about how Rust gets its packages! If you’re coming from the .NET world, you’re no stranger to NuGet. It’s been your trusty sidekick for pulling in libraries, managing versions, and bloating that csproj file with package references. In Rust, the equivalent is Cargo, and its packages are called crates. But here’s the twist: Cargo doesn’t just handle your dependencies. It’s your project manager, your build system, your tester, and your publisher, all rolled into one delightful tool. **Content:** Day 23… let’s talk about how Rust gets its packages! If you’re coming from the .NET world, you’re no stranger to NuGet. It’s been your trusty sidekick for pulling in libraries, managing versions, and bloating that `csproj` file with package references. In Rust, the equivalent is **Cargo**, and its packages are called **crates**. But here’s the twist: Cargo doesn’t just handle your dependencies. It’s your project manager, your build system, your tester, and your publisher, all rolled into one delightful tool. ## The Rust Package Manager: Cargo When you create a new Rust project with: ``` cargo new my_app ``` Cargo spins up a neat little project for you, complete with: ``` my_app/ ├── Cargo.toml # Like your .csproj file └── src/ └── main.rs ``` The `Cargo.toml` file is where your project metadata and dependencies live. Think of it as the Rust cousin to your `csproj` file, but simpler and less noisy. Here’s a basic `Cargo.toml`: ``` [package] name = "my_app" version = "0.1.0" edition = "2021" [dependencies] ``` Need a dependency? You can add it directly to `[dependencies]`, or just let Cargo handle it for you. ## Adding Dependencies: cargo add Let’s say you want to use the popular `rand` crate for random number generation. You could manually edit `Cargo.toml`, but why not let Cargo do the heavy lifting? ``` cargo add rand ``` This updates your `Cargo.toml` like so: ``` [dependencies] rand = "0.8" ``` Compare that with adding a package via NuGet CLI: ``` dotnet add package Newtonsoft.Json ``` Which updates your `csproj` file: ``` ``` The concept is familiar, but Cargo’s TOML format feels a bit lighter and easier on the eyes. ## Working with Dependencies Want to use that `rand` crate in your code? Easy: ``` use rand::Rng; fn main() { let mut rng = rand::thread_rng(); let n: u8 = rng.gen_range(1..=10); println!("Random number: {}", n); } ``` No fuss with manual `using` statements or hunting for assembly references. Cargo takes care of everything. ## Locking It Down When you build your project (`cargo build`), Cargo creates a `Cargo.lock` file. This is similar to `.csproj`’s `packages.lock.json` in .NET. It pins exact versions of your dependencies to ensure reproducible builds. Cargo.lock example snippet: ``` [[package]] name = "rand" version = "0.8.5" ``` ## Publishing Your Own Crate When you’re ready to share your library with the world, Cargo makes it super simple to publish to [crates.io](https://crates.io): ``` cargo publish ``` Compare that to pushing your package to a NuGet feed: ``` dotnet pack nuget push MyLibrary.nupkg -Source https://api.nuget.org/v3/index.json ``` Both ecosystems are solid here, but again, Cargo makes it feel a bit more integrated and less ceremony-heavy. ## Why Cargo Feels Like a Breath of Fresh Air - **Batteries included**: Dependency management, building, testing, and publishing all with one tool. - **Minimal configuration**: TOML files are clean and easy to read. - **Version resolution is predictable**: Thanks to `Cargo.lock`. - **No project file XML gymnastics**: Dependencies are just a few lines away. ## Wrapping It Up Cargo may be new to you, but it’s easy to fall in love with. If you’re used to juggling NuGet, `csproj` edits, and package managers as separate tasks, Rust’s approach will feel refreshingly cohesive. Tomorrow, we’re going to talk about error propagation with the `?` operator, so simple, so smart. Don’t miss it! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Organizing Code: Rust Modules vs C# Namespaces](https://www.woodruff.dev/organizing-code-rust-modules-vs-c-namespaces/) **Published:** May 2, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 22! After a week of wrestling with data modeling, it’s time to talk about something near and dear to every developer’s heart: keeping your code organized (and your sanity intact). If you're a C# developer, you've lived in the world of namespaces, public and internal access modifiers, and .cs files that can stack up faster than your coffee cups during crunch time. In Rust, the story is a little different, but delightfully simple once you get the hang of it. **Content:** Welcome to Day 22! After a week of wrestling with data modeling, it’s time to talk about something near and dear to every developer’s heart: keeping your code organized (and your sanity intact). If you’re a C# developer, you’ve lived in the world of **namespaces**, `public` and `internal` access modifiers, and `.cs` files that can stack up faster than your coffee cups during crunch time. In Rust, the story is a little different, but delightfully simple once you get the hang of it. ## C# Namespaces: The Familiar Territory In C#, you group your code into namespaces, like this: ``` namespace MyApp.Services public class EmailService { public void SendEmail(string address, string message) { Console.WriteLine($"Sending '{message}' to {address}"); } } ``` C# gives you `public`, `internal`, `protected`, and `private` as tools to control access. Plus, namespaces can be arbitrarily deep, independent of file structure. Your project might have folders that *kind of* line up with namespaces, but they don’t have to. ## Rust Modules: Organization That Matches Your Files In Rust, code organization is driven by **modules**, which are explicitly tied to your folder and file structure. Here’s the core idea: - `mod` declares a module. - `pub` makes items public outside the module. - Files and directories mirror the module structure. Example: ``` // src/main.rs mod services; fn main() { services::send_email("woody@dev.com", "Hello from Rust!"); } ``` And in `src/services.rs`: ``` pub fn send_email(address: &str, message: &str) { println!("Sending '{}' to {}", message, address); } ``` Notice how the module directly maps to the filename? Clean and predictable. ## Going Deeper: Nested Modules Rust also lets you create submodules using folders and `mod.rs` files, like this: ``` src/ ├── main.rs └── services/ ├── mod.rs └── email.rs ``` In `mod.rs`: ``` pub mod email; ``` In `email.rs`: ``` pub fn send_email(address: &str, message: &str) { println!("Sending '{}' to {}", message, address); } ``` In `main.rs`: ``` mod services; fn main() { services::email::send_email("woody@dev.com", "Hello again!"); } ``` ## Access Control: pub vs. private by Default In Rust, everything is **private by default,** unlike C#, where classes and methods are often public unless you say otherwise. - `pub`: Makes things visible outside the module. - No `pub`: Private to the module. If you need to expose something to just sibling modules (like C#’s `internal`), Rust offers `pub(crate)` for crate-wide visibility. ``` pub(crate) fn internal_tool() { println!("Only visible within this crate!"); } ``` ## C#’s Flexibility vs. Rust’s Structure C# lets you throw namespaces around however you want. Files don’t need to match namespaces. Rust, on the other hand, **encourages clarity** by enforcing file/module alignment. This can feel restrictive at first, but once you get into the flow, it makes code navigation and organization much easier. ## Why This Matters In C#, you might find yourself hunting through files to match up namespaces and folders. Rust’s “your modules are your files” approach keeps things straightforward: - Fewer surprises. - Easier to onboard new developers. - Access rules are enforced at compile-time, not just by convention. ## Wrapping It Up Rust’s module system is like that friend who insists on labeling their spice jars and alphabetizing their bookshelf. At first, it feels a little rigid, but when you’re looking for the paprika (or that one helper function), you’re *really* glad they did it. Tomorrow, we’ll dig into crates and dependencies. Where Rust’s package manager, Cargo, really shines. See you then! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Week 3: Wrap-Up: Data Modeling That Fights Back](https://www.woodruff.dev/week-3-wrap-up-data-modeling-that-fights-back/) **Published:** May 1, 2025 **Author:** Chris Woodruff **Excerpt:** Three weeks into Rust, and if your brain isn’t at least a little bit melted, I applaud your resilience! This week was all about data modeling. But not just any modeling—the kind that actively fights back when you try to make bad decisions. If you're used to the world of C#, you probably lean on classes, POCOs, and the occasional enum to represent state. And hey, that works… until it doesn’t. Rust’s structs and enums bring some serious muscle to the table by making sure your data models are clear, correct, and (most importantly) safe by design. Let’s take a moment to reflect on what we learned and why Rust’s approach makes data modeling feel less like a "best practices" hope-and-pray scenario and more like a language-enforced guarantee. **Content:** Three weeks into Rust, and if your brain isn’t at least a little bit melted, I applaud your resilience! This week was all about **data modeling**. But not just any modeling—the kind that actively *fights back* when you try to make bad decisions. If you’re used to the world of C#, you probably lean on classes, POCOs, and the occasional enum to represent state. And hey, that works… until it doesn’t. Rust’s structs and enums bring some serious muscle to the table by making sure your data models are clear, correct, and (most importantly) safe by design. Let’s take a moment to reflect on what we learned and why Rust’s approach makes data modeling feel less like a “best practices” hope-and-pray scenario and more like a language-enforced guarantee. ## Structs: Simple, Lean, and Mean Rust structs are deceptively simple. They group your data together just like C# classes or structs, but with one key difference: they’re value types without any hidden behavior, inheritance chains, or surprise runtime overhead. ``` struct User { username: String, email: String, active: bool, } let user1 = User { username: String::from("woodydev"), email: String::from("woody@dev.com"), active: true, }; ``` Compare that to a typical C# class: ``` public class User { public string Username { get; set; } public string Email { get; set; } public bool Active { get; set; } } var user1 = new User { Username = "woodydev", Email = "woody@dev.com", Active = true }; ``` Sure, they look similar—but Rust’s structs stay lean and honest. No hidden nullability, no inheritance headaches, and immutability by default unless you explicitly opt in. ## Enums: The Real MVP If structs are the foundation, enums are the secret sauce. Rust’s enums aren’t just glorified integer labels. They’re fully powered, discriminated unions that can hold data. ``` enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } let msg = Message::Move { x: 10, y: 20 }; ``` Compare this to how you’d represent something similar in C#: ``` abstract class Message { } class Quit : Message { } class Move : Message { public int X { get; set; } public int Y { get; set; } } class Write : Message { public string Text { get; set; } } class ChangeColor : Message { public int R { get; set; } public int G { get; set; } public int B { get; set; } } ``` C# encourages you to utilize class hierarchies, inheritance, and pattern matching. Rust just gives you enums and `match`, and it works beautifully. ## Pattern Matching: Clarity Wins With enums comes pattern matching. Rust’s `match` expression forces you to think through every possible case, no forgotten edge cases or accidental fall-throughs. ``` match msg { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to ({}, {})", x, y), Message::Write(text) => println!("Write message: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to RGB({}, {}, {})", r, g, b), } ``` Rust won’t compile if you forget to handle a variant. C# might warn you (if you’re lucky). Rust *enforces* it. ## Why Rust Makes You Better (Even If It Hurts a Little) The key lesson this week? Rust doesn’t just let you model state; it ensures you do it correctly. There’s no relying on convention or developer discipline to avoid bugs. Rust’s type system works like that strict but loving coach who won’t let you cut corners. - No null surprises - No accidental forgotten cases - No ambiguous “magic strings” or fragile inheritance trees Your models are explicit. Your intent is clear. And your compiler has your back. ## Wrapping Up Week 3 This week taught us that modeling state well isn’t about writing more code, it’s about writing better code. Rust’s structs and enums push you toward designs that are clear, correct, and maintainable from the start. Next week, we level up again: modules, crates, and error handling strategies. Stay tuned! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Result: A Better Way to Fail](https://www.woodruff.dev/result-a-better-way-to-fail/) **Published:** April 30, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 20! Today we’re talking about failure, but in the best possible way. Because, let's be honest, things go wrong. Files go missing. Network calls timeout. Data isn’t always what we expect. And if you've been living in the .NET world like I have, your first instinct might be to reach for the trusty try-catch block. Rust, though? Rust says, "Let’s not wait until runtime to deal with failure. Let’s handle it right now, at compile time." And the tool that it gives us to do that is Result. Rust, though? Rust says, "Let’s not wait until runtime to deal with failure. Let’s handle it right now, at compile time." And the tool it gives us to do that is Result. **Content:** Welcome to Day 20! Today we’re talking about failure, but in the best possible way. Because, let’s be honest, things go wrong. Files go missing. Network calls timeout. Data isn’t always what we expect. And if you’ve been living in the .NET world like I have, your first instinct might be to reach for the trusty `try-catch` block. Rust, though? Rust says, “Let’s not wait until runtime to deal with failure. Let’s handle it right now, at compile time.” And the tool that it gives us to do that is `Result`. ## Exceptions in .NET: The Traditional Way In C#, you’re probably used to something like this: ``` try { var data = File.ReadAllText("config.txt"); Console.WriteLine(data); } catch (IOException ex) { Console.WriteLine($"Failed to read file: {ex.Message}"); } ``` Exceptions bubble up at runtime, and unless you’re really diligent about catching them, your app might crash unexpectedly. Plus, there’s always that overhead of exception handling, especially if you’re dealing with lots of small errors. ## Enter `Result`: Rust’s Smarter Approach Rust flips the script. Instead of letting errors ambush you at runtime, it makes success or failure part of the type system itself: ``` use std::fs::File; use std::io::{self, Read}; fn read_file_contents(path: &str) -> Result { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } fn main() { match read_file_contents("config.txt") { Ok(data) => println!("File contents: {}", data), Err(e) => println!("Failed to read file: {}", e), } } ``` No exceptions. No surprises. `Result` makes it explicit: either you get `Ok(T)` or you get `Err(E)`. And the compiler makes sure you handle both. ## What Makes `Result` Better? - **No runtime surprises**: You can’t accidentally forget to handle an error case. - **Lightweight error handling**: No stack unwinding or performance penalty like exceptions. - **Safer concurrency**: Errors stay contained, making async and concurrent code easier to manage. - **Composable**: Use operators like `?` to propagate errors easily without boilerplate. Compare that with how tedious some error handling can be in .NET: ``` try { var result = await SomeApiCallAsync(); if (result == null) { throw new InvalidOperationException("API returned null!"); } Console.WriteLine(result); } catch (Exception ex) { Console.WriteLine($"Something went wrong: {ex.Message}"); } ``` In Rust, you could handle this more gracefully with: ``` fn do_something() -> Result { // Some logic that might fail Err(String::from("API returned an error!")) } fn main() { match do_something() { Ok(result) => println!("Success: {}", result), Err(e) => println!("Error: {}", e), } } ``` ## Chaining with `?`: Cleaner Code, Fewer Tears Rust’s `?` operator is the ultimate helper when working with `Result`. Instead of writing nested `match` statements, you can propagate errors up the call stack effortlessly: ``` fn read_username_from_file() -> Result { let mut file = File::open("username.txt")?; let mut username = String::new(); file.read_to_string(&mut username)?; Ok(username) } ``` If any of the operations fail, `?` automatically returns the error from the function. Clean and efficient. ## Wrapping It Up In .NET, exceptions are often the blunt instrument of error handling. Rust’s `Result` offers a scalpel—a precise, clear, and safe way to deal with failures. By making failure part of the type system, Rust encourages you to think about the “what ifs” upfront, not as an afterthought. And honestly? That mindset shift is a game-changer. Next up, we’re taking a look at panic and how Rust handles those “stop the world” situations versus recoverable errors like `Result`. Stick around! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Option: Where Null Is Not an Option](https://www.woodruff.dev/optiont-where-null-is-not-an-option/) **Published:** April 29, 2025 **Author:** Chris Woodruff **Excerpt:** Ah, nulls, the "billion-dollar mistake" that haunts just about every C# developer. How many times have you chased down a NullReferenceException, muttering under your breath, "But how could this even be null?" Well, guess what? In Rust, nulls are not a thing. At least, not in the wild-and-dangerous sense we're used to in .NET. Instead, Rust gives us Option. And let me tell you, once you get a taste of it, you'll wonder why we ever let null run loose in the first place. **Content:** Ah, nulls, the “billion-dollar mistake” that haunts just about every C# developer. How many times have you chased down a `NullReferenceException`, muttering under your breath, “But how could this even be null?” Well, guess what? In Rust, nulls are not a thing. At least, not in the wild-and-dangerous sense we’re used to in .NET. Instead, Rust gives us `Option`. And let me tell you, once you get a taste of it, you’ll wonder why we ever let `null` run loose in the first place. ## What Is Option? `Option` is Rust’s way of representing an optional value. A value can either be `Some(T)`—meaning there’s a value or `None`, meaning there isn’t. Here’s the basic idea: ``` fn main() { let some_number = Some(5); let no_number: Option = None; if let Some(n) = some_number { println!("Number is: {}", n); } else { println!("No number found"); } } ``` No rogue nulls. No “oops, I forgot to check”. It’s all baked into the type system, and the compiler makes sure you handle both cases. ## Nullable in C#: Close, But Not Quite C# has `Nullable`, which handles value types like `int?`, but reference types have always had a bit of a Wild West relationship with null. C# 8+ introduced nullable reference types with warnings and annotations, but it’s still on you to check and handle it properly. Example in C#: ``` int? someNumber = 5; if (someNumber.HasValue) { Console.WriteLine($"Number is: {someNumber.Value}"); } else { Console.WriteLine("No number found"); } ``` But with reference types: ``` string? name = null; if (name != null) { Console.WriteLine(name); } else { Console.WriteLine("No name"); } ``` The difference? In Rust, you can’t even *forget* to check. The compiler will smack your hand if you try. ## Pattern Matching FTW Rust makes handling `Option` not only safe but also elegant with pattern matching: ``` fn print_number(num: Option) { match num { Some(n) => println!("The number is: {}", n), None => println!("No number provided"), } } fn main() { print_number(Some(42)); print_number(None); } ``` Compare that with how much boilerplate we often have to write in C# just to feel “safe.” ## Chaining Options: The `unwrap_or` Magic Sometimes you want to provide a default if `None` shows up. Rust makes this painless: ``` fn main() { let number = Some(5); let value = number.unwrap_or(0); // returns 5 because Some(5) let no_number: Option = None; let default_value = no_number.unwrap_or(0); // returns 0 because None println!("Value: {} | Default: {}", value, default_value); } ``` This beats the pants off of repetitive null checks any day. ## Why This Matters Rust’s approach forces you to handle the “maybe” case upfront, making your intentions clear. No surprises at runtime. No, wondering if a function might hand you a null in disguise. And because it’s enforced at compile-time, you sleep better at night. In C#, nullable reference types are a nice band-aid, and `Nullable` helps with value types, but neither offers the rock-solid guarantee that Rust’s `Option` brings to the table. ## Wrapping Up Rust’s `Option` turns a common headache into a non-issue. It’s not about being fancy—it’s about making “do you have a value or not?” explicit, safe, and easy to handle. Next time, we’ll look at `Result`—because sometimes things don’t just disappear, they fail. And Rust wants to help you deal with that gracefully. See you there! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Destructuring: Pattern Matching’s Power Move](https://www.woodruff.dev/destructuring-pattern-matchings-power-move/) **Published:** April 28, 2025 **Author:** Chris Woodruff **Excerpt:** We’ve made it to Day 18, and today we’re going to unlock one of the coolest moves in Rust’s pattern matching toolkit: destructuring. If you've played around with C# 7+ deconstruction, you're already familiar with the idea of "pulling things apart" for easier access. But Rust? Rust takes that idea, cranks it up, and throws in some extra muscle. **Content:** We’ve made it to Day 18, and today we’re going to unlock one of the coolest moves in Rust’s pattern matching toolkit: destructuring. If you’ve played around with C# 7+ deconstruction, you’re already familiar with the idea of “pulling things apart” for easier access. But Rust? Rust takes that idea, cranks it up, and throws in some extra muscle. ## Destructuring 101: Breaking It Down Destructuring is all about breaking a data structure into its individual parts. It allows you to directly access the pieces inside a tuple, struct, or enum right where you need them. Here’s a simple tuple destructure in Rust: ``` fn main() { let point = (3, 7); let (x, y) = point; // destructuring the tuple println!("x: {}, y: {}", x, y); } ``` Compare that with the C# equivalent using deconstruction: ``` var point = (3, 7); (var x, var y) = point; Console.WriteLine($"x: {x}, y: {y}"); ``` Pretty similar, right? But Rust doesn’t stop there. ## Struct Destructuring: Get Specific Rust lets you destructure structs just as easily: ``` struct User { username: String, email: String, active: bool, } fn main() { let user = User { username: String::from("woodydev"), email: String::from("woody@dev.com"), active: true, }; let User { username, email, active } = user; println!("User: {}, Email: {}, Active: {}", username, email, active); } ``` Compare that with a similar concept in C#: ``` public class User { public string Username { get; set; } public string Email { get; set; } public bool Active { get; set; } public void Deconstruct(out string username, out string email, out bool active) { username = Username; email = Email; active = Active; } } var user = new User { Username = "woodydev", Email = "woody@dev.com", Active = true }; var (username, email, active) = user; Console.WriteLine($"User: {username}, Email: {email}, Active: {active}"); ``` Notice in C#, we need to manually implement the `Deconstruct` method. In Rust, it just works out of the box. ## Enum Destructuring: The Real Power Move The real MVP of Rust’s destructuring story comes when you’re working with enums. Here’s an example: ``` enum Shape { Circle(f64), Rectangle(f64, f64), Square(f64), } fn main() { let shape = Shape::Rectangle(10.0, 20.0); match shape { Shape::Circle(radius) => println!("Circle with radius: {}", radius), Shape::Rectangle(width, height) => println!("Rectangle ({} x {})", width, height), Shape::Square(side) => println!("Square with side: {}", side), } } ``` In C#, this would typically require class hierarchies with downcasting or pattern matching with `is` and type casts—not as seamless as Rust’s baked-in support for enums and destructuring. ## Ignore What You Don’t Need Another neat trick: Rust lets you ignore fields you don’t care about: ``` let (x, _) = (5, 10); println!("x: {}", x); // Ignores the second value ``` This keeps your code clean and intentional—no need to assign variables you aren’t going to use. ## Wrapping It Up Destructuring in Rust feels natural, powerful, and refreshingly flexible. Whether you’re cracking open tuples, structs, or enums, Rust gives you tools to access exactly what you need with minimal fuss. C# has its own flavor of deconstruction, and it’s solid, but Rust makes destructuring a core part of the language—no extra ceremony required. Next up, we’ll tackle `Option` and why Rust proudly says, “Null is not an option.” Stay tuned! **Categories:** Rust --- ### [Match: Switch on Steroids](https://www.woodruff.dev/match-switch-on-steroids/) **Published:** April 27, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to Day 17! By now, we've covered some seriously cool Rust features, but today might be my favorite so far: the match expression. If you're coming from a C# background—especially with the newer C# 8+ switch expressions—you might think you've seen it all. Trust me, Rust's match is like a switch statement on steroids, protein shakes, and a Red Bull chaser. **Content:** Welcome to Day 17! By now, we’ve covered some seriously cool Rust features, but today might be my favorite so far: the `match` expression. If you’re coming from a C# background—especially with the newer C# 8+ switch expressions—you might think you’ve seen it all. Trust me, Rust’s match is like a switch statement on steroids, protein shakes, and a Red Bull chaser. ## Basic Match: Rust’s Superpowered Switch In Rust, `match` is incredibly powerful and expressive. At its core, it’s used to match a value against multiple patterns. Think of it like a Swiss Army knife—it can handle anything from simple cases to complex scenarios with ease: ``` fn main() { let number = 4; match number { 1 => println!("One!"), 2 | 3 | 5 | 7 => println!("Prime"), 4..=10 => println!("Between 4 and 10"), _ => println!("Something else"), } } ``` Cool, right? Rust lets you handle multiple values (`2 | 3 | 5 | 7`) and even ranges (`4..=10`) with clean, concise syntax. ## Pattern Matching vs. C# Switch Expressions C# devs might recognize pattern matching from recent language updates. Let’s quickly remind ourselves of what C# 8+ switch expressions look like: ``` var number = 4; var result = number switch { 1 => "One", 2 or 3 or 5 or 7 => "Prime", >= 4 and "Between 4 and 10", _ => "Something else" }; Console.WriteLine(result); ``` Both languages share similarities, but Rust’s match goes even further: - **Exhaustive Checks**: Rust enforces exhaustive pattern matching at compile-time. If you miss a case, Rust won’t compile until you handle every possible scenario. - **Powerful Patterns**: Rust can match complex data types, destructure tuples and structs, and even match on enum variants. ## Advanced Match: Pattern Matching on Complex Types Here’s where Rust really flexes its muscles: ``` enum Coin { Penny, Nickel, Dime, Quarter(UsState), } enum UsState { Alabama, Alaska, // more states... } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } fn main() { let coin = Coin::Quarter(UsState::Alaska); println!("Value: {} cents", value_in_cents(coin)); } ``` This example demonstrates how match elegantly destructures and matches data within enums—something C# is starting to do but not yet as fluidly. ## The Wildcard `_` to the Rescue One more powerful feature: Rust’s wildcard `_`. This ensures every case is covered without explicitly defining them all: ``` let some_number = Some(5); match some_number { Some(7) => println!("Lucky number 7!"), _ => println!("Some other number"), } ``` ## Why Rust’s Match Is a Game-Changer - **Safety**: You simply cannot miss a case by accident. Rust forces you to handle every scenario explicitly or use a wildcard. - **Readability**: Complex logic stays concise and clear. - **Maintainability**: Adding or modifying patterns is straightforward, making your code easy to evolve and understand. ## Wrap-Up Rust’s match isn’t just powerful—it’s addictive. Once you start using it, you’ll wonder how you ever managed complex branching logic without it. It’s more than just a switch statement; it’s pattern matching at its very best. Tomorrow, we’ll push pattern matching even further with destructuring—prepare for another thrilling Rust adventure! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Enums: Discriminated Unions Done Right](https://www.woodruff.dev/enums-discriminated-unions-done-right/) **Published:** April 26, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to day 15! Today, we're diving into Rust's enums—and spoiler alert—they're not your typical enums from C#. Rust enums are powerful, flexible, and genuinely fun. If you've ever looked longingly at F#'s discriminated unions (or scratched your head at C#'s enums), you're about to discover something delightful. **Content:** Welcome to day 17! Today, we’re diving into Rust’s enums—and spoiler alert—they’re not your typical enums from C#. Rust enums are powerful, flexible, and genuinely fun. If you’ve ever looked longingly at F#’s discriminated unions (or scratched your head at C#’s enums), you’re about to discover something delightful. ## Not Your Grandma’s Enum When you think of enums in C#, you probably picture something like this: ``` public enum Direction { North, East, South, West } var direction = Direction.North; Console.WriteLine(direction); // Prints: North ``` Sure, this works fine for simple scenarios, but Rust’s enums are on a whole other level. They allow you to encapsulate multiple types and states under a single, coherent type. Check this out: ``` enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::Move { x: 10, y: 20 }; match msg { Message::Quit => println!("Quit"), Message::Move { x, y } => println!("Move to x:{} y:{}", x, y), Message::Write(text) => println!("Write: {}", text), Message::ChangeColor(r, g, b) => println!("Change color to: {}, {}, {}", r, g, b), } } ``` Rust enums aren’t just labels—they’re fully-fledged types that hold data! ## Discriminated Unions? What’s That? If you’ve dabbled in F#, you might recognize discriminated unions (DUs). They allow storing multiple related data structures in a single type, making them powerful for modeling complex states. Rust enums offer the same capability, bringing this powerful functional programming idea into the mainstream systems programming realm. Here’s how an F# discriminated union might look: ``` type Shape = | Circle of radius: float | Rectangle of width: float * height: float | Square of side: float let shape = Circle 10.0 match shape with | Circle radius -> printfn "Circle with radius: %f" radius | Rectangle (w, h) -> printfn "Rectangle (%f, %f)" w h | Square side -> printfn "Square with side: %f" side ``` And here’s a similar concept translated into Rust: ``` enum Shape { Circle(f64), Rectangle(f64, f64), Square(f64), } fn main() { let shape = Shape::Circle(10.0); match shape { Shape::Circle(radius) => println!("Circle with radius: {}", radius), Shape::Rectangle(w, h) => println!("Rectangle ({}, {})", w, h), Shape::Square(side) => println!("Square with side: {}", side), } } ``` Both F# and Rust support this powerful approach, but Rust enums integrate this functionality seamlessly into the language’s type system and safety guarantees. ## Why Rust Enums Rock - **Safety**: Rust enums are type-safe, meaning you’ll never accidentally treat one variant as another. - **Expressive**: You can clearly represent complex scenarios, states, and behaviors. - **Pattern Matching**: Built-in support for pattern matching makes handling enum types elegant and intuitive. ## Compared to C# In C#, you’d typically represent these complex scenarios using class hierarchies, inheritance, and polymorphism. But this approach can become cumbersome, making your code harder to follow and maintain: ``` abstract class Shape {} class Circle : Shape { public double Radius { get; set; } } class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } } // usage var shape = new Circle { Radius = 10.0 }; switch (shape) { case Circle c: Console.WriteLine($"Circle with radius: {c.Radius}"); break; case Rectangle r: Console.WriteLine($"Rectangle ({r.Width}, {r.Height})"); break; } ``` While the C# pattern matching has improved greatly, it still lacks the elegance and simplicity of Rust’s enums. ## Wrap It Up Rust enums are discriminated unions done right—expressive, safe, and seamlessly integrated into the language. They simplify your code and give you powerful tools to represent complex logic clearly and safely. Tomorrow, we’re going deeper into Rust’s powerful `match` statement—get ready to see just how strong your pattern-matching muscles can become! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Rust Structs vs C# Classes: Less is More](https://www.woodruff.dev/rust-structs-vs-c-classes-less-is-more/) **Published:** April 25, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back! After wrestling with Rust's strict ownership rules, let's ease into something a little more familiar (yet refreshingly different): structs. As a C# developer, you're probably thinking, "Oh, great, Rust structs are just classes, right?" Well, not exactly. Rust structs offer a minimalist and highly efficient way to structure data, contrasting nicely with our beloved C# Plain Old CLR Objects (POCOs). **Content:** Welcome back! After wrestling with Rust’s strict ownership rules, let’s ease into something a little more familiar (yet refreshingly different): structs. As a C# developer, you’re probably thinking, “Oh, great, Rust structs are just classes, right?” Well, not exactly. Rust structs offer a minimalist and highly efficient way to structure data, contrasting nicely with our beloved C# Plain Old CLR Objects (POCOs). ## Structs: A Quick Rust Recap Structs in Rust are fundamentally simple. They’re used to group related data together in a single type. Let’s jump right into a quick Rust example: ``` struct User { username: String, email: String, active: bool, sign_in_count: u64, } fn main() { let user1 = User { username: String::from("woodydev"), email: String::from("woody@dev.com"), active: true, sign_in_count: 1, }; println!("Username: {}", user1.username); } ``` Simple, right? Rust structs define clear, explicit fields and types without hidden behaviors. Everything you see is everything you get—no surprises. ## But Wait, Aren’t C# Classes Similar? In C#, you’re accustomed to using classes as your default building blocks: ``` var user1 = new User { Username = "woodydev", Email = "woody@dev.com", Active = true, SignInCount = 1 }; Console.WriteLine($"Username: {user1.Username}"); public class User { public string Username { get; set; } public string Email { get; set; } public bool Active { get; set; } public ulong SignInCount { get; set; } } ``` Looks pretty similar, doesn’t it? But there’s a catch. C# classes come with more than meets the eye—they’re reference types, managed by the garbage collector, and they can have inheritance, polymorphism, and various built-in behaviors. ## The Minimalist Philosophy Rust embraces minimalism with structs, doing away with inheritance and hidden complexities. Structs in Rust are always value types (like structs in C#, but without limitations on size or usability). They encourage you to be explicit and intentional about how data is copied or referenced. If you want methods on your Rust structs, no problem! Rust separates data and behavior clearly through implementations: ``` impl User { fn display_info(&self) { println!("{} ({})", self.username, self.email); } } fn main() { let user1 = User { username: String::from("woodydev"), email: String::from("woody@dev.com"), active: true, sign_in_count: 1, }; user1.display_info(); } ``` Notice the clarity and explicitness. Unlike C#, Rust doesn’t hide object behaviors behind inheritance chains or virtual methods unless explicitly requested. ## Struct vs. POCOs: Which Is Better? Both have their places: - **Rust Structs:** Lightweight, predictable, and highly performant. They’re perfect when you want clear, no-nonsense data grouping. - **C# Classes (POCOs):** Feature-rich, easy to use, and integrated deeply with the .NET runtime. They offer more flexibility with inheritance and runtime features. Rust’s minimalism might seem restrictive initially, but it actually helps prevent many issues that occur from unexpected object behaviors or memory management complexities in larger applications. ## Less Is Indeed More By removing layers of complexity, Rust structs help you create reliable and maintainable code that clearly expresses its intent. It’s an approach where simplicity becomes a strength, not a limitation. Tomorrow, we’re exploring Rust enums—a feature that’s significantly more powerful than what you’re used to in C#. Get ready; it’s another step towards mastering Rust’s elegant simplicity. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Week 2: Reflections on Ownership Week: My Brain Hurts (in a Good Way)](https://www.woodruff.dev/week-2-reflections-on-ownership-week-my-brain-hurts-in-a-good-way/) **Published:** April 24, 2025 **Author:** Chris Woodruff **Excerpt:** It's official—week two of learning Rust has wrapped, and I've survived. Mostly. This week was all about ownership, borrowing, and wrestling with Rust's strict rules. And boy, did my C# brain get a workout! It hurts, but honestly, it's the good kind of pain—the one you feel after finally nailing that tough workout or solving an especially nasty bug. **Content:** It’s official—week two of learning Rust has wrapped, and I’ve survived. Mostly. This week was all about ownership, borrowing, and wrestling with Rust’s strict rules. And boy, did my C# brain get a workout! It hurts, but honestly, it’s the good kind of pain—the one you feel after finally nailing that tough workout or solving an especially nasty bug. ## The Ownership Rollercoaster Rust’s ownership model initially felt like an overly protective parent. “You can’t touch this memory—it’s mine!” the compiler screamed at me repeatedly. Coming from the garbage-collected, runtime-managed world of .NET, where memory is mostly an afterthought, Rust’s rules felt restrictive. But soon enough, the genius behind this strictness clicked: it’s all about avoiding those pesky runtime issues and achieving fearless concurrency. Here’s a quick recap of what ownership looks like in Rust: - **Each value has a single owner.** - **When the owner goes out of scope, the value gets dropped.** - **Ownership can be moved, but never shared implicitly.** ``` fn main() { let s1 = String::from("hello"); let s2 = s1; // Ownership moves to s2 // println!("{}", s1); // Compiler error: s1 is no longer valid! println!("{}", s2); // Works fine } ``` This might look alien to seasoned C# devs, but Rust ensures that memory safety is maintained at compile-time—no dangling pointers or double-frees here! ## Borrowing, the Friendlier Cousin Borrowing quickly became my best buddy. With borrowing, you can use a value without taking ownership, letting multiple parts of your code “share” data safely. Here’s borrowing in action: ``` fn calculate_length(s: &String) -> usize { s.len() // Borrows the String without taking ownership } fn main() { let my_string = String::from("Hello, Rust!"); let length = calculate_length(&my_string); println!("Length: {}", length); // my_string is still valid! } ``` This is a game-changer from a .NET perspective because it removes a lot of runtime overhead and safety checks, making your code faster and safer. ## Rust vs. .NET Mindset Reflecting on this week, the biggest revelation was how Rust reshapes the way you think about code. In .NET, memory management and ownership are handled by the garbage collector. This frees you up but at the cost of performance overhead and potential runtime surprises (hello, `NullReferenceException`!). Rust flips the script by pushing these concerns upfront—right at compile-time. It’s stricter and demands more from you initially, but the payoff is enormous: - **Predictable and performant code**: You can trust your program’s behavior in production. - **Safety by default**: Memory errors become almost impossible to create inadvertently. - **Concurrency confidence**: Rust lets you write concurrent code without constantly worrying about race conditions. ## The Brain Stretch Was Worth It Initially, ownership and borrowing made my head spin. But wrestling with these concepts has deepened my understanding of memory, performance, and safety in ways .NET never forced me to explore. My C# habits had to adapt, and that’s a good thing. So, yes, my brain hurts—but it’s a satisfying ache. It’s the sensation of genuinely learning something transformative. And I’m eager to keep this momentum going. Next week, we’re diving into structs, enums, and pattern matching—another powerful set of Rust features ready to reshape my coding style yet again. Stay tuned, it’s going to be another wild ride! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Shadowing in Rust: Redeclaring with Style](https://www.woodruff.dev/shadowing-in-rust-redeclaring-with-style/) **Published:** April 23, 2025 **Author:** Chris Woodruff **Excerpt:** We've all been there. You're coding away, and suddenly you reuse a variable name without realizing it. If you're a C# developer, your brain probably triggers a warning alarm, screaming something like, "Hey, buddy! You've already used that name!" But guess what? In Rust, this isn't just allowed—it's encouraged, and it even has a cool name: shadowing. So what exactly is shadowing, and why does Rust promote it? Let's dive in and explore how this stylish redeclaration trick differs from what we're used to in C#. **Content:** We’ve all been there. You’re coding away, and suddenly you reuse a variable name without realizing it. If you’re a C# developer, your brain probably triggers a warning alarm, screaming something like, “Hey, buddy! You’ve already used that name!” But guess what? In Rust, this is not only allowed but also encouraged, and it even has a cool name: shadowing. So what exactly is shadowing, and why does Rust promote it? Let’s dive in and explore how this stylish redeclaration trick differs from what we’re used to in C#. ## Shadowing 101 Shadowing is simply declaring a new variable with the same name as an existing one. It sounds unusual at first, especially given the strict scoping rules of C#. But trust me, once you get the hang of it, it’s a neat feature that can lead to cleaner and safer code. Check out this simple example in Rust: ``` fn main() { let x = 5; let x = x + 1; // shadows previous x { let x = x * 2; // shadows again in an inner scope println!("Inner scope x: {}", x); // prints 12 } println!("Outer scope x: {}", x); // prints 6 } ``` This snippet demonstrates that each new declaration of `x` doesn’t mutate the previous value. It creates a new binding altogether. The shadowed variables aren’t changed; they’re just hidden by the new declarations. Once the scope ends, the original variables re-emerge. ## But How Does C# Handle This? In C#, shadowing isn’t explicitly supported the same way. If you declare a variable with the same name within the same scope, you’ll get a compile-time error. Let’s quickly glance at how C# deals with similar situations: ``` void Method() { int x = 5; // int x = x + 1; // Error: A local variable named 'x' is already defined { int x = 10; // This is allowed as it's a new scope Console.WriteLine($"Inner scope x: {x}"); // prints 10 } Console.WriteLine($"Outer scope x: {x}"); // prints 5 } ``` In C#, the new `x` in the inner scope hides the outer scope’s `x`. But you can’t redeclare `x` in the same scope. ## Why Shadow in Rust? Shadowing in Rust isn’t just syntactic sugar, and it’s genuinely helpful. Here are a few scenarios where shadowing shines: - **Transformations and Validations**: Often, you might want to parse a string into a number or apply validation. Shadowing lets you reuse a descriptive variable name after transforming its value: ``` let input = "42"; let input: u32 = input.trim().parse().expect("Not a number!"); ``` - **Immutable by Default**: Since variables are immutable by default in Rust, shadowing helps keep your code clean without explicitly using mutable variables. You can redeclare variables instead of mutating them, clearly signaling that each step is a separate operation. ## Gotchas and Good Practices Although shadowing is beneficial, it’s essential not to overdo it. Using shadowing excessively might make your code harder to read, so keep it clear and concise: - **Avoid Shadowing Across Large Scopes**: Shadowing is most effective in close proximity. If your shadowed variables are too far apart, it might confuse readers (or future you). - **Clarity Is Key**: Always shadow variables for clear transformations and not just to reuse names arbitrarily. ## Wrapping It Up Shadowing might seem odd at first, especially given our C# background, where each variable is assigned a unique identity within its scope. But once you get used to Rust’s elegant way of reusing variable names to express transformations clearly, it becomes second nature and might even become your style of choice. Tomorrow, we’ll wrap up our journey through Rust’s ownership and borrowing concepts with a reflection on how these ideas have reshaped our coding approach. Prepare yourself, it’s going to be an insightful experience! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Slices and Strings with Rust: Goodbye C# StringBuilder?](https://www.woodruff.dev/slices-and-strings-goodbye-c-stringbuilder/) **Published:** April 22, 2025 **Author:** Chris Woodruff **Excerpt:** When you’ve spent years writing C#, you get really comfortable with string being immutable, Span being your performance trick, and StringBuilder being your go-to hammer when a for loop starts building text. And then you start learning Rust. Suddenly, String isn’t immutable. &str looks suspiciously like a Span in disguise. And you realize… wait, do I even need a StringBuilder anymore? Today on Day 12, I dove into slices and strings in Rust, and let me tell you, it’s a whole new world, but a surprisingly elegant one. **Content:** When you’ve spent years writing C#, you get really comfortable with `string` being immutable, `Span` being your performance trick, and `StringBuilder` being your go-to hammer when a `for` loop starts building text. And then you start learning Rust. Suddenly, `String` isn’t immutable. `&str` looks suspiciously like a `Span` in disguise. And you realize… wait, do I even need a `StringBuilder` anymore? Today on Day 12, I dove into **slices and strings in Rust**, and let me tell you, it’s a whole new world, but a surprisingly elegant one. ### Strings in C#: Immutable, Safe, and Everywhere You probably know this dance by heart: ``` string name = "Alice"; string upper = name.ToUpper(); Console.WriteLine(name); // still "Alice" ``` Strings in .NET are immutable reference types backed by UTF-16. If you want to modify one, you either: - Reassign the variable, or - Reach for a `StringBuilder` if performance matters You also get tools like `Span` and `Memory` for performance-critical code when slicing or manipulating buffers. ### Strings in Rust: A Bit More to Unpack Rust splits strings into **two distinct types**: - `String` – a growable, heap-allocated UTF-8 string - `&str` – a string slice, referencing a part of a `String` (or a string literal) ``` fn main() { let name = String::from("Alice"); let greeting = format!("Hello, {}!", name); println!("{}", greeting); } ``` You can modify `String` directly: ``` let mut msg = String::from("Hello"); msg.push_str(", world"); println!("{}", msg); // Hello, world ``` Yep, no `StringBuilder` needed. Just mutate the `String`. ### `&str`: The Slice-y Sidekick The `&str` type is like a read-only view into a `String`. Think of it like a `Span` in C#, but with guaranteed safety and lifetimes checked at compile time. ``` fn greet(name: &str) { println!("Hi, {}!", name); } fn main() { let user = String::from("Alice"); greet(&user); // passing a &str } ``` You can even slice strings with range syntax: ``` let text = String::from("Rustacean"); let slice = &text[0..4]; // "Rust" println!("{}", slice); ``` But here’s the catch: Rust strings are UTF-8 encoded. So slicing is **byte-based**, not char-based. Slicing in the middle of a multibyte character? Compiler panic. ### Comparing with C#: Span Vibes Rust’s `&str` gives you the safety and flexibility of a C# `ReadOnlySpan`, but with much tighter compiler enforcement. And if you want something like a `Span` in Rust, just use a `&[u8]` slice: ``` let bytes = b"hello"; // byte string literal ``` There’s no need to pin memory, worry about unsafe access, or juggle multiple string types. Rust’s system is consistent, even if it’s more verbose at times. ### UTF-8 vs UTF-16: Why It Matters C# strings are UTF-16. That means most common characters (including emojis) take 2 bytes, but some use surrogate pairs. Slicing blindly is usually okay, but not always safe. Rust strings are **UTF-8**, which is smaller for ASCII and more web-native. But it also means: ``` let smile = "😊"; println!("{}", &smile[0..1]); // ERROR! ``` Why? Because you’re trying to cut a byte in the middle of a 4-byte emoji. Rust protects you from doing that by accident. Thanks, borrow checker (again). ### Do You Ever Need StringBuilder in Rust? Not really. With `String`, you can: - Append with `push_str()` or `push()` - Format with `format!()` (like `string.Format()` or C# interpolated strings) - Replace substrings, trim, split… everything you expect If you need high-performance streaming or chunked writes, you’d probably reach for `std::fmt::Write` or buffered I/O, but for most devs, `String` just does the job. ### Final Thoughts: It’s Simpler Than It Looks At first, having `String` and `&str` felt like one type too many. But now? I get it. Rust separates string *ownership* from string *views*. And once you embrace slices, you realize they’re everywhere: strings, arrays, buffers. It’s one concept to learn that applies to multiple types. Tomorrow, we examine **shadowing**, specifically re-declaring variables with the same name intentionally. It may sound unusual, but it’s actually quite elegant. Stay tuned. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [The Borrow Checker: Rust’s Tough-Love Mentor](https://www.woodruff.dev/the-borrow-checker-rusts-tough-love-mentor/) **Published:** April 21, 2025 **Author:** Chris Woodruff **Excerpt:** You think you’ve been writing safe code… until you meet the Rust borrow checker. Then suddenly, your once-proud instincts are being side-eyed by a compiler that’s not mad, just disappointed. Today, on Day 11 of my Rust journey, we talk about the infamous, unyielding, sometimes infuriating, but ultimately brilliant guardian of Rust safety: the borrow checker. **Content:** You think you’ve been writing safe code… until you meet the Rust borrow checker. Then suddenly, your once-proud instincts are being side-eyed by a compiler that’s not mad, just disappointed. Today, on Day 11 of my Rust journey, we talk about the infamous, unyielding, sometimes infuriating, but ultimately brilliant guardian of Rust safety: **the borrow checker**. ### First Contact: Compiler Says No It started innocently enough. I wrote this: ``` fn main() { let mut message = String::from("Hello"); let r1 = &message; let r2 = &mut message; // ERROR! println!("{}, {}", r1, r2); } ``` And BAM… compile-time failure. What? I just wanted to read and write the same variable! C# would’ve shrugged and let me do it. Rust? > “You’re trying to borrow `message` as mutable while it’s still borrowed as immutable. I’m not mad. I’m just preventing undefined behavior.” ### Why So Strict? Because the borrow checker is doing what the garbage collector can’t: **enforcing safe memory access rules at compile time**. In C#, I could be reading from a variable while something else is writing to it. In multithreaded code, this is a landmine. That’s why we have `lock`, `volatile`, and a few prayers. Rust’s answer: don’t allow that situation *ever*. You get **either**: - One `&mut` mutable reference - **Or** any number of `&` immutable references But never both at the same time. ### A Real-World Example Let’s say I wanted to update a field in a struct while also printing it: ``` struct Profile { name: String, } fn main() { let mut profile = Profile { name: String::from("Alice") }; let name_ref = &profile.name; profile.name.push_str(" Smith"); // ERROR println!("Name: {}", name_ref); } ``` The compiler’s response? Nope. I’m still using `name_ref` when I try to mutate `profile.name`. Although this may seem safe in C#, Rust considers it a violation of the borrowing contract. ### The Lesson: Think in Lifetimes What the borrow checker *really* teaches you is how long your variables live and who owns them. It makes lifetimes feel **real**, not abstract. The moment I started thinking, *“Who owns this?”* and *“How long will this reference stick around?”*, my code got clearer, not just in Rust, but even in my C# brain. ### Getting Around It: Scope Ends = Borrow Ends Sometimes, it’s just a matter of scope: ``` fn main() { let mut name = String::from("Alice"); { let r1 = &name; println!("{}", r1); } // r1 goes out of scope here let r2 = &mut name; // OK now! r2.push_str(" Smith"); println!("{}", r2); } ``` This compiles because `r1`’s lifetime ends before `r2` starts. The borrow checker isn’t trying to ruin your day, it’s just making sure you *actually* stop using something before you mutate it. ### Why It Feels So Different from C# In C#, you don’t really think about lifetimes. The GC handles cleanup, and the runtime handles access. But that flexibility comes at a cost: runtime bugs, potential data races, and memory churn. Rust flips the script. It gives you **compile-time certainty** that you aren’t doing anything sketchy. But that certainty comes with a learning curve. ### Final Thoughts: From Frustration to Respect At first, I’ll be honest, the borrow checker felt like a wall. But now? I see it as a mentor. One that says: - “No, you can’t do that yet.” - “You need to think more carefully.” - “I’m here to help you write code that can’t crash because of memory issues.” That’s not annoying. That’s empowering. Tomorrow, we’ll ease into something a little more familiar again: strings and slices… *aka* goodbye `StringBuilder`, hello UTF-8. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Borrowing and References: Rust’s Version of ref (But Nicer)](https://www.woodruff.dev/borrowing-and-references-rusts-version-of-ref-but-nicer/) **Published:** April 20, 2025 **Author:** Chris Woodruff **Excerpt:** If you've been writing C# for a while, you’ve likely crossed paths with ref, in, and out parameters. They allow you to pass variables by reference, enabling a method to read or modify the original value. Useful? Definitely. Safe? Uh... sometimes. In Rust, there's a similar concept called borrowing. It uses & and &mut, and it feels a lot like passing ref or in in C#, but with one significant difference: The compiler enforces safety rules that make data races and invalid access impossible. Today, we’re diving into borrowing, references, and how Rust holds your hand (and your memory) without letting you write foot-gun code. **Content:** If you’ve been writing C# for a while, you’ve likely crossed paths with `ref`, `in`, and `out` parameters. They allow you to pass variables by reference, enabling a method to read or modify the original value. Useful? Definitely. Safe? Uh… sometimes. In Rust, there’s a similar concept called **borrowing**. It uses `&` and `&mut`, and it feels a lot like passing `ref` or `in` in C#, but with one significant difference: > The **compiler** enforces safety rules that make data races and invalid access *impossible*. Today, we’re diving into borrowing, references, and how Rust holds your hand (and your memory) without letting you write foot-gun code. ### C# Refresher: Passing by Reference In C#, you might pass by reference like this: ``` void Double(ref int x) { x *= 2; } int value = 10; Double(ref value); Console.WriteLine(value); // 20 ``` This works, but you’re on the honor system. You could accidentally mutate something you didn’t mean to. There’s nothing stopping you from sharing that `ref` with another thread or keeping it alive too long. Rust doesn’t leave this to chance. ### Rust’s Version: Borrowing with `&` Let’s start with borrowing in read-only mode: ``` fn main() { let name = String::from("Alice"); greet(&name); println!("{}", name); // Still valid! } fn greet(person: &String) { println!("Hello, {}!", person); } ``` Here, `&name` means “borrow `name` temporarily.” The `greet` function doesn’t take ownership; it just gets a reference to the data. Rust tracks this at compile time. When `greet` is done, the borrow ends, and `name` is still fully usable. ### Want to Mutate? Use `&mut` Just like C#’s `ref`, Rust lets you *mutably* borrow a value using `&mut` but it’s even stricter. ``` fn main() { let mut count = 5; double(&mut count); println!("Doubled: {}", count); } fn double(num: &mut i32) { *num *= 2; } ``` Notes: - You need to declare the original variable as `mut`. - You borrow it mutably with `&mut`. - Inside `double`, you *dereference* it with `*num`. And the best part? Rust won’t let you have more than one mutable reference at a time. You either get: - Many immutable references **or** - One mutable reference Not both. This avoids the classic threading issues and memory races that C# devs have to tiptoe around with locks or `volatile`. ### The Borrow Checker: Annoying but Trustworthy Here’s where Rust gets strict. Try doing this: ``` let mut count = 10; let r1 = &mut count; let r2 = &mut count; // ERROR! ``` Rust won’t allow it. Why? Because you’re trying to create two mutable references to the same data at the same time. Even if `r1` and `r2` exist in separate lines, Rust sees the *overlapping lifetimes* and panics *at compile time*. No runtime surprises. No corrupted memory. No segfaults. It feels annoying at first, but it’s the kind of frustration that saves you from hours of debugging later. ### Lifetime of a Reference? Compiler’s Got It Covered You don’t have to manually manage memory like in C++. Rust figures out the **lifetime** of each reference for you in most cases. It ensures that your borrowed data never outlives the thing it points to. This means: ``` fn main() { let r; { let x = 5; r = &x; // ERROR: x doesn’t live long enough } println!("{}", r); } ``` This won’t compile because `x` gets dropped when the inner scope ends. Rust refuses to let you create a dangling reference. C# would happily let you hold a reference to a deallocated object (until the GC saves you… or doesn’t). ### TL;DR: Borrowing Is Like `ref`, But With Rules Here’s the cheat sheet: ConceptC#RustPass by ref`ref`, `in`, `out``&`, `&mut`DereferenceImplicit (`x`)Explicit (`*x`)SafetyRuntime GC, trust systemCompile-time rulesThread-safe?Not guaranteedGuaranteed--- ### Final Thoughts Borrowing in Rust might feel heavy-handed at first, but it’s actually freeing. It frees you from GC pauses. It frees you from race conditions. It frees you from wondering, *“Can I still use this?”* And it all happens **before** your code even runs. Tomorrow we tackle the ultimate tough-love teacher: **the Borrow Checker** itself. You’ll probably hate it a little, but eventually, you’ll realize it’s the mentor you never knew you needed. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Move Semantics in Rust: What Just Happened to My Variable?](https://www.woodruff.dev/move-semantics-in-rust-what-just-happened-to-my-variable/) **Published:** April 19, 2025 **Author:** Chris Woodruff **Excerpt:** Okay, so picture this: you're cruising along in your nice, type-safe Rust code, and suddenly... your variable vanishes. Not literally, of course. But the compiler throws a fit, and you’re left staring at an error that says something like: "value borrowed here after move". Wait, move? Welcome to move semantics, Rust’s very opinionated way of managing memory and keeping you from accidentally using things that don’t belong to you anymore. Today’s post is a follow-up to yesterday’s crash course in ownership and this one might sting a little at first if you're coming from the comfy world of .NET’s reference types. But stick with me. It’s about to make sense. **Content:** Okay, so picture this: you’re cruising along in your nice, type-safe Rust code, and suddenly… your variable vanishes. Not literally, of course. But the compiler throws a fit, and you’re left staring at an error that says something like: **“value borrowed here after move”**. Wait, **move**? Welcome to **move semantics**, Rust’s very opinionated way of managing memory and keeping you from accidentally using things that don’t belong to you anymore. Today’s post is a follow-up to yesterday’s crash course in ownership and this one might sting a little at first if you’re coming from the comfy world of .NET’s reference types. But stick with me. It’s about to make sense. ### C# Land: References Rule Everything In C#, most of our objects live on the heap and are accessed via references. When we assign a variable to another, we’re copying the reference, not the object. ``` var a = new Person("Alice"); var b = a; b.Name = "Bob"; Console.WriteLine(a.Name); // Bob ``` Both `a` and `b` point to the same object. It’s shared, and changes affect both. ### Rust Land: Ownership Transfer by Default Rust, on the other hand, doesn’t use reference semantics by default. Instead, it *moves* the value unless it’s a type that’s `Copy`. Here’s a head-scratcher from early on in my Rust journey: ``` fn main() { let name = String::from("Alice"); let other = name; println!("{}", name); // ERROR: value borrowed here after move } ``` Wait… what? When we do `let other = name;`, Rust *moves* the ownership of the `String` from `name` to `other`. That means `name` no longer owns the value and using it again is forbidden. Think of it like this: the value didn’t get cloned or copied it got handed off. ### But Why Move Instead of Copy? Great question. In Rust, types like `String`, `Vec`, and anything that allocates on the heap are *not* cheap to copy. Instead of silently cloning data and potentially causing performance issues, Rust makes moves explicit. This design choice forces you to think about whether you truly need a clone or a reference. It’s a performance win wrapped in a safety blanket. ### But Some Types *Are* Copyable Rust distinguishes between types that are **Copy** (duplicated on assignment) and those that aren’t. These scalar types are `Copy` by default: ``` let x = 5; let y = x; // no move, just a copy println!("x: {}, y: {}", x, y); // totally fine ``` The same applies to `bool`, `char`, and simple numeric types. So why isn’t `String` `Copy`? Because `String` owns heap memory. Copying it blindly would mean a deep copy, which Rust won’t do unless you ask explicitly. ### Clone All the Things? Not Quite. Want to keep using a value after moving it? Then you’ll need to `clone()` it. ``` fn main() { let name = String::from("Alice"); let other = name.clone(); println!("name: {}, other: {}", name, other); } ``` Just like in C#, this creates two independent strings. But Rust makes sure you know that it’s not cheap you called `.clone()` on purpose. ### References: Your New Best Friend Sometimes you don’t want to move or clone. You just want to *borrow* a value. ``` fn main() { let name = String::from("Alice"); greet(&name); println!("{}", name); // still usable } fn greet(person: &String) { println!("Hello, {}!", person); } ``` Passing a reference means you’re saying: “I don’t want to take this, just use it for a second.” Very much like `ref` or `in` in C#, but safer and enforced at compile time. ### Final Thoughts: Rust Isn’t Trying to Confuse You At first, move semantics feel harsh. Variables stop working. Ownership gets transferred. Errors fly everywhere. But once you get the hang of it, it’s honestly brilliant. It forces you to be deliberate about memory and data flow and that makes your code faster and more predictable. It’s like Rust is saying: “You want performance and safety? You’ve gotta earn it.” Tomorrow, we’ll dive into borrowing and references in more depth, including mutable borrowing. It’s like `ref` and `out` in C#, but with training wheels and a seatbelt. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Ownership in Rust: The Most C++-ish Thing I’ve Loved (and I Mean That in a Good Way)](https://www.woodruff.dev/ownership-in-rust-the-most-c-ish-thing-ive-loved-and-i-mean-that-in-a-good-way/) **Published:** April 18, 2025 **Author:** Chris Woodruff **Excerpt:** Let’s get one thing out of the way: as a C# developer, I’ve never had to think too hard about memory. The garbage collector (GC) is always there, lurking in the background, sweeping up after my code, like a very polite, very invisible butler. But Rust? Rust doesn’t do garbage collection. There’s no GC.Collect(), no memory profiler needed to chase leaks from forgotten Dispose() calls. Instead, Rust gives you something bold, powerful, and… at first, kind of intimidating: Ownership. Today’s post kicks off Week 2 of my Rust adventure, and it’s all about how this one idea changes everything. **Content:** Let’s get one thing out of the way: as a C# developer, I’ve never had to think too hard about memory. The garbage collector (GC) is always there, lurking in the background, sweeping up after my code, like a very polite, very invisible butler. But Rust? Rust doesn’t *do* garbage collection. There’s no `GC.Collect()`, no memory profiler needed to chase leaks from forgotten `Dispose()` calls. Instead, Rust gives you something bold, powerful, and… at first, kind of intimidating: **Ownership.** Today’s post kicks off Week 2 of my Rust adventure, and it’s all about how this one idea changes everything. ### Memory Management in .NET: The Easy Life In C#, memory is managed for you. You create an object: ``` var user = new User("Alice"); ``` And you just use it. When the runtime decides the object isn’t needed anymore, it gets cleaned up—eventually. Maybe after a few GC generations. Maybe never (hi, memory leaks). But the point is: *you don’t manage it directly.* ### Rust’s Ownership Model: No GC, No Problem In Rust, you don’t have a GC. Instead, you have rules. The compiler ensures that you follow them. Here’s the core idea: > Every value in Rust has a single owner. When the owner goes out of scope, the value is dropped (freed). Let’s break it down with an example: ``` fn main() { let name = String::from("Alice"); say_hello(name); println!("{}", name); // uh-oh } fn say_hello(person: String) { println!("Hello, {}!", person); } ``` This code **won’t compile**. Why? Because when you pass `name` into `say_hello`, ownership of the `String` moves to that function. After the call, you no longer “own” it, so trying to use `name` again is a big no-no. The compiler won’t let you shoot yourself in the foot. That’s ownership in action. ### But Wait—Why Is This Better? At first, it feels restrictive. But once you get it, you realize: **this is compile-time memory safety without a garbage collector**. No dangling pointers. No use-after-free. No double-frees. No finalizers. No memory pressure spikes during a GC sweep. It’s like having C++’s raw performance and control… without the classic C++ landmines. ### Want to Keep Using That Variable? Borrow It! Rust gives you an elegant way to *temporarily* use a value without transferring ownership: borrowing. ``` fn main() { let name = String::from("Alice"); greet(&name); // pass a reference println!("{}", name); // still works } fn greet(person: &String) { println!("Hi, {}!", person); } ``` The `&` means “borrow, don’t take.” So `greet` can use the `String`, but it doesn’t own it. The original `main` function still holds the ownership, so we’re allowed to use `name` afterward. C# has something kind of like this with `ref`, `in`, and `out` parameters, but in Rust, **borrowing is the foundation of how data flows**. ### What About Heap vs Stack? In .NET, you might wonder whether something is a value type (stack) or reference type (heap). In Rust, you get more control, but also clearer semantics: - Values like `i32`, `bool`, and `char` are stored on the stack. - Things like `String` (heap-allocated) are managed by ownership. There’s no need to remember what’s a “reference type.” Instead, you think in terms of **who owns the data**, and where it lives becomes a natural consequence of that. ### So, Is This Slower? Nope. In fact, Rust’s zero-cost abstractions make this model **faster**, because there’s no GC overhead. Memory gets released the moment it’s no longer needed, and it’s all done at compile time. It’s like being back in C++ land—but safer, which is wild. ### Final Thoughts: Rules That Make You Better Ownership in Rust makes you think harder about how data moves through your app. It’s annoying at first (especially when the compiler tells you “value moved here” fifty times), but then… something clicks. You start seeing data flow more clearly. You stop writing sloppy, leaky code. You trust the compiler like never before. Tomorrow, we’ll go deeper into what happens when values move, and why your variable sometimes just… disappears. Spoiler: it’s not magic. It’s Rust. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Reflections on Week 1: Rust's Minimalism Hits Different](https://www.woodruff.dev/reflections-on-week-1-rusts-minimalism-hits-different/) **Published:** April 17, 2025 **Author:** Chris Woodruff **Excerpt:** Seven days into learning Rust, and I feel like I’ve been through a developer bootcamp with a compiler that doubles as a personal trainer. It doesn’t let you slack, but it does make you better. This week, I went from installing the toolchain to wrestling with immutable variables and puzzling over semicolons in return statements. As a C# developer, I expected syntax differences, but I didn’t expect the philosophical shifts. Let’s recap what stood out, what stung, and why I’m still excited to keep going. **Content:** Seven days into learning Rust, and I feel like I’ve been through a developer bootcamp with a compiler that doubles as a personal trainer. It doesn’t let you slack, but it *does* make you better. This week, I went from installing the toolchain to wrestling with immutable variables and puzzling over semicolons in return statements. As a C# developer, I expected syntax differences, but I didn’t expect the philosophical shifts. Let’s recap what stood out, what stung, and why I’m still excited to keep going. ### Hello, World—Now Please Explain Yourself Starting with “Hello, World!” was comforting. It felt familiar until I realized that in Rust, functions don’t need a class, and printing uses a macro. Like this: ``` fn main() { println!("Hello, World!"); } ``` No `class Program`, no `static void Main`, and no `using System;`. Clean. Minimal. Straight to the point. It was the first sign that Rust’s default posture is: **Do less. Mean more.** ### dotnet new vs cargo new Setting up my first Rust project with `cargo new` was suspiciously smooth. Almost *too* smooth. Compared to the .NET CLI: ``` dotnet new console -n HelloWorld ``` Rust’s version felt leaner and more focused: ``` cargo new hello_world ``` And the fact that it created a Git repo for me automatically? Bonus points. `cargo` is one of the nicest CLIs I’ve ever used and that’s coming from someone who genuinely enjoys `dotnet`. ### Let Me Be Immutable In C#, `var` is friendly. It assumes you might want to change things. Rust says: *Prove it.* ``` let name = "Chris"; // Immutable let mut name = "Woody"; // Mutable, but only if you ask ``` Having to add explicitly mut made me stop and think: Do I need this to change? That pause, though annoying at first, actually led to cleaner code. Compare that to how easily we sprinkle state changes throughout our C# apps without thinking twice. ### Functions: Watch That Semicolon I didn’t expect to be tripped up by a semicolon, but here we are: ``` fn add(a: i32, b: i32) -> i32 { a + b } ``` In Rust, that works. But if you add a semicolon? ``` fn add(a: i32, b: i32) -> i32 { a + b; } ``` Now the function returns `()` (unit), not the sum of the numbers. It’s wild how one little `;` changes everything. But it’s a good reminder that **Rust is an expression-first language**, not a statement-first one like C#. ### Types Without the Bloat Rust handed me scalars, tuples, and arrays and basically said, “See what you can do without reaching for a class.” ``` let person = ("Chris", 45); let scores = [10, 20, 30]; ``` I found myself modeling real things without writing a single `struct`. That restraint? Kinda refreshing. Like being told you can’t use PowerPoint and having to explain your idea with a whiteboard instead. You get creative. ### What Clicked - **The tooling** is fantastic. `cargo` makes the .NET CLI look a bit dated. - **The compiler** is strict but helpful. It’s not just yelling, it’s coaching. - **The language** is expressive without being verbose. I started to feel at home once I embraced the idea that **Rust wants me to think before I write**. It’s not trying to be easy it’s trying to help me write better code. ### What Tripped Me Up - Semicolon sensitivity in return values. - Forgetting to add `mut` when I just wanted to change a value “real quick.” - The fact that there are no classes yet. I kept reaching for one like muscle memory. ### Week 1 in a Sentence? > “It’s like learning to cook with only five ingredients and realizing that’s all you needed in the first place.” Next week we dive into **ownership, borrowing, and the borrow checker**, aka “The Compiler’s Tough-Love Phase.” Time to rewire my .NET brain a little more. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Functions in Rust: Familiar Yet Different](https://www.woodruff.dev/functions-in-rust-familiar-yet-different/) **Published:** April 15, 2025 **Author:** Chris Woodruff **Excerpt:** Today’s Rust lesson hit a familiar note but with a twist. Writing functions in Rust feels almost like writing them in C#. Until, of course, the compiler reminds you that this language doesn’t always play by the same rules. On Day 5 of my Rust-for-C# developers journey, let’s break down function definitions, type inference, return values, and where Rust and C# shake hands... and where they give each other a suspicious side-eye. On Day 5 of my Rust-for-C# developers journey, let’s break down function definitions, type inference, return values, and where Rust and C# shake hands... and where they give each other a suspicious side-eye. **Content:** Today’s Rust lesson hit a familiar note but with a twist. Writing functions in Rust feels *almost* like writing them in C#. Until, of course, the compiler reminds you that this language doesn’t always play by the same rules. On Day 5 of my Rust-for-C# developers journey, let’s break down function definitions, type inference, return values, and where Rust and C# shake hands… and where they give each other a suspicious side-eye. ### The C# Way: You Know It By Heart Let’s take a basic C# method: ``` int Add(int a, int b) { return a + b; } ``` Simple, clean, and typed from top to bottom. It’s the kind of method you’ve probably written a thousand times. Now let’s take a look at the Rust equivalent. ### The Rust Way: Concise, but Watch the Semicolon ``` fn add(a: i32, b: i32) -> i32 { a + b } ``` Looks familiar, right? But let’s pause. That return value isn’t using the `return` keyword. Instead, Rust has this *expression-style* return: the last line of your function becomes the return value **as long as you don’t end it with a semicolon**. Now try this: ``` fn add(a: i32, b: i32) -> i32 { a + b; } ``` Boom. The compiler complains. Why? Because adding that semicolon turns the expression into a statement—and statements return `()` (the unit type), not your expected value. So yeah… **semicolons are not just decoration in Rust. They have meaning.** If you prefer being explicit, you *can* use `return`: ``` fn add(a: i32, b: i32) -> i32 { return a + b; } ``` That works too, but the idiomatic way in Rust is to lean into expression returns when possible. ### Type Inference: The Good and the Explicit C# lets us do things like: ``` var result = Add(2, 3); ``` And Rust? It loves type inference *inside* functions but **not at function boundaries**. Inside the function: ``` fn greet(name: &str) { let message = format!("Hello, {}!", name); println!("{}", message); } ``` Totally inferred. But when you *define* a function, Rust wants you to be explicit with parameter and return types. No `var`-style magic here. So this won’t fly: ``` fn multiply(a, b) { a * b } ``` Nope. Rust needs to know what `a` and `b` are, or it won’t compile. ### Multiple Returns? Think Tuples If you’re used to C# tuples or out parameters, Rust does tuples too: ``` fn split_number(n: i32) -> (i32, i32) { (n / 2, n % 2) } ``` You call it like: ``` let (quotient, remainder) = split_number(9); ``` Destructuring built-in? Yes, please. ### Functions as First-Class Citizens Rust also lets you pass functions around just like delegates in C#: ``` fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 { f(f(x)) } fn square(n: i32) -> i32 { n * n } fn main() { let result = apply_twice(square, 2); println!("Result: {}", result); // 16 } ``` Yep, functions are values. Just like C# delegates and lambdas—but with less ceremony. ### Final Thoughts: Familiar, but with Rules That Bite Writing functions in Rust gave me déjà vu, but in a good way. But the small differences matter. Rust functions are predictable, expressive, and often less verbose than C# once you get past the quirks: - Semicolons are powerful. - Return types matter. - You’ll love tuples. - And the compiler? It wants you to be *very* clear. Tomorrow we explore Rust’s basic data types, including tuples and arrays. The title might just be: *“Where Are My Classes?”* **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Variables in Rust: Let Me Be Immutable](https://www.woodruff.dev/variables-in-rust-let-me-be-immutable/) **Published:** April 14, 2025 **Author:** Chris Woodruff **Excerpt:** So today, I ran head-first into a Rust design decision that made me pause and go, “Wait… really?” In C#, I can declare a variable with var and change it later. No fuss. In Rust? Not so fast. Your variables are frozen solid if you don’t explicitly ask for mutability. Welcome to Day 4: the world of let, mut, and what happens when your muscle memory meets a strict compiler. **Content:** So today, I ran head-first into a Rust design decision that made me pause and go, “Wait… really?” In C#, I can declare a variable with `var` and change it later. No fuss. In Rust? Not so fast. Your variables are frozen solid if you don’t explicitly ask for mutability. Welcome to Day 4: the world of `let`, `mut`, and what happens when your muscle memory meets a strict compiler. ### The C# Way: Flexible by Default In C#, we’re used to this kind of setup: ``` var name = "Chris"; name = "Woody"; // totally fine ``` Unless we go out of our way to make something `readonly`, we expect variables to be mutable. It’s the default. We use `readonly` for class fields when we want to protect them. But local variables? We change those all the time. ### The Rust Way: Immutable Until Proven Mutable In Rust, the story flips. ``` fn main() { let name = "Chris"; name = "Woody"; // ERROR: cannot assign twice to immutable variable } ``` The compiler stops you cold. Why? Because `let` creates an *immutable* binding by default. If you want to change a value later, you need to opt the variable from the start into mutability: ``` fn main() { let mut name = "Chris"; name = "Woody"; // now it's totally fine println!("{}", name); } ``` Adding `mut` tells Rust, “Yes, I know this might change—and I’m okay with that.” ### Why Does Rust Care So Much? Rust’s whole design centers around *safety and predictability*. By defaulting to immutability, the compiler helps prevent accidental state changes that can introduce bugs—especially in concurrent scenarios. It’s like using `readonly` in C#, but it’s baked into every line unless you say otherwise. ### You Don’t Own the Variable, You Bind It One thing that tripped me up was this little mental shift: Rust doesn’t really talk about “declaring variables.” It talks about *binding values to names*. ``` let x = 5; ``` You’re binding the value `5` to the name `x`. It’s not just a semantic detail—this matters big-time when you get into move semantics, ownership, and borrowing (spoiler alert for next week). ### Shadowing: The Plot Twist Oh—and get this: you can “re-bind” a variable to a new value (even with a different type) using the same name: ``` fn main() { let score = "100"; let score = score.parse::().unwrap(); println!("Parsed score: {}", score); } ``` That’s not a mistake—it’s called *shadowing*, and it’s allowed (and encouraged) in Rust when you want to transform or reinterpret values without mutating state. ### Wrap-Up: Mutability as a Conscious Choice I’m starting to appreciate how Rust makes me *think* before changing things. It’s like the language is saying, “Are you really sure this needs to change?” And honestly, sometimes the answer is no. C# is like a whiteboard—easy to scribble on and erase. Rust is more like a stone tablet—you can write on it, but you’d better mean it unless you explicitly chip away at it with `mut`. Tomorrow, we dive into functions. Can you guess whether return values need semicolons or not? I couldn’t either. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Hello, Rust! Hello, World! Rust vs C# Syntax](https://www.woodruff.dev/hello-rust-hello-world-rust-vs-c-syntax/) **Published:** April 13, 2025 **Author:** Chris Woodruff **Excerpt:** Let’s be honest—every new language journey begins the same way: with a humble "Hello, World!" It’s the developer's rite of passage. So today, on Day 3 of my 42-day Rust challenge, I’m writing the most iconic two-word phrase in programming in both C# and Rust… and then tearing it apart. Because while the output is the same, what these two languages make you say to get there reveals a lot about how they think. **Content:** Let’s be honest—every new language journey begins the same way: with a humble “Hello, World!” It’s the developer’s rite of passage. So today, on Day 3 of my 42-day Rust challenge, I’m writing the most iconic two-word phrase in programming in both C# and Rust… and then tearing it apart. Because while the output is the same, what these two languages make you *say* to get there reveals a lot about how they think. ### C# Says Hello (Politely, Verbosely) Here’s what a typical C# “Hello, World!” looks like today: ``` using System; namespace HelloWorld; class Program { static void Main(string[] args) { Console.WriteLine("Hello, World!"); } } ``` If you’re like me, this feels second nature. It’s ceremony. It’s structure. It’s the .NET way. You’ve got a namespace, a class, a static method, and a whole lot of boilerplate for a single line of output. We’ve come to accept this verbosity as normal. But when you compare it to Rust, you realize… maybe we’ve been doing too much. ### Rust Says Hello (Straight to the Point) Here’s the Rust version: ``` fn main() { println!("Hello, World!"); } ``` Wait, that’s it? No `using` directives, no classes, no static modifiers. Just a function named `main` and a macro call. Let’s unpack this simplicity: - **No classes**: Rust isn’t object-oriented by default. You don’t need a class to hold your code. - **`fn main()`**: Rust’s entry point is just a function. No need for `static` or `void`. - **`println!`**: That exclamation mark? It means you’re calling a *macro*, not a function. (More on that later) ### The Syntax Gap: What It Tells Us Let’s highlight a few differences that immediately stood out: ConceptC#RustEntry Point`static void Main(string[] args)``fn main()`Print Output`Console.WriteLine("text")``println!("text")`Type Imports`using System;`None needed for `println!`Semicolon Required?YesYes, usually (but careful—it matters more)Object OrientationDefaultOptional / Trait-basedWhat struck me right away is how *intentional* Rust is. It doesn’t assume you want a class, or an object-oriented design. It gives you just what you need—and nothing more. It’s like ordering a black coffee after years of Starbucks lattes with 3 pumps of ceremony. ### But… Where’s the Runtime? C# runs on a managed runtime with JIT compilation and garbage collection. Rust, on the other hand, compiles down to machine code with zero runtime overhead. So when you build and run your Rust app, it’s about as “close to the metal” as you can get—without writing C or sweating over memory allocation yourself. That minimalism in the “Hello, World!” syntax? It’s not just aesthetics—it’s a hint that Rust is designed to stay out of your way and let you control the details when you need to. ### Wrap-Up: A Simpler Start, A Different Philosophy C# holds your hand—maybe too much. Rust gives you a nudge and says, “Go ahead, you’ve got this.” Writing my first Rust app felt liberating. The syntax was lean, focused, and surprisingly friendly once I accepted that I didn’t need all the fluff. Tomorrow, I’ll dig into variables—specifically Rust’s obsession with immutability. Spoiler: You have to *ask* permission to change things. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [dotnet new, Meet cargo new: A Tale of Two CLIs](https://www.woodruff.dev/dotnet-new-meet-cargo-new-a-tale-of-two-clis/) **Published:** April 12, 2025 **Author:** Chris Woodruff **Excerpt:** It’s Day 2 of my journey learning Rust as a longtime C# developer, and today I took the plunge: I installed Rust and created my first project. The whole thing felt a bit like unpacking a new toolbox—familiar enough to recognize the tools, but different enough that I had to check the manual. **Content:** It’s Day 2 of my journey learning Rust as a longtime C# developer, and today I took the plunge: I installed Rust and created my first project. The whole thing felt a bit like unpacking a new toolbox—familiar enough to recognize the tools, but different enough that I had to check the manual. Let’s walk through how installing Rust compares to getting started with .NET and how `cargo` stacks up against our old friend `dotnet`. ## Step One: Installing Rust (Yes, It’s Just One Command) C# devs are used to Visual Studio installers and SDK juggling. With Rust, it’s… shockingly simple. ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` That’s it. One command. `rustup` is Rust’s version manager, installer, and updater all in one. It even sets up your environment variables and gives you `cargo`, the Rust CLI by default. I didn’t need a separate runtime or SDK or debate which version of Visual Studio to install, which was refreshing. Want to verify the install? Just run: ``` rustc --version cargo --version ``` Boom. You’re ready to code. ## Step Two: Project Bootstrapping — `cargo` vs `dotnet` In .NET land, we’ve all typed this at some point: ``` dotnet new console -n HelloWorld cd HelloWorld dotnet run ``` Rust’s version? ``` cargo new hello_world cd hello_world cargo run ``` It’s almost identical in spirit. But `cargo` does a few nice things right out of the gate: - It initializes a Git repo for you by default. - It gives you a `Cargo.toml` file (like `.csproj`, but… readable). - It keeps source code in a `src` folder, which feels neat and tidy. Here’s what you get from `cargo new hello_world`: ``` hello_world/├── Cargo.toml└── src └── main.rs ``` And the default `main.rs`: ``` fn main() { println!("Hello, world!"); } ``` No using directives, no `class Program`, no `static void Main`. Just a function and some curly braces. Minimalism, meet productivity. ## Why `cargo` Feels Like the CLI I Always Wanted If you’re coming from the .NET world, you’ll notice `cargo` bundles everything: it builds, runs, tests, adds dependencies, and even publishes your packages. Need a new dependency? No NuGet commands, no UI dialogs—just: ``` cargo add serde ``` And `cargo` grabs it for you and updates your `Cargo.toml`. It’s as if `dotnet`, `nuget`, and MSBuild got together and decided to be one happy command. FYI, the *serde* package is a generic serialization/deserialization framework. ## The Verdict: Setup That Gets Out of the Way Installing Rust and getting started with `cargo` was fast and frictionless. It reminded me how refreshing it is when tooling *just works* and doesn’t require me to babysit it. Tomorrow, I’ll get my hands dirty writing “Hello, World!” in both C# and Rust and start teasing apart the syntax differences. Spoiler: One of them makes you think harder about semicolons. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Why Rust? A C# Developer’s Journey Begins](https://www.woodruff.dev/why-rust-a-c-developers-journey-begins/) **Published:** April 11, 2025 **Author:** Chris Woodruff **Excerpt:** Today marks Day 1 of my 42-day challenge to learn Rust—with a twist. I’m not approaching this as a blank-slate beginner. I’m bringing along years of C# experience, mental muscle memory from countless LINQ expressions, async/await workflows, and enough IEnumerables to make your head spin. But now? I’m learning a language that doesn’t have a garbage collector, doesn’t throw exceptions the same way, and thinks null is a bad idea. **Content:** *The daily list of topics for the series is here: [From C# to Rust: A 42-Day Developer Challenge](https://woodruff.dev/from-c-to-rust-a-42-day-developer-challenge/)* Today marks Day 1 of my 42-day challenge to learn Rust—with a twist. I’m not approaching this as a blank-slate beginner. I’m bringing along years of C# experience, mental muscle memory from countless LINQ expressions, async/await workflows, and enough `IEnumerable`s to make your head spin. But now? I’m learning a language that doesn’t have a garbage collector, doesn’t throw exceptions the same way, and thinks `null` is a bad idea. Why? ### The Rusty Itch I Needed to Scratch Here’s the thing—I’ve grown very comfortable in the .NET ecosystem. Too comfortable. Writing code in C# often feels like working in a luxury car: smooth, feature-rich, and with great support systems like LINQ, Jetbrains Rider, and the ever-present garbage collector doing its thing behind the scenes. But lately, I’ve been craving something that makes me think differently, something that forces me out of autopilot. I wanted a systems language that feels modern, safe, and powerful but without the memory horror stories of C++. That’s where Rust comes in. Rust doesn’t hold your hand but doesn’t let you shoot yourself in the foot without asking, “Are you *really* sure about that?” ### First Impressions: C# Brain Meets Rust Compiler Let me say that my C# brain and the Rust compiler are not best friends yet. For instance, in C#, I might casually throw around a variable like this: ``` var name = "Rustacean"; Console.WriteLine(name); ``` In Rust? ``` fn main() { let name = "Rustacean"; println!("{}", name); } ``` Looks similar, right? But the moment I try to *change* that value, Rust slams the brakes. ``` fn main() { let name = "Rustacean"; name = "Ferris"; // Compiler says nope! } ``` If I want mutability, I have to *explicitly* ask for it: ``` fn main() { let mut name = "Rustacean"; name = "Ferris"; println!("{}", name); } ``` The Rust compiler is basically a strict teacher who wants me to write safer code by default. It’s like going from a chill substitute teacher (C#) to a no-nonsense professor with a red pen (Rust). ### Why This Journey Matters I’m not here to switch camps. I love C#—it’s elegant, powerful, and great for web apps, APIs, and enterprise workloads. But if there’s one thing I’ve learned over the years, it’s that **learning a new language sharpens your skills in all the others.** Rust’s approach to memory management, error handling, and data modeling forces you to think differently. And that “different” is what excites me. Even if I never ship a production app in Rust, the ideas I pick up will make my C# code better. Also, let’s be real—how many developers can say they’ve wrestled with the borrow checker and lived to tell the tale? ### What to Expect For the next 42 days, I’ll be documenting my Rust journey daily. Each post will draw connections between Rust and C#, highlight the “aha!” moments, and probably include a few “why won’t this compile?!” meltdowns. If you’re a .NET dev curious about Rust or want to follow along for the entertainment, welcome aboard. Tomorrow: installing Rust and comparing `cargo new` to `dotnet new`. Spoiler: One of them feels like it came from 2025. **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Swapping and Targeting Like a Pro: htmx Magic for Razor Pages](https://www.woodruff.dev/swapping-and-targeting-like-a-pro-htmx-magic-for-razor-pages/) **Published:** March 30, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, htmx explorer. Today, we’re unlocking two of the most powerful tools in the HTMX toolbox: hx-target and hx-swap. These are the keys to making your Razor Pages do exactly what you want without breaking a sweat. **Content:** Welcome back, htmx explorer. Today, we’re unlocking two of the most powerful tools in the htmx toolbox: **hx-target** and **hx-swap**. These are the keys to making your Razor Pages do precisely what you want without breaking a sweat. ## Why Targeting and Swapping Matter When building interactive web pages, you don’t always want to replace the entire page every time you fetch new content. Sometimes, you want to update a small part of the page, like a form or a specific component. That’s where **hx-target** and **hx-swap** come in. Let’s break down how these features work and see some real-world use cases. ## Understanding hx-target The **hx-target** attribute tells htmx where to place the response from a request. By default, htmx replaces the element that triggered the request, but you can specify any element you want. ### Example Usage Imagine you have a button that fetches user details without refreshing the page. #### Index.cshtml ``` @page @model IndexModel htmx Targeting Demo User Details Load User Info ``` #### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace YourNamespace.Pages; public class IndexModel : PageModel { public IActionResult OnGetGetUser() { var userHtml = "Name: John DoeAge: 30"; return Content(userHtml, "text/html"); } } ``` What’s happening here? The button sends a request to `/GetUser`, and the result is injected into the `#user-info` div because of the **hx-target** attribute. Simple and effective. ## Understanding hx-swap Now let’s talk about **hx-swap**. It controls how the response is inserted into the target element. Here are your options: - `innerHTM`L (Default): Replaces only the contents of the target element. - `outerHTML` : Replaces the entire target element. - `beforebegin`: Inserts content before the target element. - `afterbegin`: Inserts content inside the target element, before existing content. - `beforeend`: Inserts content inside the target element, after existing content. - `afterend`: Inserts content after the target element. ### Example Usage Suppose you have a list of items you want to update without replacing the entire list. #### Index.cshtml ``` @page @model IndexModel htmx Swapping Demo Item List Add Item Item 1 Item 2 ``` #### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace YourNamespace.Pages; public class IndexModel : PageModel { private static int _itemCount = 2; public IActionResult OnGetAddItem() { _itemCount++; var newItemHtml = $"Item {_itemCount}"; return Content(newItemHtml, "text/html"); } } ``` In this example, `hx-swap="beforeend"` ensures that every time you click the button, a new item is added **to the end of the list** instead of replacing the entire list. That’s some serious power with minimal effort. ## Use Cases for Dynamic Content Loading The combination of **hx-target** and **hx-swap** is ideal for all kinds of scenarios, such as: - **Infinite Scrolling:** Load more content when a user scrolls to the bottom of a page. - **Dynamic Forms:** Adding or removing form elements without reloading the page. - **Comment Systems:** Appending new comments without refreshing the comment thread. - **Real-time Updates:** Swapping out parts of the UI based on live data. ## Bringing It All Together The true power of htmx lies in mixing and matching attributes to make your web apps more interactive without overloading them with client-side logic. Swapping and targeting with htmx is one of the simplest ways to improve your user experience with very little code. Next up, we’ll be building full-fledged interactive applications where these techniques really shine. Stay tuned. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Rust for the Sharp Mind: 6 Weeks of Learning Rust as a C# Developer](https://www.woodruff.dev/rust-for-the-sharp-mind-6-weeks-of-learning-rust-as-a-c-developer/) **Published:** April 9, 2025 **Author:** Chris Woodruff **Excerpt:** What will happen when you take a C# developer, hand them a curly-brace language that loves safety and hates nulls, and give them six weeks to figure it out? You will experience a journey filled with rewired brain circuits, redefined mental models, and a newfound respect for the compiler. This will mark the beginning of my 42-day dive into Rust, a daily learning challenge that will push me out of my .NET comfort zone and into a language that promises to be both frustratingly strict and beautifully empowering. **Content:** What will happen when you take a C# developer, hand them a curly-brace language that loves safety and hates nulls, and give them six weeks to figure it out? You will experience a journey with me filled with rewired brain circuits, redefined mental models, and a newfound respect for the compiler. This will mark the beginning of our 42-day dive into Rust. This daily learning challenge will push us out of our .NET comfort zone and into a language that promises to be both frustratingly strict and beautifully empowering. ## Week 1: A New Syntax, A New Mindset The first week will appear deceptively easy. Installing Rust with rustup will feel a lot like setting up .NET with the CLI. Running `cargo new` will evoke memories of `dotnet new`, and printing the first “Hello, world!” to the console will feel familiar enough. Then, the gentle shake will come: variables will be immutable by default, semicolons will signify more than you think, and functions will return values without a return statement. The syntax will strike as clean but purposefully minimal. By Day 7, we will realize that Rust isn’t just a language but a mindset shift. ## **Week 2: The Ownership Awakening** Week two will hit like a freight train made of compiler errors. Ownership, borrowing, and lifetimes—none of these will have a direct equivalent in C#. As a developer accustomed to the garbage collector doing the heavy lifting, being told “you moved that value, you can’t use it anymore” will feel rude. But then it will click. Rust will compel us to be explicit about how memory is accessed and when it’s shared. It will be like having the world’s pickiest code reviewer living inside our compiler. Even though we and the borrow checker won’t get along initially, by the end of the week, we will find ourselves writing safer code without even thinking about it. ## **Week 3: Structs, Enums, and the Joy of Pattern Matching** This will be the week we fall in love with Rust’s type system. `struct` and `enum` in Rust will do what C#’s classes and enums can’t—they will model state precisely and safely. Pattern matching with `match` will feel like a supercharged switch, and using `Option` instead of null will make bugs harder to create. Rust’s enums, especially with associated data, will feel like something we have always wanted in C# but never quite had (unless we dipped into F#). Modeling data in Rust will become a joy, not a chore. ## **Week 4: Modules, Crates, and Errors Done Right** By week four, we will start to feel at home. We will dig into modules and visibility—how `mod` and `pub` work—and how Rust’s project structure will be rigid but clear. Adding dependencies through Cargo and `Cargo.toml` will be clean and intuitive. Crates will resemble NuGet packages with fewer quirks. Then, we will encounter error handling. `Result` will change the way we think about failure. Instead of hiding behind exceptions, Rust will make me deal with errors upfront. Using `?` to bubble errors up without ceremony will be genius. It will be like getting compiler-enforced error handling with almost no boilerplate. ## **Week 5: Traits, Generics, and the Lifetime Dance** This week will be the deepest water. Traits will be Rust’s answer to interfaces but with far more power. They will be extensible and composable and feel more like type classes than contracts. Generics will be similar to C#’s but without runtime overhead. And then there will be lifetimes—the part that every Rust beginner dreads. I will be honest: lifetimes won’t be easy. However, once we understand that they’re not about how long something lives, but about how long references are valid, things will begin to make sense. Rust will make the implicit guarantees of C#’s memory safety explicit, and that will be a good thing. ## **Week 6: Building Something Real** We will cap off the journey by building a small CLI tool using `clap`. It will parse arguments, read from a file, and do something mildly useful. But more importantly, it will work, it will be fast, and it will feel solid. Testing with `#[test]` will be fast and built-in. Packaging with `cargo build --release` will be simple. And the binary will be tiny compared to anything .NET has ever given me. By the end of the week, we will benchmark it against a similar C# tool. Rust will be noticeably faster, use less memory, and ship in a single file. For certain workloads, it will be hard to argue with that kind of efficiency. ## **Final Thoughts: What Rust Will Teach Us About C#** Learning Rust will teach us a new language and make me a better C# developer. We will begin to appreciate the cost of abstraction more clearly. We will start thinking more deliberately about memory and ownership. And we will realize that the strictness of a compiler isn’t a burden—it’s a partner that keeps my future self out of trouble. Will we use Rust everywhere? No. But will we reach for it when performance, safety, and small binaries matter? Absolutely. If you’re a C# developer curious about Rust, I encourage you to give it six weeks. It will challenge your habits, sharpen your skills, and change how you think about code. And hey—if the borrow checker starts feeling like a friend by Day 42, you’ll definitely be doing something right. I will post the outline for the 42 days in a blog post tomorrow, and then we will start Friday. I hope you will join me. **Categories:** Rust **Tags:** C#, dotnet, programming, rust --- ### [The Future of Server-Driven Web Apps: Why htmx and ASP.NET Are Just Getting Started](https://www.woodruff.dev/the-future-of-server-driven-web-apps-why-htmx-and-asp-net-are-just-getting-started/) **Published:** April 8, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, Razor Pages fan. Today, we’re looking ahead at where htmx and ASP.NET Core are heading in the grand scheme of web development. Spoiler alert: Server-driven apps are making a comeback, and htmx is leading the charge. **Content:** Welcome back, Razor Pages fan. Today, we’re looking ahead at where htmx and ASP.NET Core are heading in the grand scheme of web development. Spoiler alert: Server-driven apps are making a comeback, and htmx is leading the charge. ## The Growing Adoption of htmx In a world dominated by front-end frameworks like React, Angular, and Vue, it’s refreshing to see a library like htmx making waves. Why? Not every app needs a full-blown SPA (Single Page Application). In fact, most apps are better off being server-driven with sprinkles of interactivity. ### Why Developers Are Embracing htmx - **Simplicity:** No complicated build pipelines. Just drop in a ` Server-Driven Dashboard Loading stats... ``` #### Dashboard.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; namespace YourNamespace.Pages; public class DashboardModel : PageModel { public IActionResult OnGetUpdateStats() { var stats = $"Server Time: {DateTime.Now:HH:mm:ss}"; return Content(stats, "text/html"); } } ``` ### Why This Is So Good - **No JavaScript Frameworks:** You didn’t have to reach for React, Vue, or Angular. - **Pure Razor Pages:** Your existing ASP.NET Core knowledge is all you need. - **Automatic Updates:** htmx’s `hx-trigger="every 5s"` makes real-time updates dead simple. ## Predictions for Server-Side Rendered Applications So, where are we heading with all this? Is htmx the future of web development or just a cool tool for specific use cases? ### The Rise of Hybrid Apps It’s not about “killing SPAs” or “destroying JavaScript frameworks.” Instead, it’s about finding the right balance. And here’s what I see happening: 1. **Server-Driven Rendering Will Continue to Grow:** More developers will realize they don’t need a full SPA for most apps. 2. **htmx Will Gain Popularity:** As developers discover its simplicity and power, htmx will become a go-to tool for building interactive server-rendered apps. 3. **Hybrid Architectures Will Dominate:** Developers will mix htmx with client-side frameworks when necessary, rather than making everything a client-rendered SPA. ### What About ASP.NET Core? ASP.NET Core is already one of the best server-side frameworks. Tools like htmx make it even better by allowing developers to build interactive apps without the complexity of modern JavaScript frameworks. - Razor Pages will remain relevant for building fast, server-driven apps. - Blazor will continue to evolve, but htmx will attract developers who prefer server-side rendering without the WebAssembly overhead. ## The Bottom Line The future of web development isn’t about killing SPAs or replacing frameworks. It’s about using the right tool for the job. And for many ASP.NET Core developers, htmx is proving to be that tool. This is the last post on the topic of htmx and ASP.NET. Tomorrow will be something new, so stay with me on my daily blog post for my 2025 goal. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Keeping Your htmx Apps Safe: Security Best Practices for ASP.NET Developers](https://www.woodruff.dev/keeping-your-htmx-apps-safe-security-best-practices-for-asp-net-developers/) **Published:** April 7, 2025 **Author:** Chris Woodruff **Excerpt:** Alright, developer friends, it’s time to talk about something we all know is essential but often neglect until it’s too late: security. htmx makes building interactive apps with ASP.NET Razor Pages a breeze, but if you’re not careful, you could be opening your app to all sorts of nasty vulnerabilities. Let’s make sure that doesn’t happen. **Content:** Alright, developer friends, it’s time to talk about something we all know is essential but often neglect until it’s too late: security. htmx makes building interactive apps with ASP.NET Razor Pages a breeze, but if you’re not careful, you could be opening your app to all sorts of nasty vulnerabilities. Let’s make sure that doesn’t happen. ## Avoiding Common Security Pitfalls Just because you’re not writing JavaScript doesn’t mean your app is magically secure. Here are some common pitfalls to watch out for: ### 1. Trusting Client-Side Data htmx makes it easy to send data to your server using attributes like `hx-post` or `hx-get`. But remember, just because it’s easy to send data doesn’t mean it’s safe. #### What to Avoid - Assuming data from the client is safe. - Using query strings or form data directly without validation. #### What to Do Instead - Always validate incoming data on the server. - Use model binding and server-side validation as your first line of defense. ## Handling Authentication and CSRF Protection Since htmx uses standard HTTP requests, the same security practices you apply to regular Razor Pages apply here. But there are some things you need to be extra careful about. ### CSRF Protection ASP.NET Core uses Anti-Forgery Tokens to prevent Cross-Site Request Forgery (CSRF) attacks. And guess what? htmx works well with this mechanism. #### Adding Anti-Forgery Tokens Make sure your forms include the `@AntiForgeryToken()` helper: ``` @Html.AntiForgeryToken() Post Comment ``` #### Validating Tokens On the server side, add the `[ValidateAntiForgeryToken]` attribute to your handlers. ``` [ValidateAntiForgeryToken] public IActionResult OnPostSubmit(string comment) { if (string.IsNullOrWhiteSpace(comment)) { return BadRequest("Comment cannot be empty."); } return Content($"{comment}", "text/html"); } ``` ### Authentication If you’re building a secure area of your site, make sure your htmx requests are properly authenticated. Razor Pages with `AuthorizeAttribute` work perfectly fine with htmx. #### Example ``` [Authorize] public class AdminModel : PageModel { public IActionResult OnGetSensitiveData() { return Content("Super secret information.", "text/html"); } } ``` If the user isn’t authenticated, htmx will simply display the login page in the target element. Not ideal, but definitely secure. ## Best Practices for Secure htmx-Based Applications To make sure you’re keeping your htmx-powered apps safe, follow these guidelines: ### 1. Always Validate Data on the Server Never trust the client. Use model binding, validation attributes, and manual validation as needed. ### 2. Implement CSRF Protection Use `@Html.AntiForgeryToken()` and the `[ValidateAntiForgeryToken]` attribute to ensure requests are coming from authenticated users. ### 3. Use `Authorize` Attributes Where Needed If certain actions require authentication, mark them with `[Authorize]`. htmx will gracefully handle unauthorized requests by returning the login page or an error message. ### 4. Don’t Return Sensitive Data Without Authentication Always ensure your backend checks user permissions before serving sensitive data. Just because a request comes from htmx doesn’t mean it’s trustworthy. ### 5. Monitor Your Logs Keep an eye on your server logs for suspicious requests. htmx requests will appear like any other HTTP request, so ensure you have proper logging in place. ## Conclusion Security should never be an afterthought. By applying these best practices, you can ensure that your htmx-powered Razor Pages apps remain safe and sound. Next time, we’ll cover some performance tips to make your apps feel even more responsive. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Debugging htmx in ASP.NET Razor Pages: Tips, Tricks, and Tools](https://www.woodruff.dev/debugging-htmx-in-asp-net-razor-pages-tips-tricks-and-tools/) **Published:** April 6, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, fellow developer! So you’ve embraced htmx to make your ASP.NET Razor Pages more interactive and excellent. But just like anything in development, things can go wrong. And when they do, you need to know how to fix them. Today, we’re diving into debugging HTMX requests and responses like a pro. **Content:** Welcome back, fellow developer! So you’ve embraced htmx to make your ASP.NET Razor Pages more interactive and excellent. But just like anything in development, things can go wrong. And when they do, you need to know how to fix them. Today, we’re diving into debugging HTMX requests and responses like a pro. ## Common Issues When Working with htmx Before we get into the troubleshooting techniques, let’s talk about the usual suspects that cause htmx-related bugs. ### 1. Incorrect Endpoints If you’ve ever written a bad URL in your `hx-get` or `hx-post`, you know what I’m talking about. htmx won’t complain. It’ll just send a request to nowhere. ### 2. Missing `hx-target` You’ve set up your `hx-get` request, but nothing’s happening. Nine times out of ten, it’s because you forgot to specify where to render the response. ### 3. Wrong HTTP Method Trying to `hx-post` but your server-side method is handling `OnGet()` instead of `OnPost()`? That’ll do it. ### 4. Returning Plain Text Instead of HTML htmx expects HTML, not JSON. If you return JSON without setting up the right expectations, it won’t know what to do with it. ## How to Debug htmx Requests and Responses Luckily, htmx provides some helpful tools to make debugging easier. Let’s walk through them. ### 1. Using Browser DevTools This should be your first stop. - Open the **Network Tab** in your browser’s DevTools. - Filter by **XHR** to see your htmx requests. - Check the **Request URL**, **Method**, and **Response** to verify everything is wired up correctly. ### Example Let’s say you have a form that adds new comments to a list. #### Comments.cshtml ``` @page @model CommentsModel Comments Comments @foreach (var comment in Model.Comments) { @comment } Post Comment ``` #### Comments.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; namespace YourNamespace.Pages; public class CommentsModel : PageModel { public static List Comments = new() { "First comment", "Second comment" }; public void OnGet() { } public IActionResult OnPostAddComment(string comment) { if (!string.IsNullOrWhiteSpace(comment)) { Comments.Add(comment); return Content($"{comment}", "text/html"); } return BadRequest("Comment cannot be empty."); } } ``` ### What to Check - Is the request URL correct? It should be `/Comments/AddComment`. - Is the request method correct? It should be **POST**, not **GET**. - Is the server returning valid HTML? Use the **Response** tab in DevTools to confirm. ### 2. Enabling htmx Debug Mode htmx comes with a debug mode providing extra console logging. To enable it, add this line to your HTML: ``` ``` Now, every htmx interaction will be logged to your browser console. You’ll see what’s being sent, what’s received, and where things go wrong. ### 3. Using `hx-on:error` You can add an `hx-on:error` attribute to catch errors and display them gracefully. ``` ``` This will pop up an alert if something goes wrong on the server side. ### 4. Checking Server Logs Always check your server logs for exceptions or errors. If your C# handler throws an exception, you’ll see it here. ## Tools and Techniques for Better Development Workflow To make your htmx development workflow smoother, consider the following: ### Using Hotwire for Local Development [Hotwire](https://hotwire.io/frameworks/aspdotnet) is a Python-based tool for rapid prototyping. It lets you simulate backend responses without running a real server. ### Using `hx-boost` and `hx-trigger` Wisely - Make sure your triggers are appropriate for the task. For example, `hx-trigger="click"` is common but not always necessary. - Consider using `hx-trigger="changed"` for form fields to update content as the user types. ### Setting `hx-swap` Correctly - If you want to **append content** instead of replacing it, use `hx-swap="beforeend"`. - If you want to **replace the entire element**, stick with `hx-swap="outerHTML"` (the default). ## Recap - Always check your URLs, HTTP methods, and server responses. - Use browser DevTools to inspect requests and responses. - Enable htmx debugging with `htmx.logAll()` for better visibility. - Use `hx-on:error` to handle issues gracefully. htmx is powerful, but knowing how to debug it effectively makes it even better. Next time, we’ll dive into performance tuning and making your htmx apps feel buttery smooth. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Boosting Razor Pages UX: The htmx Upgrade You Need](https://www.woodruff.dev/boosting-razor-pages-ux-the-htmx-upgrade-you-need/) **Published:** April 5, 2025 **Author:** Chris Woodruff **Excerpt:** Hey there, fellow .NET developer! Today, we’re talking about how you can take your ASP.NET Razor Pages apps from "meh" to "whoa!" with htmx. We’re diving into seamless navigation, enhancing user interactions, and even doing a little case study to show you how this all comes together. Ready? Let’s go. **Content:** Hey there, fellow .NET developer! Today, we’re talking about how you can take your ASP.NET Razor Pages apps from “meh” to “whoa!” with htmx. We’re diving into seamless navigation, enhancing user interactions, and even doing a little case study to show you how this all comes together. Ready? Let’s go. ## The Problem With Traditional Razor Pages Don’t get me wrong. Razor Pages are fantastic. They let you build dynamic web apps with clean separation of concerns. But when it comes to adding interactivity, you often find yourself writing JavaScript to handle basic stuff. And if you’re not careful, that JavaScript snowballs into a giant mess. ## Enter htmx: Your UI Sidekick htmx makes it incredibly easy to enhance user experience by letting your server do the heavy lifting. You don’t need a ton of client-side JavaScript. Just some clever HTML attributes and your server-side C# logic. ### How htmx Enhances UX - **Seamless Navigation:** Load new content without full-page refreshes. - **Dynamic Interactions:** Update parts of a page without touching JavaScript. - **Progressive Enhancement:** Everything still works even if JavaScript is disabled. ## Seamless Navigation With htmx Let’s say you have a blog with multiple articles. Normally, clicking on an article would trigger a full-page reload. But what if we could just load the content in place? ### Example: Loading Articles Without Refresh #### Index.cshtml ``` @page @model IndexModel Blog Articles Blog Articles @foreach (var article in Model.Articles) { @article.Title } Select an article to read. ``` #### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; namespace YourNamespace.Pages; public class IndexModel : PageModel { public List Articles { get; set; } = new() { new Article { Id = 1, Title = "Understanding HTMX" }, new Article { Id = 2, Title = "Razor Pages Best Practices" } }; public IActionResult OnGetLoadArticle(int id) { var content = id switch { 1 => "Understanding HTMXHTMX makes HTML fun again!", 2 => "Razor Pages Best PracticesKeep your views clean and your logic lean.", _ => "Article not found." }; return Content(content, "text/html"); } public class Article { public int Id { get; set; } public string Title { get; set; } } } ``` ### What’s Happening Here - We’re using `hx-get` to load article content from the server. - `hx-target="#content"` specifies where the content should be rendered. - No page reloads. No JavaScript. Just pure Razor Pages magic. ## Enhancing User Interactions Now, let’s take a simple form submission and make it feel snappier with htmx. ### Example: Adding a Comment Without Reloading #### Comments.cshtml ``` @page @model CommentsModel Comments Comments @foreach (var comment in Model.Comments) { @comment } Post Comment ``` #### Comments.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; namespace YourNamespace.Pages; public class CommentsModel : PageModel { public static List Comments = new() { "Great post!", "Very informative." }; public void OnGet() { } public IActionResult OnPostAddComment(string comment) { if (!string.IsNullOrWhiteSpace(comment)) { Comments.Add(comment); var newCommentHtml = $"{comment}"; return Content(newCommentHtml, "text/html"); } return BadRequest("Comment cannot be empty."); } } ``` ### Why This Works - The form uses `hx-post` to send data to the server and immediately inject the response. - No need for AJAX setup, JavaScript event listeners, or weird client-side state handling. ## Case Study: Improving an Existing App with htmx Imagine you have a Razor Pages app that displays a product catalog. Normally, clicking a product link would take you to a new page. With htmx, you can make this feel much more interactive. ### Before htmx - Clicking a product triggers a full-page reload. - Navigation feels clunky and slow. ### After htmx - Clicking a product loads the details dynamically via `hx-get`. - The user stays on the same page, with the content seamlessly replaced. ## Why htmx Improves UX - It reduces page reloads, making navigation feel instantaneous. - It allows you to load only the parts of the page that change. - It plays nicely with server-rendered HTML, so you don’t have to throw away everything you know. htmx is a fantastic way to upgrade the UX of your ASP.NET Razor Pages apps without going down the JavaScript framework rabbit hole. Give it a try and watch your apps become faster, simpler, and way more enjoyable to use. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [htmx vs. JavaScript Frameworks: Choosing the Right Tool for the Job](https://www.woodruff.dev/htmx-vs-javascript-frameworks-choosing-the-right-tool-for-the-job/) **Published:** April 4, 2025 **Author:** Chris Woodruff **Excerpt:** Alright, web developers. Today, we’re stepping into the ring to watch htmx go toe-to-toe with the big players: React, Vue, and Angular. But this isn’t a deathmatch. It’s about finding the right tool for the right job. Let’s get into it. **Content:** Alright, web developers. Today, we’re stepping into the ring to watch htmx go toe-to-toe with the big players: React, Vue, and Angular. But this isn’t a deathmatch. It’s about finding the right tool for the right job. Let’s get into it. ## What’s the Difference? JavaScript frameworks like React, Vue, and Angular are designed for building full-blown client-side applications. They offer: - **State Management:** Fancy mechanisms for handling complex data structures. - **Routing:** Single Page Applications (SPAs) that update the view without refreshing. - **Component-Based Architecture:** Reusable building blocks to keep code organized. But here’s the catch. They’re also: - **Heavy:** Your users have to download and parse tons of JavaScript. - **Complex:** State management, hooks, virtual DOM, build tools – it’s a lot. - **Overkill for Simple Use Cases:** Sometimes, you just need a form to submit asynchronously. ### Where htmx Fits In htmx is built for situations where you want interactivity without the baggage. It’s simple, small, and doesn’t require a mountain of tooling. ## Comparing htmx with React, Vue, and Angular FeaturehtmxReact / Vue / AngularLearning CurveSuper LowModerate to HighBundle Size~10KBHundreds of KBsServer-Side RenderingBuilt-InRequires SetupState ManagementHandled by ServerRequires Client-Side SolutionsUse CasesCRUD, Forms, DashboardsSPAs, Rich UI, Complex Apps## Scenarios Where htmx Shines Let’s be real. If you’re building something like Facebook, you’re going to need a heavy-duty JavaScript framework. But most of us aren’t building Facebook. So, when is htmx the better choice? ### 1. Basic CRUD Applications If your app is mostly about retrieving, updating, and displaying data, htmx is a great fit. No need to ship a bloated front-end library just to create a To-Do List or a simple blog. ### 2. Server-Rendered Apps ASP.NET Core developers are already used to building apps with server-side rendering. htmx lets you keep most of your logic on the server, where C# is your best friend. ### 3. Incremental Upgrades Have an existing app that needs a little interactivity? You can sprinkle htmx over your existing HTML without having to rewrite your entire frontend. ### Example: Editing a List of Items Imagine you have a list of items you want to edit in place. #### Index.cshtml ``` @page @model IndexModel Editing Items with htmx Item List @foreach (var item in Model.Items) { @item.Name Edit } ``` #### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; using System.Linq; namespace YourNamespace.Pages; public class IndexModel : PageModel { public List Items { get; set; } = new() { new Item { Id = 1, Name = "Item One" }, new Item { Id = 2, Name = "Item Two" } }; public IActionResult OnGetEditItem(int id) { var item = Items.FirstOrDefault(i => i.Id == id); if (item == null) return NotFound(); var editForm = $"" + $"" + $"" + "Save" + ""; return Content(editForm, "text/html"); } public IActionResult OnPutUpdateItem(int id, string name) { var item = Items.FirstOrDefault(i => i.Id == id); if (item == null) return NotFound(); item.Name = name; return Content($"{item.Name} " + $"Edit", "text/html"); } public class Item { public int Id { get; set; } public string Name { get; set; } } } ``` ## Mixing htmx with JavaScript Frameworks Okay, so what if you want the simplicity of htmx but you also need a fancy component here and there? Easy. You can use HTMX for most of your application and sprinkle in something like Vue or React where it makes sense. ### Hybrid Approach Example You can use htmx for most of your application, but when you need more complex UI elements, you can use a React component. ``` ``` Your React component can live on the same page, and you don’t have to go full SPA mode. ## The Bottom Line If you need blazing fast interactivity without the hassle of setting up a complex front-end stack, htmx is a fantastic choice. It’s not here to replace React, Vue, or Angular. Instead, it offers a simpler, more efficient way to build apps when you don’t need the heavy artillery. Stick around, because next time, we’ll dive into more practical use cases and how to make the most of htmx in your ASP.NET Core applications. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [htmx for ASP.NET Core Developers: The Simpler, Faster Way to Build Web Apps](https://www.woodruff.dev/htmx-for-asp-net-core-developers-the-simpler-faster-way-to-build-web-apps/) **Published:** April 3, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, fellow .NET enthusiast. Today, we’re talking about why htmx is a game-changer for ASP.NET Core developers. If you’re tired of drowning in client-side frameworks and you want something simpler without sacrificing performance, you’re in the right place. **Content:** Welcome back, fellow .NET enthusiast. Today, we’re talking about why htmx is a game-changer for ASP.NET Core developers. If you’re tired of drowning in client-side frameworks and you want something simpler without sacrificing performance, you’re in the right place. ## Finding the Right Balance Modern web development feels like it’s all about fancy JavaScript frameworks. But if we’re honest, most applications don’t need the complexity of SPAs (Single Page Applications). In fact, most apps just need a good balance between server-side rendering and some dynamic interactivity. This is where htmx shines. ### The Problem with SPAs - They’re heavy. Shipping megabytes of JavaScript to the client is a performance killer. - They’re complicated. State management, client-side routing, build tools – it’s easy to get lost in the weeds. - They force you to write a ton of boilerplate code just to handle basic interactivity. ### htmx to the Rescue htmx gives you the benefits of client-side interactivity without the overhead. You can still use server-side rendering, but you sprinkle in dynamic updates only where you need them. No need to build an entire front-end app just to make a button work. ## Performance Improvements Compared to SPA Frameworks Let’s talk about performance. Because htmx leaves most of the heavy lifting to your backend, your client-side bundle stays lightweight. You only send what you need, when you need it. ### Example: Dynamic Content Loading Imagine you have a list of blog posts and you want to load more posts when the user clicks a button. #### Index.cshtml ``` @page @model IndexModel htmx Performance Demo Blog Posts Load More Posts ``` #### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; namespace YourNamespace.Pages; public class IndexModel : PageModel { private static readonly List Posts = new() { "Post 1", "Post 2", "Post 3" }; public IActionResult OnGetLoadMorePosts() { var newPost = $"New Post - {Posts.Count + 1}"; Posts.Add(newPost); return Content(newPost, "text/html"); } } ``` ### Why This is Fast - You’re only fetching the HTML you need, not a giant JSON payload. - No virtual DOM diffing – just straight-up DOM manipulation. - You can optimize your C# server-side logic however you want. ## Simplifying Application Development The beauty of htmx is that it lets you keep your application simple. Your backend remains the brains of the operation, while htmx handles the UI interactions. And guess what? You don’t have to write JavaScript. ### What htmx Brings to the Table - **Server-Driven UI:** Keep your rendering logic where it belongs – in your Razor Pages. - **Minimal Client-Side Code:** Forget about complex JavaScript frameworks and their insane learning curves. - **Graceful Degradation:** If JavaScript fails, your app will still work as long as your server works. ### Example: Simple Form Submission This example shows how you can submit a form and update the UI without a full page refresh. #### Contact.cshtml ``` @page @model ContactModel Contact Form Contact Us Send ``` #### Contact.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace YourNamespace.Pages; public class ContactModel : PageModel { public IActionResult OnPostSubmitContact(string name, string email, string message) { var responseMessage = $"Thanks for reaching out, {name}. We will get back to you soon."; return Content(responseMessage, "text/html"); } } ``` ## Why htmx is a Game-Changer Let’s face it: most of your application’s logic already lives on the server. With htmx, you don’t have to split your app into two separate projects (frontend and backend). Instead, you enhance your existing Razor Pages to be more interactive without breaking your architecture. - It’s fast. - It’s simple. - And most importantly, it lets you focus on what matters – building features. htmx offers a refreshing approach to building web apps that feel modern without throwing away everything you know about ASP.NET Razor Pages. Next time, we’ll dive deeper into advanced patterns and how to use htmx for building real-world applications. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Going Modular: Using htmx with Partial Views in Razor Pages](https://www.woodruff.dev/going-modular-using-htmx-with-partial-views-in-razor-pages/) **Published:** April 2, 2025 **Author:** Chris Woodruff **Excerpt:** Hey there, fellow coder. Today, we’re diving into the wonderful world of partial views and how you can make them dance beautifully with htmx. If you’ve ever wanted to break down your Razor Pages into reusable, modular components, you’re going to love this. Let’s get rolling. **Content:** Hey there, fellow coder. Today, we’re diving into the wonderful world of partial views and how you can make them dance beautifully with htmx. If you’ve ever wanted to break down your Razor Pages into reusable, modular components, you’re going to love this. Let’s get rolling. ## Why Partial Views Matter Breaking your UI into smaller, reusable components is a great way to keep your code clean and organized. Razor Pages offers the `Partial()` method to render sections of HTML, but what if you want to **dynamically load or update these partials without refreshing the entire page**? That’s where htmx comes to the rescue. ## Setting Up the Project We’re going to build a simple dashboard where you can update different parts of the UI independently using partial views and htmx. Start by creating a new ASP.NET Razor Pages project: ``` dotnet new razor -n htmxPartialDemo ``` Add htmx to your `_Layout.cshtml`: ``` ``` ## Creating the Partial View Let’s say we have a simple user dashboard with a statistics panel that gets updated periodically. ### \_StatsPartial.cshtml ``` @model int User Statistics Total Users: @Model Refresh Stats ``` ## Adding the Backend Logic Now, we’ll build the page model to serve this partial view and update it upon request. ### Dashboard.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; namespace htmxPartialDemo.Pages; public class DashboardModel : PageModel { private static readonly Random Random = new(); public int TotalUsers { get; private set; } public void OnGet() { TotalUsers = Random.Next(1000, 5000); } public IActionResult OnGetStats() { var updatedCount = Random.Next(1000, 5000); return Partial("_StatsPartial", updatedCount); } } ``` ## Building the Main Page Here’s the main page where the partial view is included and updated dynamically. ### Dashboard.cshtml ``` @page @model htmxPartialDemo.Pages.DashboardModel htmx + Partial Views Dashboard @await Html.PartialAsync("_StatsPartial", Model.TotalUsers) Last refreshed at: @DateTime.Now.ToString("HH:mm:ss") ``` ## What’s Happening Here - The `_StatsPartial.cshtml` file is a simple partial view that renders the user count. - The `OnGetStats()` method in `Dashboard.cshtml.cs` serves the partial view asynchronously. - The button in the partial view triggers a GET request to the server using `hx-get` and replaces the content in the `#stats-partial` div using `hx-target`. ## Why This Approach Rocks - **Reusability:** You can use partial views all over your application and fetch them dynamically as needed. - **Performance:** Only the needed section of the page is updated, saving bandwidth and making your app feel snappy. - **Simplicity:** You’re still using Razor Pages, just enhanced with a touch of htmx magic. ## Expanding the Concept This technique works perfectly for: - Dashboards with various panels updated independently. - Multi-step forms where each step is a partial view. - Modals that load their content on demand. Next time, we’ll dive even deeper into building more advanced applications using htmx and Razor Pages. Stay tuned. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [CRUD Made Easy: Building Dynamic Apps with htmx and ASP.NET Razor Pages](https://www.woodruff.dev/crud-made-easy-building-dynamic-apps-with-htmx-and-asp-net-razor-pages/) **Published:** April 1, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, developer! Today, we’re tackling something everyone needs to build at some point: a CRUD (Create, Read, Update, Delete) application. And guess what? We’re going to do it without the JavaScript bloat. That’s right, htmx is here to make your CRUD dreams come true. **Content:** Welcome back, developer! Today, we’re tackling something everyone needs to build at some point: a CRUD (Create, Read, Update, Delete) application. And guess what? We’re going to do it without the JavaScript bloat. That’s right, htmx is here to make your CRUD dreams come true. ## What We’re Building We’re building a simple To-Do List where you can: - Add tasks (Create) - View tasks (Read) - Edit tasks (Update) - Delete tasks (Delete) The twist? We’re going to handle all interactions dynamically without reloading the page. ## Setting Up Your Project Start by creating a new ASP.NET Razor Pages project: ``` dotnet new razor -n htmxCrudDemo ``` Add htmx to your `_Layout.cshtml`: ``` ``` ## Building the Backend Let’s set up the backend to handle all our CRUD operations. ### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; using System.Linq; namespace htmxCrudDemo.Pages; public class IndexModel : PageModel { private static readonly List Tasks = new(); private static int _idCounter = 1; public IActionResult OnGetLoadTasks() { var tasksHtml = string.Join("", Tasks.Select(t => $"" + $"{t.Description} " + $"Edit " + $"Delete" + "")); return Content(tasksHtml, "text/html"); } public IActionResult OnPostAddTask(string description) { var newTask = new TaskItem { Id = _idCounter++, Description = description }; Tasks.Add(newTask); return Content($"" + $"{newTask.Description} " + $"Edit " + $"Delete" + "", "text/html"); } public IActionResult OnGetEditTask(int id) { var task = Tasks.FirstOrDefault(t => t.Id == id); if (task == null) return NotFound(); var editForm = $"" + $"" + $"" + "Save" + ""; return Content(editForm, "text/html"); } public IActionResult OnPutUpdateTask(int id, string description) { var task = Tasks.FirstOrDefault(t => t.Id == id); if (task == null) return NotFound(); task.Description = description; return Content($"" + $"{task.Description} " + $"Edit " + $"Delete" + "", "text/html"); } public IActionResult OnDeleteDeleteTask(int id) { var task = Tasks.FirstOrDefault(t => t.Id == id); if (task != null) Tasks.Remove(task); return Content(""); } private class TaskItem { public int Id { get; set; } public string Description { get; set; } } } ``` ## Building the Frontend Now, let’s put together the UI that interacts with our backend. ### Index.cshtml ``` @page @model IndexModel htmx CRUD Demo Task List Add Task ``` ## What’s Happening Here - **Creating Tasks:** The form at the top sends a POST request to `/AddTask` and injects the new task directly into the list without refreshing the page. - **Reading Tasks:** The task list is loaded from the server when the page loads thanks to `hx-get` and `hx-trigger="load"`. - **Updating Tasks:** Clicking an Edit button replaces the task’s HTML with a form that lets you update the description. Submitting the form sends a PUT request to update the task. - **Deleting Tasks:** Clicking a Delete button sends a DELETE request to the server, and the task is removed from the UI instantly. ## Why htmx Wins Here - It’s all server-driven, so you can stick with your familiar C# and Razor Pages. - You avoid JavaScript-heavy frameworks but still get all the interactivity you want. - Your UI code is simple and straightforward. No fancy front-end frameworks required. We’ve just scratched the surface of what’s possible with htmx and Razor Pages. Next time, we’ll explore more advanced topics and put all this together to build a more feature-rich application. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Real-Time Magic: Live Updates with htmx and ASP.NET Razor Pages](https://www.woodruff.dev/real-time-magic-live-updates-with-htmx-and-asp-net-razor-pages/) **Published:** March 31, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, code adventurer. Today, we’re diving into how htmx can give your ASP.NET Razor Pages some real-time flavor. Whether you want to build a chat app, a live stock ticker, or a dashboard that updates itself without manual refreshing, htmx has you covered. **Content:** Welcome back, code adventurer. Today, we’re diving into how htmx can give your ASP.NET Razor Pages some real-time flavor. Whether you want to build a chat app, a live stock ticker, or a dashboard that updates itself without manual refreshing, htmx has you covered. ## Why Real-Time Matters Traditional web apps usually rely on page reloads to fetch fresh data. But what if your users want that sweet, smooth experience where updates just appear magically on their screen? That’s where htmx comes into play. You might be thinking, “Isn’t this what SignalR is for?” And yes, SignalR is awesome for certain scenarios. But if you want something lightweight, simple, and doesn’t require websockets, htmx is your new best friend. ## How htmx Makes Real-Time Easy htmx allows you to poll the server at intervals and update parts of your page when new data is available. And the best part? It’s dead simple to set up. ### Example: Live Stock Price Updates Let’s say you want to build a stock ticker that shows live price updates without having to hit that refresh button. ### Index.cshtml ``` @page @model IndexModel Live Stock Prices Live Stock Prices Loading stock prices... ``` ### Index.cshtml.cs ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; using System.Collections.Generic; namespace YourNamespace.Pages; public class IndexModel : PageModel { public IActionResult OnGetGetStockPrices() { var random = new Random(); var stocks = new List { $"Apple: ${random.Next(150, 200)}.00", $"Microsoft: ${random.Next(250, 300)}.00", $"Google: ${random.Next(1000, 1500)}.00" }; var responseHtml = string.Join("", stocks); return Content(responseHtml, "text/html"); } } ``` ## What’s Happening Here - **hx-get:** Makes a GET request to `/Index?handler=GetStockPrices` to fetch the latest data. - **hx-trigger=”every 2s”:** Tells htmx to poll the server every 2 seconds. - **hx-target:** Specifies where the response should be injected. No page reloads, no WebSockets, and no need for client-side frameworks. It just works. ## Making Things Even Cooler What if you only want to update the stock prices when they change? htmx has a handy attribute for that: **hx-swap=”outerHTML”**. You can combine this with conditional server-side rendering to make sure you’re only sending fresh content when something actually changes. ## Example: Selective Updates Update your `Index.cshtml` like this: ``` Loading stock prices... ``` Now, the content will only be replaced if the server sends back something different. ## When To Use This Pattern - Live Dashboards - Notifications - Status Monitoring - News Feeds ## When Not To Use This Pattern - High-frequency data updates (Use SignalR instead). - Scenarios where maintaining a persistent connection is required. htmx gives you a low-effort way to provide a smooth real-time experience without the hassle of WebSockets or heavy client-side frameworks. Next time, we’ll explore more advanced real-time patterns to make your apps feel even more alive. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Interactive Forms Made Easy: htmx Meets ASP.NET Razor Pages](https://www.woodruff.dev/interactive-forms-made-easy-htmx-meets-asp-net-razor-pages/) **Published:** March 29, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back to the HTMX party! Today, we’re talking about a topic every web developer has wrestled with at some point: forms. They’re essential, but making them play nicely with your backend can sometimes feel like wrestling a bear. Enter HTMX. It’s here to simplify your form handling and make the experience feel smooth and modern. **Content:** Welcome back to the htmx party! Today, we’re talking about a topic every web developer has wrestled with at some point: forms. They’re essential, but making them play nicely with your backend can sometimes feel like wrestling a bear. Enter HTMX. It’s here to simplify your form handling and make the experience feel smooth and modern. ## Enhancing Form Submissions with htmx Normally, handling forms in ASP.NET Razor Pages means doing a full-page reload when you hit that submit button. Not terrible, but certainly not snappy. With htmx, you can make your forms interactive without all the overhead of a traditional JavaScript-heavy solution. ### Setting Up Your Project Before we dive into the code, make sure your project is set up with htmx. This means adding the following line to your `_Layout.cshtml` file: ``` ``` Done? Cool. Let’s get to it. ## Building a Search Feature with hx-post and hx-include Let’s imagine you’re building a simple search feature where you can type in a query and get the results instantly without refreshing the page. Here’s how to make it happen. ### Search Page (Index.cshtml) ``` @page @model IndexModel htmx Search Example Search for Products Search ``` ### Backend Handler (Index.cshtml.cs) ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; using System.Linq; namespace YourNamespace.Pages; public class IndexModel : PageModel { private static readonly List Products = new() { "Laptop", "Smartphone", "Keyboard", "Mouse", "Monitor", "Headphones" }; public IActionResult OnPostSearch(string query) { var results = Products .Where(p => p.Contains(query, StringComparison.OrdinalIgnoreCase)) .Select(p => $"{p}") .ToList(); var responseHtml = results.Any() ? string.Join("", results) : "No results found."; return Content(responseHtml, "text/html"); } } ``` ### What’s Going On Here? - The form is using `hx-post` to send the request to the server-side handler. No page reloads here. - `hx-target` is telling htmx to inject the results directly into the `#search-results` div. - You can still handle your request in pure C# on the server side. No JavaScript drama involved. ## Handling Validation and Responses Let’s say you want to provide some basic validation. Instead of refreshing the page or writing a ton of JavaScript, htmx makes this painless. ### Adding Basic Validation Update your `Index.cshtml` file like this: ``` Search ``` Now, the browser will handle the basic `required` validation. But let’s say you want your server to validate the query too. Here’s how you can do that: ### Updated Handler (Index.cshtml.cs) ``` public IActionResult OnPostSearch(string query) { if (string.IsNullOrWhiteSpace(query)) { return Content("Please enter a valid search query.", "text/html"); } var results = Products .Where(p => p.Contains(query, StringComparison.OrdinalIgnoreCase)) .Select(p => $"{p}") .ToList(); var responseHtml = results.Any() ? string.Join("", results) : "No results found."; return Content(responseHtml, "text/html"); } ``` ## What Did We Learn? - You can make your forms interactive without sacrificing server-side logic. - `hx-post` allows you to submit data without refreshing the page. - `hx-target` controls where the response goes. - Validation can be handled both client-side and server-side. In just a few lines of code, you’ve created a responsive, search-driven interface that feels modern and polished. And the best part? You didn’t have to wrestle with JavaScript at all. Stay tuned for the next post where we’ll dive into building full CRUD interfaces with htmx and ASP.NET Razor Pages. It’s about to get even better! **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Mastering htmx Attributes: Your Toolkit for Razor Pages Awesomeness](https://www.woodruff.dev/mastering-htmx-attributes-your-toolkit-for-razor-pages-awesomeness/) **Published:** March 28, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome back, coders! Now that you’ve dipped your toes into the htmx waters, it’s time to wade in a little deeper. Today, we’re diving headfirst into htmx attributes — the secret sauce that makes your Razor Pages dance with interactivity. So, buckle up and get ready for some fun! **Content:** Welcome back, coders! Now that you’ve dipped your toes into the htmx waters, it’s time to wade in a little deeper. Today, we’re diving headfirst into htmx attributes — the secret sauce that makes your Razor Pages dance with interactivity. So, buckle up and get ready for some fun! ## Understanding the Core htmx Attributes htmx gives you a bunch of powerful attributes to play with. Let’s break them down. ### hx-get, hx-post, hx-put, hx-delete These are your bread and butter when it comes to making HTTP requests from the client-side. - **hx-get:** Sends a GET request to the server. - **hx-post:** Sends a POST request, often used for submitting forms. - **hx-put:** Sends a PUT request, usually for updating data. - **hx-delete:** Sends a DELETE request, often for, you guessed it, deleting data. Here’s a quick example using **hx-get** and **hx-post**. #### `Index.cshtml` ``` @page @model IndexModel htmx Attributes Demo htmx Attributes Deep Dive Fetch Message Send Message ``` #### `Index.cshtml.cs` ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; namespace YourNamespace.Pages; public class IndexModel : PageModel { public IActionResult OnGetGetMessage() { var message = "Hello from the server!"; return Content($"{message}", "text/html"); } public IActionResult OnPostPostMessage(string message) { var responseMessage = $"You said: {message}"; return Content(responseMessage, "text/html"); } } ``` ## Targeting, Swapping, and Triggering Now let’s talk about how you can make these requests even cooler. ### hx-target This tells htmx where to inject the server’s response. In the previous example, `hx-target="#get-result"` ensures the message gets rendered right where you want it. ### hx-swap Controls **how** the response content gets swapped into the target element. Options include: - `outerHTML` (Default): Replaces the entire target element. - `innerHTML`: Replaces the contents of the target element. - `beforebegin`, `afterbegin`, `beforeend`, `afterend`: Places content in relation to the target element. Example usage: ``` Fetch Message ``` This will replace only the content **inside** the `#get-result` div. ### hx-trigger Decides **when** an htmx request should be made. You can trigger requests on events like `click`, `load`, `mouseover`, or even custom events. Example: ``` Loading message... ``` This will automatically trigger a request when the page loads. Super handy for preloading content. ## Making It All Work Together Here’s a little challenge for you: Let’s build a simple CRUD interface using these attributes. #### `Index.cshtml` ``` @page @model IndexModel CRUD with htmx Simple CRUD Interface Load Messages Add Message ``` #### `Index.cshtml.cs` ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.Collections.Generic; namespace YourNamespace.Pages; public class IndexModel : PageModel { private static readonly List Messages = new(); public IActionResult OnGetGetMessages() { var messagesHtml = string.Join("", Messages.Select(m => $"{m}")); return Content(messagesHtml, "text/html"); } public IActionResult OnPostAddMessage(string message) { if (!string.IsNullOrWhiteSpace(message)) { Messages.Add(message); } var messagesHtml = string.Join("", Messages.Select(m => $"{m}")); return Content(messagesHtml, "text/html"); } } ``` ## Why htmx Attributes Are Game-Changers When you combine all of this, htmx’s attributes make building interactive pages **simple and expressive**. No overkill frameworks. No complicated state management. Just Razor Pages doing what they do best: serving HTML, and htmx making it feel dynamic. Stay tuned for more because we’ve only scratched the surface of what htmx can do. Next, we’ll dive into advanced usage patterns that will make your Razor Pages even more powerful. **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [htmx + ASP.NET Razor Pages: Your First Dance with Interactivity](https://www.woodruff.dev/htmx-asp-net-razor-pages-your-first-dance-with-interactivity/) **Published:** March 27, 2025 **Author:** Chris Woodruff **Excerpt:** Hey there, fellow C# wizard! Ready to sprinkle some HTMX magic on your ASP.NET Razor Pages? You’re about to see how easy it is to get started with HTMX and add dynamic features without turning your app into a JavaScript spaghetti mess. Let’s boogie! **Content:** Hey there, fellow C# developers! Ready to sprinkle some htmx on your ASP.NET Razor Pages? You’re about to see how easy it is to get started with htmx and add dynamic features without turning your app into a JavaScript spaghetti mess. Let’s boogie! ## Installing and Configuring htmx in ASP.NET Core Good news: Installing htmx is like adding sprinkles to a cake. You don’t have to mess around with NPM or crazy build pipelines. Just pop it into your layout page. ### Step 1: Create a New ASP.NET Core Razor Pages Project First, make sure you have .NET 8 installed and create a new Razor Pages project: ``` dotnet new razor -n htmxDemo ``` ### Step 2: Add htmx to Your Project The beauty of htmx is that you can just drop it in as a script tag. So, in your `_Layout.cshtml` file, add this line inside the `` tag: ``` ``` Boom! You’re all set up. No npm install nonsense. Now, let’s put this puppy to work. ## Basic htmx Syntax and Adding Attributes htmx works by using attributes that you slap onto your existing HTML. The main ones you’ll be using are: - `hx-get` – Makes a GET request to your server. - `hx-post` – Makes a POST request. - `hx-trigger` – Decides when the request should happen (like clicking a button). - `hx-target` – Where the returned HTML should be injected. Easy, right? Now, let’s build something fun. ## Your First Example: A Simple GET Request with htmx Let’s create a simple page that lets you request a greeting from the server. ### `Index.cshtml` ``` @page @model IndexModel htmx Demo Welcome to htmx + ASP.NET Razor Pages! Get Greeting ``` ### `Index.cshtml.cs` ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; namespace htmxDemo.Pages; public class IndexModel : PageModel { public IActionResult OnGetGetGreeting() { var greetingHtml = "Hello there! The time is " + DateTime.Now.ToString("HH:mm:ss") + ""; return Content(greetingHtml, "text/html"); } } ``` ### What Just Happened? - The `` tag has two essential htmx attributes: - `hx-get="/Index?handler=GetGreeting"` – A GET request is sent to the server when you click the button. - `hx-target="#greeting-area"` – The response gets injected right into the `div` with `id="greeting-area"`. - The C# handler `OnGetGetGreeting()` returns HTML content with a greeting message and the current time. ## Why This Is So Cool - You didn’t have to write JavaScript. - No need for client-side frameworks or complicated routing. - Everything stays nice and simple, just the way you like it. And that’s it! You’ve just built your first htmx-powered feature. Next up, we’ll take this to the next level by handling POST requests and building more interactive pages. [If you want to read more about htmx and ASP.NET Razor Pages, I have an online book with deeper examples and information.](https://aspnet-htmx.com/) **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Level Up Your Razor Pages: Meet htmx, Your New Best Friend](https://www.woodruff.dev/level-up-your-razor-pages-meet-htmx-your-new-best-friend/) **Published:** March 25, 2025 **Author:** Chris Woodruff **Excerpt:** Hey there, fellow ASP.NET developer! If you’re tired of wrestling with JavaScript frameworks or want to simplify your life, you’re in for a treat. Today, we’re diving into the wonderful world of HTMX and why it might just be the missing piece to make your ASP.NET Razor Pages even more awesome. **Content:** Hey there, fellow ASP.NET developer! If you’re tired of wrestling with JavaScript frameworks or want to simplify your life, you’re in for a treat. Today, we’re diving into the wonderful world of htmx and why it might just be the missing piece to make your ASP.NET Razor Pages even more awesome. ## What the Heck is htmx? Think of htmx as that cool, easy-going friend who shows up, doesn’t demand much, and still makes the party better. It’s a tiny JavaScript library (about 10KB) that lets you add interactivity to your web pages without writing mountains of JavaScript. In essence, htmx allows you to make AJAX requests, handle HTML swaps, and trigger events, all while keeping your server-side rendering game strong. It’s a simpler, less opinionated way to create dynamic web experiences that don’t make you pull your hair out. ## Why ASP.NET Developers Should Care You might be asking, “But isn’t JavaScript the way to go for dynamic content?” Well, sure. But JavaScript frameworks can feel like using a chainsaw when you only need a butter knife. htmx fits like a glove with ASP.NET Razor Pages because: - **Server-Side Rendering Stays Intact:** No need to throw away your server-rendered HTML. - **Tiny and Efficient:** You get dynamic behavior without bloating your app with megabytes of JavaScript libraries. - **No Build Tools Required:** Say goodbye to complicated Webpack setups or TypeScript configs. - **It Just Works:** Seriously, integrating htmx into an ASP.NET Razor Pages app is about as straightforward as it gets. ## Okay, Show Me Some Code! Let’s say you have a Razor Page that displays a list of products. You want to add a button that loads more products without reloading the whole page. Here’s how it looks with good ol’ htmx. ### Razor Page (`Index.cshtml`): ``` @page @model IndexModel htmx Demo Products Loading... Load More Products ``` ### Server-side Handler (`Index.cshtml.cs`): ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace YourNamespace.Pages; public class IndexModel : PageModel { public IActionResult OnGetProducts() { var productsHtml = "Product 1Product 2Product 3"; return Content(productsHtml, "text/html"); } public IActionResult OnGetLoadMore() { var moreProductsHtml = "Product 4Product 5Product 6"; return Content(moreProductsHtml, "text/html"); } } ``` ## What’s Going On Here? - The `div` with `id="product-list"` automatically loads content from `/Products` when the page loads. That’s what `hx-get="/Index?handler=Products"` and `hx-trigger="load"` do. - The button uses `hx-get="/Index?handler=LoadMore"` to fetch more products and plop them into the `product-list` div. Magic, right? ## Why htmx > Traditional JavaScript Frameworks Sure, JavaScript frameworks are powerful, but sometimes they’re overkill. Here’s why htmx is worth your attention: - **Less Code:** You write fewer lines to achieve similar interactivity. - **Server-Side Power:** Your C# code does the heavy lifting while htmx handles the UI interactions. - **Fast and Efficient**: No virtual DOM, no client-side rendering drama. These are just quick, server-driven updates. ## Ready to Jump In? Stick around if you’re ready to level up your ASP.NET Razor Pages with htmx. We’ll be diving into more HTMX magic in upcoming posts. Happy coding! [If you want to read more about htmx and ASP.NET Razor Pages, I have an online book with deeper examples and information.](https://aspnet-htmx.com/) **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [Back to the Past: How htmx is Reviving Server-Driven Web Development](https://www.woodruff.dev/back-to-the-past-how-htmx-is-reviving-server-driven-web-development/) **Published:** March 26, 2025 **Author:** Chris Woodruff **Excerpt:** Hey folks! Welcome back to the HTMX journey. Today, we’re diving into a bit of web development history and how HTMX is taking us forward... by looking backward. Cue the DeLorean, because we’re heading back to the days when server-driven development was all the rage. **Content:** Hey folks! Welcome back to the htmx journey. Today, we’re diving into a bit of web development history and how htmx is taking us forward… by looking backward. Cue the DeLorean because we’re heading back to the days when server-driven development was all the rage. ## The Evolution of Web Technologies Once upon a time, websites were just plain HTML, served straight from a server to your browser. And guess what? It worked! Sure, it was primarily static, but it was simple, effective, and didn’t require a degree in JavaScript wizardry. Then came the rise of **JavaScript frameworks—Angular, React, Vue—all promising rich client-side interactivity. They brought us SPAs (Single-Page Applications**), where the server’s only job was to throw data over the fence and say, “Good luck!” But here’s the thing: - Client-side frameworks introduced complexity. - They made simple things complicated. - They demanded build tools, module bundlers, transpilers—a whole toolbox just to get a web app running. ## Why We’re Coming Back to Server-Driven Development After years of struggling with massive client-side apps and their ever-growing complexity, developers are starting to say, “Wait a minute… Wasn’t it easier when the server did most of the work?” The truth is, for many web apps, you **don’t need a full SPA framework**. Instead, you can get 80% of the benefits with just a sprinkling of htmx. Here’s why developers are flocking back to server-driven setups: - **Better Performance:** HTML over the wire is surprisingly fast. - **Simplicity:** You don’t have to wrestle with state management libraries and build tools. - **SEO Friendly:** Since most rendering happens on the server, search engines don’t need fancy tricks to read your content. ## Enter htmx: Your New Best Friend So how does htmx make server-driven development cool again? You can add interactivity to your ASP.NET Razor Pages without a ton of JavaScript. Think of htmx as the bridge between simple server-rendered pages and the dynamic web experiences users love. ### Here’s a fun little example: Let’s say we want to add some interactivity to a basic ASP.NET Razor Page. #### `Index.cshtml` ``` @page @model IndexModel htmx History Example Welcome Back to Server-Driven Development! What's the time? ``` #### `Index.cshtml.cs` ``` using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System; namespace YourNamespace.Pages; public class IndexModel : PageModel { public IActionResult OnGetGetTime() { var currentTimeHtml = $"Current Time: {DateTime.Now:HH:mm:ss}"; return Content(currentTimeHtml, "text/html"); } } ``` ## What’s Happening Here? - The `button` is making a request to the server (`hx-get="/GetTime"`) and loading the response right into the `#time-area` div. - No need for writing JavaScript or handling the AJAX call manually. htmx does the heavy lifting for you. - It’s simple, clean, and easy to understand. ## How htmx Simplifies Frontend Development Unlike traditional frameworks that require complex client-side state management and rendering logic, htmx lets you keep your **C# Razor Pages as the brains of your app**. The front end just handles displaying the HTML the server sends back. htmx brings back the simplicity of server-side rendering while allowing you to sprinkle interactivity where it’s needed. No fancy build tools. No massive dependency trees. Just straightforward, developer-friendly code. ## Why This Matters for ASP.NET Developers If you’re building apps with Razor Pages, htmx is a perfect match. It’s like adding dynamic functionality to your pages without turning your app into a complicated beast. And the best part? Your existing skills with C# and Razor Pages are all you need. Stay tuned for the next post where we’ll build something even cooler with htmx. And as always, happy coding! [If you want to read more about htmx and ASP.NET Razor Pages, I have an online book with deeper examples and information.](https://aspnet-htmx.com/) **Categories:** htmx **Tags:** asp.net, asp.net core, htmx, web development, webdev --- ### [REST Constraint #6: Code on Demand—When, Why, and How to Use It](https://www.woodruff.dev/rest-constraint-6-code-on-demand-when-why-and-how-to-use-it/) **Published:** March 24, 2025 **Author:** Chris Woodruff **Excerpt:** Of all the REST constraints, Code on Demand is the one that most developers either overlook or actively avoid. Unlike the other five constraints, it is the only optional one, meaning a RESTful system doesn’t have to use it. But when applied correctly, it can unlock powerful capabilities in web applications. Let’s explore what Code on Demand is, when it makes sense to use it, and why most REST APIs don’t rely on it. **Content:** Of all the REST constraints, **Code on Demand** is the one that most developers either overlook or actively avoid. Unlike the other five constraints, it is the only **optional** one, meaning a RESTful system doesn’t have to use it. But when applied correctly, it can unlock powerful capabilities in web applications. Let’s explore what Code on Demand is, when it makes sense to use it, and why most REST APIs don’t rely on it. ## What Is Code on Demand? Code on Demand (CoD) allows a server to send **executable code** to the client, which the client can then run to extend its functionality. This could be JavaScript for a web page, a mobile app update, or even dynamically loaded UI components. The key idea is that instead of sending just raw data, the server can enhance the client’s capabilities by delivering logic along with it. What makes CoD different from other REST constraints is that it is not required for an API to be considered RESTful. Many APIs function perfectly well without ever needing to send executable code. However, in some cases, it can be a game-changer for user experience and system efficiency. ## When Does Code on Demand Make Sense? ### **Dynamic Functionality** Sometimes, an application needs to offload certain processing tasks from the server to the client. Instead of sending fully processed results, the server can send a small script that runs in the client environment, reducing server load and making applications more dynamic. For example, a RESTful web service might return JavaScript functions to be executed in a browser: ``` HTTP/1.1 200 OK Content-Type: application/javascript function updateUI(data) { document.getElementById('status').innerText = data.message; } ``` ### **Plugin Systems** Some platforms allow users to extend functionality by loading custom modules on demand. A RESTful API can provide these modules dynamically, enabling flexible feature extensions without requiring a full application redeployment. For instance, a website builder could provide REST endpoints that deliver JavaScript-based plugins for different templates and widgets. ### **htmx and Sending HTML as a REST Response** A modern take on Code on Demand is the **htmx** library, which enables servers to send HTML fragments as REST responses. This allows for dynamic, component-based updates without requiring full-page reloads or heavy JavaScript frameworks. For example, instead of returning JSON, an API can return pre-rendered HTML: ``` HTTP/1.1 200 OK Content-Type: text/html Breaking News New REST standards are emerging! ``` The client-side htmx library then seamlessly updates the DOM, reducing the complexity of building dynamic web applications. ## How Code on Demand Works in RESTful APIs The process of using Code on Demand follows a predictable pattern: 1. **Client Requests a Resource** The client makes an API call, either expecting raw data or executable code. 2. **Server Responds with Executable Code** The response contains JavaScript, WebAssembly, or another supported format. 3. **Client Executes the Code** The client runs the provided script to manipulate the UI, perform calculations, or interact with other services. While this process is simple, it requires careful handling to avoid security risks. ## Advantages & Caveats of Code on Demand ### **Pros of Using Code on Demand** - **Better User Experience**: Instead of making multiple API calls, the client can execute code locally, making interactions smoother and faster. - **Reduces Server Load**: Offloading logic to the client means the server doesn’t need to compute every detail. - **Enables Advanced Features**: Dynamic UI updates, custom behaviors, and lightweight application enhancements become easier to implement. ### **Cons and Risks of Code on Demand** - **Security Concerns**: Executing server-provided code on the client opens up the possibility of malicious scripts or vulnerabilities. - **Client Compatibility**: Not all clients support or allow execution of server-provided code (e.g., strict security policies in mobile apps or enterprise environments). - **Not Always Necessary**: Many REST APIs function perfectly well with traditional request-response data exchanges, making CoD an often-overlooked feature. ## Real-Life Examples of Code on Demand ### **Modern SPAs (Single-Page Applications)** Frameworks like React and Vue rely on dynamically loading JavaScript components as needed. While not strictly RESTful, they align with the Code on Demand principle by allowing servers to provide UI-enhancing scripts on request. ### **APIs That Provide Client-Side Plugins** Some services, like analytics platforms, deliver JavaScript snippets via RESTful endpoints. A client makes a request to an API and receives a tracking script that gets embedded into a webpage. For example, Google Analytics provides a REST-based script-loading mechanism: ``` ``` This approach enables easy updates without requiring developers to manually update their code. ## Conclusion Code on Demand is the **most optional** REST constraint, but when used correctly, it can unlock dynamic functionality, reduce server load, and improve user experiences. While security risks and client compatibility must be carefully managed, modern web applications increasingly rely on CoD principles, especially through JavaScript-based solutions like htmx and client-side plugins. With this post, we’ve now covered all six REST constraints. Each constraint plays a role in making RESTful systems scalable, efficient, and easy to work with. Whether you’re designing APIs, building web applications, or optimizing server interactions, understanding these constraints helps create better, more reliable systems. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Constraint #5: Embracing Layers for Flexibility and Scale](https://www.woodruff.dev/rest-constraint-5-embracing-layers-for-flexibility-and-scale/) **Published:** March 23, 2025 **Author:** Chris Woodruff **Excerpt:** Building a reliable, scalable web application is no easy feat. The internet is unpredictable, traffic surges happen, and security threats lurk around every corner. That’s why REST includes the Layered System constraint—a design principle that structures applications into multiple layers, each handling a specific role. Whether it’s distributing traffic, securing data, or managing services, layering helps RESTful systems stay resilient and adaptable. **Content:** Building a reliable, scalable web application is no easy feat. The internet is unpredictable, traffic surges happen, and security threats lurk around every corner. That’s why REST includes the **Layered System** constraint—a design principle that structures applications into multiple layers, each handling a specific role. Whether it’s distributing traffic, securing data, or managing services, layering helps RESTful systems stay resilient and adaptable. ## Layered Architecture Basics A **layered system** is like an assembly line where each station has a specific job. Instead of one giant component handling everything, responsibilities are divided into layers, making the system easier to manage and scale. In a RESTful API, layers might include: - **Load Balancers** that distribute incoming requests across multiple servers. - **Proxies** that route and filter requests before they reach the backend. - **API Gateways** that handle authentication, logging, and rate limiting. - **Application Services** that execute business logic and process data. - **Databases & Caches** that store and retrieve information efficiently. The key is that clients don’t need to know what’s happening behind the scenes. They send a request, and it gets handled—no need to worry about whether a load balancer, gateway, or microservice is involved. ## Why Use a Layered System in REST? Layered architecture brings significant benefits to RESTful applications, making them more **scalable, secure, and maintainable**. ### Scalability By distributing workloads across multiple layers, applications can handle growing traffic without a hitch. A load balancer ensures requests are evenly spread across servers, preventing bottlenecks. If demand increases, additional servers can be added to maintain performance. ### Security With layers in place, security policies can be enforced at different levels. Firewalls can filter incoming traffic, API gateways can manage authentication, and backend services can be isolated from direct external access. This makes it harder for attackers to compromise the system. ### Maintainability Each layer in a RESTful system has a clear purpose. The API gateway doesn’t need to know how data is stored in the database, and the frontend doesn’t need to understand backend business logic. This separation makes debugging, updating, and extending the application much easier. ## How to Implement a Layered System Layering isn’t just theoretical—it’s a best practice widely used in modern applications. Here’s how it works in action. ### API Gateways: The Traffic Managers An **API gateway** acts as the first point of contact for clients, managing authentication, rate limiting, and request routing. It simplifies client interactions by consolidating multiple backend services into a single entry point. Example: Instead of calling multiple services separately, a client makes a request to the API gateway, which then routes it appropriately. ``` Client → API Gateway → Authentication Service → User Service → Database ``` ### Microservices: Separating Concerns Instead of a monolithic backend, microservices break applications into smaller, focused services. A layered approach helps manage these services efficiently: ``` Client → API Gateway → Aggregator Service → Multiple Microservices → Database/Cache ``` Each microservice handles a specific function, improving modularity and making updates less risky. ## A Real-World Example: Request Flow in a Layered System Let’s say a user requests their order history from an e-commerce application. Here’s how the request flows through a layered architecture: 1. **Client → Load Balancer** The request first reaches a load balancer, which determines the best available API server to handle it. 2. **Load Balancer → API Gateway** The gateway checks authentication, enforces rate limits, and forwards the request. 3. **API Gateway → Application Service** The request reaches the appropriate backend service responsible for processing order history. 4. **Application Service → Database/Cache** The service fetches order details from a cache (if available) or queries the database. 5. **Response Travels Back** The data flows back through the same layers, ensuring security and efficient processing before reaching the client. ## Challenges of Layered Systems While layering offers many advantages, it also introduces some trade-offs that must be managed carefully. ### Increased Complexity The more layers you add, the harder it becomes to track down issues. Debugging requests that pass through multiple layers requires careful logging and monitoring to identify bottlenecks. ### Latency Concerns Each additional hop in a layered system introduces a small delay. If not optimized properly, these delays can add up and impact performance. Smart caching, asynchronous processing, and efficient routing can help minimize latency. ## Conclusion The **Layered System** constraint in REST ensures that applications remain scalable, secure, and maintainable. By separating concerns across different layers, services can evolve independently, handle increased traffic, and maintain security policies without disrupting the entire system. While layering introduces some complexity, the benefits far outweigh the challenges. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Constraint #4: Cacheable for Better Performance](https://www.woodruff.dev/rest-constraint-4-cacheable-for-better-performance/) **Published:** March 22, 2025 **Author:** Chris Woodruff **Excerpt:** The internet is fast—until it’s not. Nobody likes waiting for a sluggish API response, and overloaded servers don’t help either. That’s where caching comes in. RESTful APIs embrace caching to improve performance, reduce server load, and create a smoother user experience. But caching isn’t just about speed—it’s about efficiency. Let’s dive into how REST makes caching an essential part of scalable web applications. **Content:** The internet is fast—until it’s not. Nobody likes waiting for a sluggish API response, and overloaded servers don’t help either. That’s where caching comes in. RESTful APIs embrace caching to improve performance, reduce server load, and create a smoother user experience. But caching isn’t just about speed—it’s about efficiency. Let’s dive into how REST makes caching an essential part of scalable web applications. ## Why Caching Matters Imagine you’re running a popular online store, and thousands of customers request product details every second. If your server generates a fresh response for each request, it won’t take long before it starts struggling under the load. Instead, you can cache frequently requested data, allowing it to be served instantly without hitting the database every time. Caching helps in two major ways: - **Reduces Latency**: Cached responses eliminate processing time, making applications feel snappier. - **Lowers Server Load**: By serving precomputed responses, caching prevents redundant database queries and CPU cycles. For RESTful APIs, caching isn’t just an optimization—it’s an expectation. Every response should indicate whether it’s cacheable, ensuring that clients and intermediaries (like browsers and CDNs) know how to handle it. ## Key Concepts of RESTful Caching ### **Cache Control: Setting the Rules** The `Cache-Control` header tells clients how long they can keep a response before requesting a fresh copy. This prevents unnecessary requests while ensuring users get up-to-date information. Example: ``` Cache-Control: max-age=3600, must-revalidate ``` This means: - **max-age=3600**: The response can be cached for 3600 seconds (1 hour). - **must-revalidate**: After expiration, the client must check with the server before using the cached copy. Other useful cache headers include: - **Expires**: Sets an absolute expiration date. - **ETag**: A validation token that allows clients to check if cached content is still valid. ### **Public vs. Private Caches** Not all caches work the same way. Some responses should be cached **for everyone**, while others should be cached **per user**. - **Public Caches (CDNs)**: Shared caches store responses for multiple users. These are great for static assets, like images, CSS, and public API responses. - **Private Caches (Browsers)**: A user’s browser may cache responses specific to their session, like user dashboards or profile data. ## How to Implement Caching in REST APIs ### **Setting Cache Headers** For public resources that rarely change, use longer cache lifetimes: ``` Cache-Control: public, max-age=86400 ``` This tells browsers and CDNs to store the response for a full day. For user-specific data, keep caches private and shorter: ``` Cache-Control: private, max-age=300 ``` This ensures only the user’s device caches the response for five minutes. ### **Using Validation Tokens (ETag & If-None-Match)** Sometimes, you don’t want to store a response indefinitely but also don’t want to send unnecessary data. The `ETag` header helps by letting clients check if the content has changed. 1. The server sends an `ETag` (a unique identifier for the response content): ``` ETag: "abc123" ``` 2. The next time the client requests the same resource, it includes the `If-None-Match` header: ``` GET /products/42 If-None-Match: "abc123" ``` 3. If the content hasn’t changed, the server responds with: ``` HTTP/1.1 304 Not Modified ``` This tells the client to use its cached version instead of downloading the full response again. ## Examples of Caching in Action ### **GET Requests: The Perfect Fit for Caching** Since `GET` requests **don’t modify data**, they are perfect for caching. Product listings, blog posts, and public user profiles can all benefit from cached responses. Example: ``` GET /articles/123 Cache-Control: public, max-age=600 ``` This allows the article to be cached for 10 minutes before checking for updates. ### **CDN Integration: Offloading the Work** Content Delivery Networks (CDNs) help distribute cached content across multiple servers worldwide. This minimizes latency by serving users from the nearest location. For example, an API might serve product images via a CDN: ``` GET https://cdn.example.com/images/product-42.jpg Cache-Control: public, max-age=604800 ``` This ensures the image is cached for a week, reducing unnecessary traffic to the origin server. ## Pitfalls of Caching ### **Stale Data: When Old Information Sticks Around** A poorly configured cache can lead to outdated content being served long after it has changed. If a product’s price is updated but the cached response still shows the old price, users might see inconsistent information. Solution: Use `ETag` validation and cache expiration strategies to ensure updates are reflected quickly. ### **Over- or Under-Caching** Caching everything sounds great—until it isn’t. Some data, like real-time stock prices or user account balances, should **never** be cached because it needs to be up-to-date at all times. Conversely, failing to cache static data (like company logos) results in unnecessary server load and wasted bandwidth. Solution: Carefully choose what gets cached and for how long, balancing performance and accuracy. ## Conclusion Caching is a fundamental part of RESTful API design, improving speed, reducing load, and enhancing scalability. By leveraging cache headers, validation tokens, and CDNs, APIs can deliver blazing-fast responses while keeping data fresh. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Constraint #3: Going Stateless for Scalability](https://www.woodruff.dev/rest-constraint-3-going-stateless-for-scalability/) **Published:** March 21, 2025 **Author:** Chris Woodruff **Excerpt:** When it comes to building scalable web applications, statelessness is one of REST’s most powerful constraints. It simplifies architecture, makes scaling easier, and helps APIs handle massive traffic loads without breaking a sweat. But what does being “stateless” actually mean in REST, and why is it such a big deal? **Content:** When it comes to building scalable web applications, **statelessness** is one of REST’s most powerful constraints. It simplifies architecture, makes scaling easier, and helps APIs handle massive traffic loads without breaking a sweat. But what does being “stateless” actually mean in REST, and why is it such a big deal? ## What Does Stateless Mean in REST? In the world of REST, statelessness means that **each request from a client to a server must contain all the information needed to process it**. The server does not store any information about previous requests. It treats every request as if it is being seen for the first time. There’s no session memory, no hidden context—just a clean slate for every request. Imagine going to your favorite coffee shop, but instead of the barista remembering your usual order, you have to tell them exactly what you want every single time. That’s how a stateless API works: no assumptions, no past history, just the information provided in the request. For RESTful APIs, this means no session data is stored on the server between requests. If a client needs to keep track of something—like user authentication or shopping cart details—it must handle that on its own. ## Why Statelessness is Crucial for Large-Scale Systems The internet is filled with millions of requests every second, and if every server had to remember every user’s past actions, things would quickly spiral out of control. Statelessness prevents this chaos by keeping servers lean and efficient. This is especially crucial for large-scale distributed systems, where requests might hit different servers in a cluster. Without the burden of session storage, APIs can respond faster, distribute traffic better, and avoid the headaches of sticky sessions or user affinity. This makes it easy to scale horizontally—just add more servers, and any one of them can handle incoming requests without worrying about user state. ## How Statelessness Impacts API Design Since RESTful servers don’t remember past interactions, clients must be responsible for maintaining any necessary state. If a user logs in, the server won’t store that session information—it’s up to the client to send authentication details with every request. For example, instead of a session-based login: ``` POST /login { "username": "alice", "password": "securepassword" } ``` And then relying on a stored session, a stateless approach uses authentication tokens like JWT (JSON Web Tokens): ``` POST /login { "username": "alice", "password": "securepassword" } ``` Response: ``` { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI..." } ``` Now, every request must include this token: ``` GET /user/profile Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI... ``` The server can validate the token without needing to store session data, ensuring statelessness. ## Benefits of Statelessness ### Horizontal Scalability Since no session data is stored on the server, any instance in a server cluster can handle any request. This makes load balancing and scaling a breeze. Need to handle more traffic? Just add more servers—no special configuration required. ### Caching & Routing Efficiency Because every request is independent, responses can be cached efficiently without worrying about session dependencies. Additionally, requests can be routed to any available server, improving performance and reliability. ## Implementing Statelessness in REST APIs ### Authentication & Tokens Instead of session-based authentication, APIs should rely on token-based authentication, like JWT or OAuth tokens. These tokens are sent with each request, allowing the server to authenticate users without storing session data. ### Handling State on the Client If an application needs to maintain user state—such as a shopping cart or step-by-step form progress—it should store that state on the client side. Options include: - Browser-based storage (localStorage, sessionStorage, cookies) - Including state-related information in each request (query parameters, request headers) ## Common Challenges in Stateless APIs ### Legacy Systems with Server-Side Sessions Many traditional web applications were built around server-side sessions. Transitioning to a stateless model often requires a refactor, replacing session storage with tokens and client-side state management. ### Stateful Exceptions: When You Might Need Some State Some workflows, like multi-step transactions or payment processing, may require state tracking. In these cases, stateless APIs can implement stateful operations at the database level or use **temporary tokens** that persist for a single transaction. ## Conclusion Statelessness is a cornerstone of RESTful API design, enabling scalability, flexibility, and efficiency. By keeping the server free of session dependencies and requiring each request to be self-contained, RESTful systems can handle high loads, distribute traffic efficiently, and integrate seamlessly with caching and load-balancing strategies. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Constraint #2: Why Client-Server Separation is a Game Changer](https://www.woodruff.dev/rest-constraint-2-why-client-server-separation-is-a-game-changer/) **Published:** March 20, 2025 **Author:** Chris Woodruff **Excerpt:** The internet as we know it thrives on separation—specifically, the separation of clients and servers. It’s a fundamental principle of REST and a key factor in building scalable, maintainable, and flexible applications. But what exactly does this separation mean, and why does it matter? Let’s dive into the Client-Server constraint, one of REST’s six core architectural principles. **Content:** The internet as we know it thrives on separation—specifically, the separation of **clients** and **servers**. It’s a fundamental principle of REST and a key factor in building scalable, maintainable, and flexible applications. But what exactly does this separation mean, and why does it matter? Let’s dive into the **Client-Server constraint**, one of REST’s six core architectural principles. ## A Little History: Where Did the Client-Server Model Come From? Before the Internet became what it is today, computing was largely **monolithic**—a single system handled everything. As networks grew, **distributed computing** became necessary, leading to the rise of the **client-server model**. - **Clients**: The part of the system that interacts with users. - **Servers**: The part that processes requests, stores data, and manages business logic. This separation allowed systems to scale better and distribute workloads efficiently. When REST was introduced, it formalized this idea as a **core constraint**, ensuring a clean division between frontend and backend responsibilities. ## What Does Client-Server Separation Mean in REST? The **Client-Server constraint** in REST means that: 1. **The client handles user interactions and interface logic.** 2. **The server handles data storage, business logic, and request processing.** The two communicate over a standard interface (like HTTP), but neither depends on the other’s internal workings. This clear separation offers major benefits. ## Why Separating Clients and Servers is a Smart Move ### **1. Independent Evolution** Since clients and servers are **decoupled**, they can evolve independently: - A mobile app can get a fresh new design without changing the backend. - The server can move from a relational database to NoSQL without breaking clients. - APIs can serve multiple frontends (web, mobile, IoT) without knowing their specifics. ### **2. Scalability & Flexibility** - Web servers can be **scaled horizontally** without affecting clients. - Load balancers, caching layers, and microservices can be added behind the scenes. - Clients can be built with different technologies (React, Angular, Vue, Swift, Kotlin) without worrying about backend changes. ### **3. Security & Maintainability** - Security policies (authentication, CORS) are enforced at the server level. - The backend can enforce **role-based access control** without relying on clients. - Maintenance and updates can be done **without impacting users directly**. ## Real-World Examples: Where Do We See Client-Server in Action? ### **1. Web Apps & APIs** Most modern web applications are **Single-Page Applications (SPAs)** powered by RESTful APIs: - The frontend (React, Angular, Vue) sends `GET`, `POST`, `PUT`, and `DELETE` requests to a REST API. - The backend (Node.js, .NET, Java, Python) processes the requests and returns JSON responses. - The browser updates the UI dynamically without needing a full page refresh. ### **2. Mobile Apps & REST APIs** - A **native iOS or Android app** communicates with a REST API to fetch user data. - Multiple apps (mobile, web, desktop) can use the same API without changes. ### **3. Third-Party Integrations** - REST APIs allow clients to **consume external services** without needing direct access to databases. - Example: A weather app retrieves real-time forecasts via a RESTful API without knowing how the server processes that data. ## Best Practices for Maintaining a Clean Client-Server Separation ### **1. Loose Coupling: Clients Shouldn’t Know Too Much** - The frontend should request **resources, not database queries**. - Example: Instead of `GET /getUserData?id=5`, use `GET /users/5`. - Avoid exposing **backend data models** directly—use **DTOs (Data Transfer Objects)**. ### **2. Versioning: Don’t Break Clients** - If backend changes could impact existing clients, use API versioning. - Example: `GET /v1/users/5` → `GET /v2/users/5` when introducing breaking changes. - Another alternative is to use HTTP headers to signal the version. ### **3. Stateless Requests Keep it Scalable** - Every request should include all the necessary information—don’t rely on sessions. - Example: Instead of tracking user state on the server, use tokens (`Authorization: Bearer xyzToken`). ## Pitfalls & Anti-Patterns to Avoid ### **1. Tight Coupling: When Clients and Servers Know Too Much About Each Other** Bad example: - The frontend directly manipulates database tables instead of going through an API. - API responses are **tailored for a specific frontend**, making it impossible to reuse across different clients. Solution: - Keep the API **resource-based**, not UI-specific. - Allow multiple frontends to consume the same API. ### **2. Server-Side Rendering with Too Much Logic** - Some people think the architecture might **blur** the lines between client and server if your API generates fully rendered HTML pages instead of JSON/XML. I do not since I see libraries like htmx as a considerable benefit for web developers. We will discuss htmx in the 6th constraint. - REST encourages a **separation of concerns**—let the frontend handle rendering and UI. ## Conclusion: Why Client-Server Separation is a Big Deal The **Client-Server constraint** is fundamental to REST because it keeps things **modular, scalable, and flexible**. By keeping responsibilities separate, APIs can serve multiple frontends, scale efficiently, and evolve without breaking existing clients. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Constraint #1: The Power of a Uniform Interface](https://www.woodruff.dev/rest-constraint-1-the-power-of-a-uniform-interface/) **Published:** March 19, 2025 **Author:** Chris Woodruff **Excerpt:** A well-designed REST API isn’t just a random collection of endpoints—it follows a set of principles that make it predictable, scalable, and easy to use. One of the core constraints of REST is the Uniform Interface. This principle ensures that APIs are structured consistently, reducing complexity for both developers and systems interacting with them. **Content:** A well-designed REST API isn’t just a random collection of endpoints—it follows a set of principles that make it predictable, scalable, and easy to use. One of the core constraints of REST is the **Uniform Interface**. This principle ensures that APIs are structured consistently, reducing complexity for both developers and systems interacting with them. But what exactly does that mean, and why should you care? Let’s break it down. ## What is a Uniform Interface? In simple terms, a **Uniform Interface** means that every resource in a REST API follows a standard, predictable structure. The API uses: - **Consistent URLs** to identify resources. - **Standard HTTP methods** to interact with those resources. - **Self-descriptive messages** that provide all the information needed to process a request. - **Hypermedia (HATEOAS)** to guide clients through available actions. The goal? Make APIs easy to understand and use—whether you’re a first-time consumer or an experienced developer integrating multiple services. ## Why It Matters A uniform interface makes RESTful APIs: - **Easier to Learn** – Developers don’t have to memorize different patterns for different resources. - **Predictable** – Knowing the structure of one part of an API means you can guess the structure of another. - **Interoperable** – Different clients (browsers, mobile apps, IoT devices) can interact with the API similarly. Without a uniform interface, APIs become inconsistent, hard to navigate, and a nightmare to maintain. ## Key Principles of a Uniform Interface ### 1. Resource Identification: Keep URLs Simple & Predictable Each resource in a REST API should have a **unique, consistent URL**. #### Example: ``` GET /api/products GET /api/products/42 ``` - `/api/products` → Retrieves all products. - `/api/products/42` → Retrieves the product with ID 42. Why this works: - URLs should represent **nouns** (resources), not **verbs** (actions). - Keeping URLs consistent improves discoverability and usability. ![](https://woodruff.dev/wp-content/uploads/2025/03/2025-03-15_08-34-21-1024x580.png)### 2. Manipulation of Resources Through Representations Clients don’t directly interact with resources—they send **representations** (usually JSON or XML) to modify them. #### Example: ``` PUT /api/products/42 Content-Type: application/json { "name": "Updated Product", "price": 99.99 } ``` This request updates product 42 with new details. The API processes the JSON and modifies the resource accordingly. Key points: - **Standard HTTP methods** (`GET`, `POST`, `PUT`, `DELETE`) define operations. - Clients send structured data (JSON, XML) to modify resources. - The API should respond with a clear status code (`200 OK`, `201 Created`, etc.). ### 3. Self-Descriptive Messages: All the Information in One Request Every request should include enough information for the server to process it, without relying on previous interactions. #### Example: ``` GET /api/orders/123 Accept: application/json ``` And the response: ``` HTTP/1.1 200 OK Content-Type: application/json { "id": 123, "status": "shipped", "items": [ { "product": "Laptop", "quantity": 1 } ] } ``` Why this matters: - The `Accept` header tells the server what format the client expects. - The response includes `Content-Type`, so the client knows how to parse it. - APIs should **never assume** the client has prior knowledge. ### 4. HATEOAS: Guiding Clients with Hypermedia Hypermedia as the Engine of Application State (HATEOAS) means that APIs **guide** clients by providing links to related actions. #### Example Response: ``` { "id": 42, "name": "Green Sneakers", "links": { "update": "/api/products/42", "delete": "/api/products/42", "related": "/api/products?category=shoes" } } ``` The API response doesn’t just return data—it **tells the client what actions it can take next**. This improves discoverability and keeps clients loosely coupled to the API’s structure. ## Examples in Practice ### 1. Simple Endpoint Design A RESTful API should use clean, structured URLs that follow a pattern: ``` GET /api/users GET /api/users/5 POST /api/users PUT /api/users/5 DELETE /api/users/5 ``` A well-structured API follows this pattern across all resources—making it easy to learn and use. ### 2. Improved Developer Experience A uniform interface makes it easier for developers to work with APIs: - Faster onboarding: Developers can understand endpoints without digging through documentation. - Consistency: If `GET /users/5` works one way, `GET /products/5` should work the same. - Debugging: Issues are easier to diagnose when APIs follow predictable patterns. ## Common Pitfalls to Avoid ### 1. Overloading Endpoints Bad example: ``` GET /api/userActions?task=delete&id=5 ``` - This mixes multiple responsibilities (retrieving and deleting) into one endpoint. - Instead, use:`DELETE /api/users/5`Each resource should have a **clear, single purpose**. ### 2. Ignoring Content Negotiation Bad practice: - Always returning JSON, even when the client requests XML. - Ignoring the `Accept` header. Good practice: ``` GET /api/products/42 Accept: application/xml ``` The API should respond with XML if it supports it: ``` HTTP/1.1 200 OK Content-Type: application/xml ``` REST APIs should respect client preferences to improve interoperability. ## Conclusion The **Uniform Interface** is a fundamental REST constraint that ensures APIs are **predictable, consistent, and easy to use**. By following its principles—resource identification, representation-based manipulation, self-descriptive messages, and hypermedia—developers create APIs that are scalable and developer-friendly. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [RESTful API Design: Why Simplicity Wins](https://www.woodruff.dev/restful-api-design-why-simplicity-wins/) **Published:** March 18, 2025 **Author:** Chris Woodruff **Excerpt:** APIs are the backbone of modern applications, and getting their design right can mean the difference between an easy-to-use system and a frustrating mess. That’s where REST (Representational State Transfer) comes in. RESTful API design focuses on simplicity, scalability, and a resource-oriented approach that makes APIs intuitive and robust. Let’s explain why REST improves API design and helps developers build better systems. **Content:** APIs are the backbone of modern applications, and getting their design right can mean the difference between an easy-to-use system and a frustrating mess. That’s where REST (Representational State Transfer) comes in. RESTful API design focuses on simplicity, scalability, and a resource-oriented approach that makes APIs intuitive and robust. Let’s explain why REST improves API design and helps developers build better systems. ## Resource-Oriented: Focus on Nouns, Not Verbs One of REST’s most significant advantages is that it organizes APIs around resources, not actions. URLs should represent **things** (nouns) rather than **operations** (verbs). ### Example: A Product Catalog API - **Good RESTful Design:** - `GET /products` → Retrieve a list of products. - `GET /products/81` → Retrieve details about a specific product. - `POST /products` → Add a new product. - `PUT /products/81` → Update an existing product. - `DELETE /products/81` → Remove a product. - **Bad Design (Action-Based URLs):** - `GET /getAllProducts` - `POST /createNewProduct` - `DELETE /removeProduct?id=81` The RESTful approach makes APIs cleaner, more predictable, and easier to use. It also aligns with how the web naturally works. ## State Transfers: Guiding Users Through the Application REST isn’t just about fetching data; it’s about **navigating an application’s state**. Each response should include the data requested and relevant links to other actions the client might need next. This concept is known as **HATEOAS (Hypermedia as the Engine of Application State)**. ### Example: Retrieving a Product #### Request: ``` GET /products/81 HTTP/1.1 Host: api.example.com ``` #### Response: ``` HTTP/1.1 200 OK Content-Type: application/json { "id": 81, "name": "Green Sneakers", "color": "green", "price": 59.99, "links": { "update": "/products/81", "delete": "/products/81", "similar": "/products?color=green" } } ``` The response not only provides product details but also suggests related actions through hyperlinks, guiding the client toward the next steps dynamically. ## Uniform Interface: Consistency is Key REST APIs follow a **uniform interface**, making them easy to understand and use. The key elements of this approach include: 1. **Consistent URL Structure** - Logical URLs should identify resources (`/users/123`, `/orders/567`). - Filtering and queries should use standard parameters (`/products?color=green`). 2. **Standard HTTP Methods for CRUD Operations** - `GET` → Retrieve data. - `POST` → Create new data. - `PUT` → Update existing data. - `DELETE` → Remove data. 3. **Standard Response Codes** - `200 OK` → Successful request. - `201 Created` → Resource successfully created. - `204 No Content` → Resource successfully updated or deleted. - `304 Not Modified` → Resource was not updated due to the same state given. - `400 Bad Request` → Invalid input. - `403 Forbidden` → Requested method for resource not allowed. - `404 Not Found` → Requested resource doesn’t exist. - `405 Method Not Allowed` → The client is not authorized to access this method or API. - `500 Internal Server Error` → Something went wrong on the server. By sticking to these conventions, RESTful APIs ensure a predictable and developer-friendly experience. Below is the process I use when thinking about creating new RESTful APIs. It follows a logic review, ensuring I respond with the best HTTP response code. ![](https://woodruff.dev/wp-content/uploads/2025/03/rest-workflow-1024x575.png)## Loose Coupling: Independence Between Client and Server A well-designed REST API allows the **client and server to evolve separately**. As long as the API’s resource structure and responses remain consistent, changes can be made independently. ### Why This Matters: - A mobile app consuming a REST API doesn’t need to be rewritten every time the backend is updated. - A frontend team can build new features without waiting for backend changes. - Different clients (web, mobile, IoT devices) can seamlessly interact with the same API. This flexibility is a huge advantage, especially for large applications that need to scale and adapt over time. ## Conclusion REST improves API design by focusing on **clarity, consistency, and scalability**. It structures APIs around resources, provides a uniform way to interact with data, and keeps the client and server loosely coupled for long-term flexibility. By following RESTful principles, developers can build APIs that are easier to use, maintain, and scale—making everyone’s lives a little bit simpler. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [The REST Formula: Six Principles That Keep the Web Running Smoothly](https://www.woodruff.dev/the-rest-formula-six-principles-that-keep-the-web-running-smoothly/) **Published:** March 17, 2025 **Author:** Chris Woodruff **Excerpt:** REST isn’t just a set of suggestions—it’s built on a strict set of architectural principles that make it scalable, flexible, and resilient. These six constraints define what makes an API truly RESTful and ensure it can handle anything from simple web apps to large-scale distributed systems. Let’s break them down and see why they matter. **Content:** REST isn’t just a set of suggestions—it’s built on a strict set of architectural principles that make it scalable, flexible, and resilient. These six constraints define what makes an API truly RESTful and ensure it can handle anything from simple web apps to large-scale distributed systems. Let’s break them down and see why they matter. ## 1. Client-Server: Separation of Responsibilities At the core of REST is the idea that the **client (frontend)** and **server (backend)** should be separate, each focusing on its own role. - The **client** is responsible for displaying data and interacting with users. - The **server** handles data storage, business logic, and responding to client requests. This separation makes scaling easier. Need to support mobile apps, web apps, and IoT devices? That is no problem. The same REST API can serve them all without modification. ## 2. Stateless: Every Request Stands Alone In a RESTful system, every request from a client to a server must contain **all the necessary information** to understand and process it. The server **doesn’t store the client state** between requests. Why is this useful? - **Reliability:** If a request fails, retrying it works because no prior state is required. - **Scalability:** Since the server doesn’t have to remember anything about a client’s previous requests, it can handle many clients efficiently. **Example:** ``` GET /orders/1234 HTTP/1.1 Host: api.example.com Authorization: Bearer XYZToken ``` The request must include authentication and all required details, as the server won’t remember anything from previous interactions. ## 3. Caching: Speeding Things Up Some responses don’t need to be fetched from the server every single time. Caching allows frequently requested resources to be stored temporarily, improving performance and reducing server load. **How it works:** - Responses include cache headers like `Cache-Control` or `ETag`. - Clients or intermediate proxies can store responses and reuse them when appropriate. **Example Cache Headers:** ``` Cache-Control: max-age=3600 ETag: "abc123" ``` This tells the client, “You can reuse this response for the next hour unless something changes.” ## 4. Uniform Interface: Keeping Things Consistent A **uniform interface** ensures that all RESTful APIs follow the same principles, making them easier to use and understand. This is broken down into four key rules: ### 1. **Identification of Resources** - Every resource should have a unique URL. - Example: `/users/42` represents user #42. ### 2. **Manipulation via Representations** - Clients don’t interact with the resource directly; they send representations (like JSON) that modify it. - Example: ``` PUT /users/42 Content-Type: application/json { "name": "Alice Updated" } ``` ### 3. **Self-Descriptive Messages** - Requests and responses include enough information so the server or client knows how to handle them. - Example: `Content-Type: application/json` This tells the client how to interpret the response. ### 4. **Hypermedia as the Engine of Application State (HATEOAS)** - Responses include links to related actions. - Example: `{ "id": 42, "name": "Alice", "links": { "update": "/users/42", "delete": "/users/42" } }` This guides clients through the API dynamically. ## 5. Layered System: Scalability & Security REST APIs should be designed so that **clients don’t need to know what’s between them and the actual server**. This allows for: - **Load balancers** to distribute traffic. - **Firewalls** to filter requests. - **Proxies** to improve caching and security. A well-structured REST API doesn’t care if requests pass through multiple layers; each layer handles its job independently. ## 6. Code on Demand (The Hard One) The only optional constraint of REST, **Code on Demand**, allows the server to send **executable code** to the client, such as JavaScript or applets. This is commonly used in web applications to enhance functionality without requiring a full page reload. **Example:** - A web API providing JavaScript widgets that clients can use to dynamically update their UI. While powerful, this feature isn’t used as frequently because it can introduce security risks. ## How These Constraints Work Together These six REST constraints aren’t just random rules—they interconnect to create a **flexible, scalable, and resilient** system. - **Client-server separation** enables independent evolution of frontend and backend. - **Statelessness** makes APIs more scalable and reliable. - **Caching** reduces unnecessary server load, improving performance. - **Uniform interface** ensures consistency across different services and clients. - **Layered architecture** supports load balancing, security, and redundancy. - **Code on Demand (optional)** can enhance flexibility by allowing dynamic functionality. By following these principles, RESTful APIs provide a structured, efficient, and predictable way to build web services that can handle anything from simple applications to globally distributed platforms. --- ## Conclusion REST isn’t just about using `GET` and `POST`—it’s an entire architectural style designed to make systems scalable, flexible, and efficient. By understanding and applying these six constraints, developers can build APIs that are easy to use, maintain, and extend. Whether you’re designing a small app or a massive distributed system, sticking to these principles will keep your API running smoothly for years to come. **We will go through each in the following 6 days, so stay tuned!** **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST: From Dissertation to Dominating the Web](https://www.woodruff.dev/rest-from-dissertation-to-dominating-the-web/) **Published:** March 16, 2025 **Author:** Chris Woodruff **Excerpt:** The internet runs on a lot of things—electricity, servers, cat videos—but when it comes to how web applications communicate, REST has been leading the way for over two decades. But where did REST come from? And how did it go from an academic concept to powering the world’s most significant web services? Let’s dive into the origin and evolution of REST. **Content:** The internet runs on a lot of things—electricity, servers, cat videos—but when it comes to how web applications communicate, REST has been leading the way for over two decades. But where did REST come from? And how did it go from an academic concept to powering the world’s most significant web services? Let’s dive into the origin and evolution of REST. --- ## The Roy Fielding Effect: Where It All Began The story of REST starts in the late 1990s with **Roy Fielding**, a computer scientist and one of the key architects behind HTTP. While working on his Ph.D. dissertation at the University of California, Irvine, Fielding explored ways to design scalable, distributed systems that leveraged the web’s existing architecture. In 2000, he introduced the term **Representational State Transfer (REST)** in his dissertation, outlining a set of principles for designing networked applications. Instead of relying on complex, rigid protocols (like SOAP and RPC), REST proposed a more flexible and scalable way to interact with resources using standard HTTP methods like `GET`, `POST`, `PUT`, and `DELETE`. Fielding wasn’t just writing about REST—he was actively shaping the Internet’s foundational protocols. His work influenced **HTTP/1.1**, which incorporated many of the ideas that made REST so powerful. --- ## REST Takes Over: A Timeline of Adoption At first, REST was mainly an academic idea, but developers quickly realized it was a better approach for building APIs as the web grew. Here’s how it went mainstream: ### **Early 2000s: REST vs. SOAP** In the early 2000s, **SOAP (Simple Object Access Protocol)** was the dominant web service standard. SOAP required XML-based messaging, strict contracts, and heavyweight processing. Conversely, REST was lightweight, flexible, and worked naturally with the web’s existing infrastructure. ### **Mid-2000s: RESTful APIs Emerge** Companies started adopting REST for APIs, especially as web applications and mobile apps became more common. Instead of forcing clients to parse XML, REST APIs returned **JSON**, a lightweight and easy-to-parse data format. ### **Late 2000s – Early 2010s: The REST Boom** By this time, RESTful APIs had become the standard for web services. Major companies like **Twitter, Facebook, GitHub, and Google** built their APIs using REST, allowing developers to interact with their platforms easily. ### **Today: REST is Everywhere** While newer technologies like GraphQL and gRPC have emerged, REST remains the dominant API standard. RESTful APIs are the backbone of web and mobile development, from startups to enterprise applications. ![](https://woodruff.dev/wp-content/uploads/2025/03/2025-03-15_06-56-02-1024x397.png)--- ## Why REST Became the Standard REST wasn’t just another buzzword—it solved real problems. Here’s why it took over: - **Simplicity** – REST APIs use URLs and standard HTTP methods, making them easy to understand and implement. - **Flexibility** – Clients and servers can communicate without being tightly coupled. - **Leverages HTTP** – REST doesn’t reinvent the wheel; it uses existing web standards like caching, authentication, and statelessness. - **Scalability** – Since REST APIs are stateless, they scale well in distributed systems. --- ## REST in the Real World: Modern Examples Some of the most widely used public APIs follow REST principles: - **Twitter API** – Allows developers to fetch tweets, post content, and interact with user data. - **GitHub API** – Used to manage repositories, issues, and user authentication. - **Stripe API** – Powers payments for thousands of online businesses. - **Spotify API** – Lets developers access music data, create playlists, and control playback. While many of these companies have evolved their APIs with additional features (some using GraphQL), REST remains a core part of their architecture. --- ## Conclusion REST started as an academic concept but quickly became the foundation of web communication. Its simplicity, scalability, and ability to work with the web’s existing structure made it the clear choice for API design. Even as new technologies emerge, REST remains the default for modern web services—and it’s not going anywhere anytime soon. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [REST Explained: Why the Web Runs on This Simple Idea](https://www.woodruff.dev/rest-explained-why-the-web-runs-on-this-simple-idea/) **Published:** March 15, 2025 **Author:** Chris Woodruff **Excerpt:** If you've worked with web services, you've probably heard of REST. It's everywhere—from APIs powering your favorite apps to backend systems handling millions of users. But what exactly is REST, and why is it the go-to approach for modern web development? Let’s break it down. **Content:** If you’ve worked with web services, you’ve probably heard of REST. It’s everywhere—from APIs powering your favorite apps to backend systems handling millions of users. But what exactly is REST, and why is it the go-to approach for modern web development? Let’s break it down. --- ## What is REST? A Simple Definition REST stands for **Representational State Transfer**, and while that sounds like something from a PhD dissertation (because it is), the concept itself is pretty intuitive. At its core, REST is a set of architectural principles for designing networked applications. Instead of complex mechanisms like SOAP (Simple Object Access Protocol) or RPC (Remote Procedure Call), RESTful systems use simple HTTP requests to interact with resources. Think of REST as how the web was meant to work: clients request resources, and servers respond with representations of those resources. A resource could be anything—a user profile, a blog post, a product listing—and REST provides a consistent way to access, modify, and delete them using standard HTTP methods like `GET`, `POST`, `PUT`, and `DELETE`. ![](https://woodruff.dev/wp-content/uploads/2025/03/2025-03-15_06-56-58-1024x411.png)--- ## A Brief History: From SOAP to REST REST wasn’t created in a vacuum. It was introduced in **2000** by Roy Fielding in his doctoral dissertation at UC Irvine. Fielding, one of the key architects behind HTTP, observed that early web services were overly complex, often requiring rigid XML-based messaging protocols like SOAP. Back in the day, if you wanted to expose data from a web service, you often had to: - Define a strict XML schema. - Use heavy SOAP envelopes. - Deal with complex contracts that dictate precisely how data should be exchanged. Fielding proposed REST as an alternative: a lightweight, scalable, and flexible way to build web services by leveraging **stateless communication over HTTP**. Instead of sending bloated XML requests, REST APIs let clients simply send an HTTP request and get back a lightweight JSON (or even plain text or XML) response. ![](https://woodruff.dev/wp-content/uploads/2025/03/2025-03-15_06-56-02-1024x397.png)--- ## REST vs. Traditional Web Services To understand why REST won the web, let’s compare it to older approaches: FeatureRESTSOAP/RPC**Message Format**JSON, XML, or other lightweight formatsXML-based SOAP messages**Complexity**Simple, human-readableVerbose, requires strict message formats**Performance**Fast, minimal overheadSlower due to XML parsing and transport overhead**Scalability**Stateless, easier to scaleOften stateful, making scaling more complex**Ease of Use**Works with standard HTTP methodsRequires additional protocolsREST’s simplicity, speed, and ease of integration made it the dominant approach for APIs, while SOAP has mostly faded into niche use cases like enterprise applications and legacy systems. --- ## Why REST Matters ### 1. **It Leverages HTTP as Intended** RESTful APIs don’t reinvent the wheel—they use HTTP the way it was designed. Resources are accessed via `GET`, modified via `PUT` or `PATCH`, created via `POST`, and deleted via `DELETE`. ### 2. **It’s Scalable and Stateless** REST follows a stateless architecture, meaning each request contains all the necessary information to process it. This makes it much easier to scale compared to systems that maintain session state on the server. ### 3. **It’s Lightweight and Fast** By using formats like JSON, REST APIs can transmit data with minimal overhead, making them ideal for modern web and mobile applications. ### 4. **It’s Universally Supported** Because REST works over HTTP, it’s compatible with nearly every programming language and platform, making it the default choice for modern API development. ![2 common perspectives on architectural design ](https://woodruff.dev/wp-content/uploads/2025/03/2025-03-15_06-57-46-1024x575.png)--- ## Wrapping Up REST isn’t just another technology buzzword—it’s the foundation of most modern web APIs. Its simplicity, scalability, and efficiency have made it the standard for building distributed systems. Whether you’re calling an API to fetch data or designing your own, understanding REST is essential for working with today’s web technologies. **Categories:** HTTP REST **Tags:** HTTP, network, REST --- ### [Decoding HTTP Response Codes: What Your Browser Isn’t Telling You](https://www.woodruff.dev/decoding-http-response-codes-what-your-browser-isnt-telling-you/) **Published:** March 13, 2025 **Author:** Chris Woodruff **Excerpt:** Every time you visit a website, your browser and the server have a little chat. Sometimes, that conversation goes smoothly; sometimes, there’s a misunderstanding, and sometimes, everything falls apart. HTTP response codes communicate success, confusion, or complete failure. If you’ve ever seen a dreaded 404 Not Found or a mysterious 500 Internal Server Error, you’ve already encountered them. But what do these numbers actually mean, and why should you care? Let’s break it down. **Content:** Every time you visit a website, your browser and the server have a little chat. Sometimes, that conversation goes smoothly; sometimes, there’s a misunderstanding, and sometimes, everything falls apart. HTTP response codes communicate success, confusion, or complete failure. If you’ve ever seen a dreaded **404 Not Found** or a mysterious **500 Internal Server Error**, you’ve already encountered them. But what do these numbers actually mean, and why should you care? Let’s break it down. --- ## The Five Families of HTTP Status Codes HTTP response codes are grouped into five categories, each with its own purpose. Think of them as different types of replies in a conversation. ### 1xx: Informational – “Hang Tight, Something’s Happening” These codes are rarely seen in everyday browsing because they’re primarily for behind-the-scenes communication between client and server. They indicate that a request has been received and is being processed. - **100 Continue** – The server says, “Got your request, keep going.” - **101 Switching Protocols** – “We’re changing things up. Let’s upgrade to a new protocol.” ### 2xx: Success – “All Good!” This is the best category. Everything went according to plan, and the server successfully handled the request. - **200 OK** – The gold standard of success. Everything worked, and here’s your requested data. - **201 Created** – Used when a new resource has been created (like when you sign up for a new account). - **204 No Content** – “Request processed, but there’s nothing to return. Move along.” ### 3xx: Redirection – “You Need to Go Over There” Redirection codes mean the resource you’re looking for isn’t exactly where you thought it was, but don’t worry—the server is pointing you in the right direction. - **301 Moved Permanently** – “This page has moved. Update your bookmarks.” - **302 Found** – “The resource is somewhere else, but only for now.” - **304 Not Modified** – “You already have the latest version, so there is no need to download it again.” or “What you are trying to modify is not different from what you sent.” ### 4xx: Client Errors – “You Messed Up” These errors happen when the client (browser, app, user) makes a mistake. They usually mean the request was invalid or unauthorized. - **400 Bad Request** – “Something’s wrong with your request. Check your syntax.” - **401 Unauthorized** – “You need to log in before accessing this.” - **403 Forbidden** – “Even if you log in, you don’t have permission.” - **404 Not Found** – “Whatever you’re looking for isn’t here. Maybe it never was.” ### 5xx: Server Errors – “We Messed Up” This is when the problem is on the server’s end, and there’s nothing the client can do about it. - **500 Internal Server Error** – “Something broke on our end. Try again later.” - **502 Bad Gateway** – “We asked another server for help, but it didn’t respond correctly.” - **503 Service Unavailable** – “The server is too busy or down for maintenance.” --- ## Common HTTP Status Codes in the Wild Some codes appear way more often than others. Here are the usual suspects: - **200 OK** – Everything is fine. - **404 Not Found** – The page doesn’t exist. Maybe it used to, maybe it never did. - **500 Internal Server Error** – Something went wrong, but the server won’t tell you what. --- ## Using HTTP Status Codes Correctly Understanding HTTP response codes is great, but using them correctly in applications is even more important. Here’s how they should map to application logic: - **Returning 200 OK for everything? Bad idea.** Return a **404**, not a generic **200 if something isn’t found**. - **Use 201 Created when making new resources.** If a user submits a form to create a new post, the API should return **201 Created** instead of **200 OK**. - **When** **nothing is returned in the response body**, you should use **204 No** **Content for success. This will not only save on return size but also** tell the client an important message. - I like to return **304 Not Modified** when the backend system **does not change the state of a resource**. This is a great message when saving something identical to what is in your database or filesystem. - **Handle 429 Too Many Requests when rate limiting.** If a user or bot is hitting your API too fast, don’t just block them—return **429 Too Many Requests** with a message about rate limits. - **500s should be logged, not ignored.** If your app returns a **500 Internal Server Error**, it’s a sign that something’s wrong on the backend. Make sure errors are logged so they can be fixed. --- ## Conclusion HTTP response codes are the web’s way of communicating what’s happening. Whether you’re a developer troubleshooting an issue, an API designer structuring your endpoints, or just someone trying to understand why a webpage isn’t loading, knowing these codes can make your life much easier. So next time you see a **404 Not Found**, you’ll know exactly what’s happening. **Categories:** HTTP REST **Tags:** HTTP, network --- ### [Breaking Down HTTP: What Really Happens in a Request and Response](https://www.woodruff.dev/breaking-down-http-what-really-happens-in-a-request-and-response/) **Published:** March 12, 2025 **Author:** Chris Woodruff **Excerpt:** When you visit a website, stream a video, or send a form, your browser and a server are having a conversation. But what are they actually saying? Every interaction on the web boils down to HTTP requests and responses, which work together like a question-and-answer session between your browser (the client) and the server. Let's break them down. **Content:** When you visit a website, stream a video, or send a form, your browser and a server are having a conversation. But what are they actually saying? Every interaction on the web boils down to HTTP requests and responses, which work together like a question-and-answer session between your browser (the client) and the server. Let’s break them down. --- ## The Anatomy of an HTTP Request An HTTP request is what your browser (or any client) sends to a server to ask for something. It consists of three key parts: the start line, headers, and sometimes a body. ### 1. Start Line: The Opening Move This line tells the server what the client wants. It has three components: - **Method** – What action should be performed (e.g., `GET`, `POST`, `PUT`, `DELETE`). - **Path** – The resource being requested (e.g., `/homepage`, `/products/42`). - **HTTP Version** – The version of HTTP being used (e.g., `HTTP/1.1`, `HTTP/2`). #### Example Start Line: ``` GET /books HTTP/1.1 ``` This means: “Give me the list of books using HTTP/1.1.” ### 2. Headers: Extra Information for the Server Headers provide additional details about the request. Some common ones include: - **Host** – Specifies the domain name (`example.com`). - **User-Agent** – Identifies the client making the request (e.g., a browser or a mobile app). - **Accept** – Informs the server what type of response is expected (e.g., `application/json`, `text/html`). - **Content-Type** – Used in requests that send data (e.g., `application/json` for a JSON payload). #### Example Headers: ``` Host: example.com User-Agent: Mozilla/5.0 Accept: application/json ``` ### 3. Body: Sending Data to the Server Not all requests have a body. `GET` requests don’t need one since they’re just fetching data. But `POST` and `PUT` requests use the body to send data, like when submitting a form or updating a resource. #### Example Request with a Body (JSON Data): ``` POST /books HTTP/1.1 Host: example.com Content-Type: application/json { "title": "HTTP Made Simple", "author": "Jane Doe" } ``` This request tells the server to add a new book. --- ## The Anatomy of an HTTP Response Once the server processes a request, it responds with its own three-part message: the status line, headers, and body. ### 1. Status Line: The First Impression The first line in a response tells the client whether the request was successful. It includes: - **HTTP Version** – The version of HTTP being used. - **Status Code** – A three-digit code that describes the result. - **Status Message** – A short description of the status. #### Example Status Line: ``` HTTP/1.1 200 OK ``` This means: “Request was successful.” Some common status codes include: - **200 OK** – The request was successful. - **201 Created** – A new resource was successfully created. - **400 Bad Request** – The request was malformed or invalid. - **404 Not Found** – The requested resource doesn’t exist. - **500 Internal Server Error** – The server encountered an issue. ### 2. Headers: Extra Info from the Server Just like requests, responses include headers that give the client more details about what’s being sent back. #### Common Response Headers: - **Content-Type** – Tells the client what type of data is in the body (e.g., `application/json`, `text/html`). - **Content-Length** – Specifies the size of the response body in bytes. - **Server** – Identifies the software running on the server. #### Example Headers: ``` Content-Type: application/json Content-Length: 120 Server: Apache ``` ### 3. Body: The Actual Data This is the part of the response that contains the content the client requested—whether it’s an HTML page, JSON data, or an image. #### Example Response with JSON Data: ``` HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "title": "HTTP Made Simple", "author": "Jane Doe" } ``` If you were fetching a webpage instead, the response body would contain HTML: ``` HTTP/1.1 200 OK Content-Type: text/html Welcome Hello, world! ``` --- ## Conclusion Every time you interact with a website, HTTP requests and responses are working behind the scenes to make it happen. Requests bring clear instructions, and responses deliver the goods. Understanding these building blocks will help you troubleshoot issues, optimize API calls, and become a more effective web developer. **Categories:** HTTP REST **Tags:** HTTP, network --- ### [HTTP Methods: The Verbs That Make the Web Go Round](https://www.woodruff.dev/http-methods-the-verbs-that-make-the-web-go-round/) **Published:** March 11, 2025 **Author:** Chris Woodruff **Excerpt:** If HTTP were a language, its methods—also known as verbs—would be the action words that keep the internet running. Every time you load a webpage, submit a form, or delete a post, you're using one of these methods. Understanding them is key to working with web APIs, debugging issues, and just generally feeling like a web wizard. Let’s break them down. **Content:** If HTTP were a language, its methods—also known as verbs—would be the action words that keep the internet running. Every time you load a webpage, submit a form, or delete a post, you’re using one of these methods. Understanding them is key to working with web APIs, debugging issues, and just generally feeling like a web wizard. Let’s break them down. --- ## GET: Retrieving Resources The most common and innocent of all HTTP methods, `GET` is like knocking on a server’s door and asking politely for information. It doesn’t modify anything; it just fetches data. ### Example Request: ``` GET /books HTTP/1.1 Host: example.com ``` ### Example Response: ``` HTTP/1.1 200 OK Content-Type: application/json [ { "id": 1, "title": "The Pragmatic Programmer" }, { "id": 2, "title": "Clean Code" } ] ``` Since `GET` only retrieves data, it’s considered a **safe** method—no changes happen on the server. It’s also **idempotent**, meaning no matter how many times you send the request, the result is the same. --- ## POST: Creating or Submitting Data When you need to create a new resource, `POST` is your go-to method. Think of it as submitting a form to sign up for a newsletter or posting a comment. ### Example Request: ``` POST /books HTTP/1.1 Host: example.com Content-Type: application/json { "title": "You Don’t Know JS" } ``` ### Example Response: ``` HTTP/1.1 201 Created Location: /books/3 ``` Unlike `GET`, `POST` is **not idempotent**—sending the same request multiple times could create multiple resources (multiple books in this case). That’s why you should be careful with duplicate submissions. --- ## PUT: Updating Existing Data If `POST` is about creating, `PUT` is about updating. When you send a `PUT` request, you’re replacing an existing resource with a new version. ### Example Request: ``` PUT /books/1 HTTP/1.1 Host: example.com Content-Type: application/json { "title": "The Pragmatic Programmer (Updated Edition)" } ``` ### Example Response: ``` HTTP/1.1 200 OK ``` `PUT` is **idempotent**, meaning if you send the same request multiple times, the result is always the same. It fully replaces the resource, so if some fields are missing, they might be removed. --- ## DELETE: Removing Data The `DELETE` method does exactly what it sounds like—it removes a resource from the server. ### Example Request: ``` DELETE /books/2 HTTP/1.1 Host: example.com ``` ### Example Response: ``` HTTP/1.1 204 No Content ``` `DELETE` is technically **idempotent**, meaning if you delete the same resource multiple times, the result should be the same (the resource stays deleted). However, some implementations might return different responses on subsequent requests. --- ## Other HTTP Methods You Should Know While `GET`, `POST`, `PUT`, and `DELETE` get most of the attention, there are a few other HTTP methods worth knowing: - **PATCH** – Unlike `PUT`, which replaces an entire resource, `PATCH` only updates specific fields. - **HEAD** – Just like `GET`, but it only returns headers, not the body. - **OPTIONS** – Asks the server what methods are supported for a resource. --- ## Idempotency & Safety: Why They Matter ### **Safe Methods** A method is considered **safe** if it doesn’t modify the server’s state. `GET` and `HEAD` are safe because they only retrieve data. ### **Idempotent Methods** A method is **idempotent** if sending the same request multiple times has the same effect. `GET`, `PUT`, and `DELETE` are idempotent because calling them repeatedly doesn’t change the result. `POST`, on the other hand, is not idempotent—submitting the same form twice could create duplicate data. --- ## Conclusion HTTP methods are the backbone of web communication. Whether you’re building APIs, debugging network requests, or just trying to understand how the web works, knowing how these methods behave will make you a better developer. Master these, and you’ll never look at a network request the same way again. **Categories:** HTTP REST **Tags:** HTTP, network --- ### [HTTP Demystified: The Secret Sauce of the Web](https://www.woodruff.dev/http-demystified-the-secret-sauce-of-the-web/) **Published:** March 10, 2025 **Author:** Chris Woodruff **Excerpt:** The internet—our beloved realm of cat videos, memes, and, occasionally, productivity—wouldn’t be the same without HTTP. It’s the invisible magic behind every website you visit. But what exactly is it, and how does it work? Buckle up because we’re about to take a joyride through the world of HTTP! **Content:** The internet—our beloved realm of cat videos, memes, and, occasionally, productivity—wouldn’t be the same without HTTP. It’s the invisible magic behind every website you visit. But what exactly is it, and how does it work? Buckle up because we’re about to take a joyride through the world of HTTP! --- ## The Origins & Evolution of HTTP HTTP (HyperText Transfer Protocol) was born in the early 1990s when Sir Tim Berners-Lee (yes, the same guy who invented the World Wide Web) needed a way for browsers and servers to communicate. The first version, HTTP/0.9, was ultra-simple—just a request for a document and a response. Over the years, it evolved through multiple versions (HTTP/1.0, 1.1, 2, and now HTTP/3) to become faster, more secure, and efficient. Imagine HTTP as the waiter in a restaurant. You (the client) place an order, the waiter (HTTP) takes it to the kitchen (server), and then delivers your piping hot meal (response). Simple, right? Let’s break it down further. --- ## The Request-Response Cycle: Internet Ping Pong At its core, HTTP is all about conversations between clients and servers. Here’s how it goes down: 1. **You (the client) send a request** – This happens when you type a URL into your browser and hit enter. 2. **The server processes the request** – It looks at what you asked for and decides how to respond. 3. **The server sends a response** – If everything goes well, the server delivers the requested webpage, image, or data. Think of it like texting a friend: - You: “Hey, send me that pizza recipe!” - Friend: “Here it is!” (sends recipe) That’s HTTP in action! --- ## Key Components of an HTTP Request Every HTTP request and response is made up of a few key ingredients: - **URL (Uniform Resource Locator)** – The address of the thing you want (e.g., `https://example.com/pizza-recipe`). - **Method** – The type of request (e.g., `GET` to fetch data, `POST` to send data, `PUT` to update data). - **Headers** – Extra bits of information like content type, authentication details, and caching rules. - **Body** (optional) – Used in `POST` and `PUT` requests to send data (like a filled-out form). And when the server responds, it comes with: - **Status Code** – A number indicating success (`200 OK`), failure (`404 Not Found`), or server meltdowns (`500 Internal Server Error`). - **Headers** – Similar to request headers but sent from the server. - **Body** – The actual content, like an HTML page or JSON data. --- ## Basic Example: A Simple GET Request Let’s see HTTP in action with a classic GET request. ### Request: ``` GET /pizza-recipe HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 ``` ### Response: ``` HTTP/1.1 200 OK Content-Type: text/html Pizza Recipe Delicious Pizza Recipe Step 1: Preheat your oven... ``` And just like that, the page appears in your browser! --- ## Wrapping Up HTTP is the lifeblood of the web, making sure clients and servers talk to each other seamlessly. Understanding how it works isn’t just for backend developers—it’s useful for anyone who builds, troubleshoots, or even just wants to understand how their favorite sites work. Next time you visit a website, just remember: a lot is happening behind the scenes, and HTTP is making it all possible. Stay curious, and happy coding! **Categories:** HTTP REST **Tags:** HTTP, network --- ### [Disaster-Proof Your Cloud: Automating Recovery with Terraform](https://www.woodruff.dev/disaster-proof-your-cloud-automating-recovery-with-terraform/) **Published:** March 9, 2025 **Author:** Chris Woodruff **Excerpt:** Picture this: Your production system crashes at 2 AM. Servers are down. Databases are unreachable. Your inbox is exploding with alerts. Panic mode activated. That’s the power of Automated Disaster Recovery with Terraform. **Content:** Picture this: **Your production system crashes at 2 AM.** Servers are down. Databases are unreachable. Your inbox is exploding with alerts. **Panic mode activated.** Now imagine this instead: - Terraform detects the issue. - Terraform spins up new resources automatically. - Your system is back **before customers even notice**. That’s the power of **Automated Disaster Recovery with Terraform**. In this post, we’ll explore **how Terraform can help you bounce back from failures**—fast and stress-free. Let’s build a **self-healing, disaster-proof infrastructure!** --- ## **1. What is Disaster Recovery in Terraform?** Disaster recovery (DR) means **preparing for the worst**—whether it’s: - **A server crash** - **A region-wide outage** - **Accidental data deletion** - **A security breach** Terraform helps by: - **Automatically restoring infrastructure** after failures. - **Backing up Terraform state files** to prevent data loss. - **Scaling resources dynamically** to handle failures. Let’s break down how to **disaster-proof your infrastructure with Terraform**. --- ## **2. Enabling Auto-Recovery with Terraform** The best disaster recovery plan? **One that requires no human intervention.** ### **Example 1: Auto-Replacing Failed EC2 Instances in AWS** If a VM crashes, Terraform can **automatically detect and replace it** using an **Auto Scaling Group (ASG)**. ``` resource "aws_launch_configuration" "web" { name = "web-lc" image_id = "ami-123456" instance_type = "t3.micro" } resource "aws_autoscaling_group" "web" { desired_capacity = 2 max_size = 5 min_size = 1 launch_configuration = aws_launch_configuration.web.id } ``` **Now, AWS automatically replaces failed instances.** --- ### **Example 2: Auto-Recovery for Azure VMs** Azure lets you **automatically recreate virtual machines** when they fail: ``` resource "azurerm_virtual_machine_scale_set" "example" { name = "myScaleSet" location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name upgrade_policy_mode = "Automatic" sku { name = "Standard_DS1_v2" capacity = 3 } automatic_instance_repair { enabled = true } } ``` **Terraform ensures that lost VMs are restored instantly!** --- ## **3. Backing Up Terraform State for Disaster Recovery** Terraform tracks everything in **terraform.tfstate**—if you lose it, **you’re in trouble.** ### **Step 1: Store Terraform State Remotely** ``` terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1" encrypt = true } } ``` **Now, even if your local machine dies, Terraform state is safe!** ### **Step 2: Enable Versioning for State File Backups** ``` resource "aws_s3_bucket_versioning" "terraform_state" { bucket = aws_s3_bucket.terraform_state.id versioning_configuration { status = "Enabled" } } ``` **If someone accidentally deletes the state file, you can restore an older version!** --- ## **4. Using Terraform to Restore Backups Automatically** Let’s say your **database fails**. Terraform can **restore a backup automatically** using AWS RDS snapshots. ### **Example: Auto-Restoring an AWS RDS Database** ``` resource "aws_db_instance" "database" { identifier = "mydb" allocated_storage = 20 engine = "mysql" engine_version = "8.0" instance_class = "db.t3.micro" skip_final_snapshot = false backup_retention_period = 7 } ``` **Now, if your DB fails, Terraform restores it from the latest snapshot!** --- ## **5. Multi-Region Failover with Terraform** What if an **entire cloud region goes down**? You don’t want to **wait hours** for a fix—you need a **backup region ready to take over**. ### **Example: AWS Multi-Region Setup with Route 53 Failover** Terraform can configure a **DNS failover** to **switch traffic to a backup region automatically**. ``` resource "aws_route53_record" "failover" { zone_id = "Z123456" name = "myapp.example.com" type = "A" set_identifier = "primary" failover_routing_policy { type = "PRIMARY" } health_check_id = aws_route53_health_check.primary.id } ``` **If the primary region goes down, Route 53 redirects traffic to the backup region!** --- ## **6. Automating Disaster Recovery Testing** The **worst time to test your disaster recovery plan is during a real disaster.** Terraform can **simulate failures** using tools like **Chaos Monkey or AWS Fault Injection Simulator**. ### **Example: Using Terraform to Test AWS Failures** ``` resource "aws_fis_experiment_template" "terminate_instances" { name = "Terminate Instances Test" role_arn = "arn:aws:iam::123456789012:role/FISRole" action { action_id = "aws:ec2:terminate-instances" } } ``` **Now, Terraform can trigger controlled failures to test recovery plans!** --- ## **8. Common Disaster Recovery Mistakes & How to Avoid Them** **Mistake****Fix**Storing Terraform state **locally**Use **S3, Azure Blob, or GCP Storage** for state management.No **auto-recovery for VMs**Use **Auto Scaling Groups (AWS) or VMSS (Azure)**.No **database backups**Enable **automated RDS/Azure SQL snapshots**.No **multi-region failover**Use **Route 53, Azure Traffic Manager, or GCP Load Balancing**.No **disaster recovery testing**Use **AWS Fault Injection Simulator or Chaos Engineering**.**Pro Tip:** If you don’t **test your disaster recovery**, you don’t have a disaster recovery plan—you have **hope.** --- ## **Wrapping Up** Terraform can **automate disaster recovery**, ensuring that your infrastructure **recovers fast and automatically**—with no manual intervention. **Quick Recap:** - **Use Auto Scaling Groups & VMSS for automatic VM recovery.** - **Backup Terraform state remotely & enable versioning.** - **Set up multi-region failover with Route 53 or Azure Traffic Manager.** - **Automate disaster recovery testing with Fault Injection tools.** Now, go **disaster-proof your infrastructure** with Terraform! --- ### **Final Thought: The End of the Terraform Blog Series** This wraps up our **Terraform blog series**! From **getting started** to **disaster-proofing your cloud**, we’ve covered **everything you need** to master Terraform. Now, it’s time to **put it into action.** Keep Terraforming, keep automating, and **keep your cloud running smoothly!** **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Evolution: Growing Your Infrastructure Without the Chaos](https://www.woodruff.dev/terraform-evolution-growing-your-infrastructure-without-the-chaos/) **Published:** March 8, 2025 **Author:** Chris Woodruff **Excerpt:** Terraform is amazing for spinning up infrastructure fast, but what happens when your small project grows into a full-blown production system? **Content:** Terraform is amazing for **spinning up infrastructure fast**, but what happens when your **small project grows into a full-blown production system**? At first, your `main.tf` file might look simple and clean. **But over time, things start to get messy.** - Too many resources in one file. - Hardcoded values everywhere. - Scaling across multiple environments becomes a nightmare. Sounds familiar? **You’re not alone!** In this post, we’ll explore how to **evolve Terraform configurations over time**, making them: - **Modular** – Reusable and organized. - **Scalable** – Easy to manage across environments. - **Maintainable** – So future-you won’t hate past-you. Let’s **level up your Terraform game!** --- ## **1. The Evolution of a Terraform Configuration** Terraform setups usually follow this **evolution path**: **1. The “Just Make It Work” Stage** – A single `main.tf` file with everything inside. **2. The “Oops, This Is a Mess” Stage** – Multiple `.tf` files, but still unorganized. **3. The “We Need Structure” Stage** – Breaking out reusable **modules**. **4. The “Full Automation” Stage** – Remote state, workspaces, and pipelines. --- ## **2. Breaking Up Large Terraform Files** A common Terraform mistake? **Jamming everything into one massive file.** ### **Example of a Messy Terraform Setup (`main.tf` contains everything)** ``` provider "aws" { region = "us-east-1" } resource "aws_instance" "web" { ami = "ami-123456" instance_type = "t2.micro" } resource "aws_s3_bucket" "logs" { bucket = "my-logs-bucket" } resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } ``` **Why This is Bad** - Hard to read and maintain. - Resources aren’t logically grouped. - Impossible to reuse configurations. **The Fix: Split Into Logical Files** ``` /terraform ├── main.tf # Calls modules ├── providers.tf # Provider configurations ├── variables.tf # Input variables ├── outputs.tf # Output values ├── vpc.tf # VPC resources ├── instances.tf # EC2 instances ├── s3.tf # S3 Buckets ``` Now, **each file serves a clear purpose**, making Terraform configurations easier to maintain. --- ## **3. Using Terraform Modules to Reuse Code** Once your infrastructure **grows**, you’ll find yourself **copy-pasting code** between environments. **STOP!** Terraform **modules** solve this by allowing you to **reuse configurations**. ### **Example: Creating a Reusable EC2 Module** **Folder Structure** ``` /terraform ├── modules │ ├── ec2 │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf ├── dev │ ├── main.tf ├── prod │ ├── main.tf ``` **`modules/ec2/main.tf` (EC2 Module Code)** ``` resource "aws_instance" "this" { ami = var.ami instance_type = var.instance_type } output "public_ip" { value = aws_instance.this.public_ip } ``` **`modules/ec2/variables.tf` (Define Variables)** ``` variable "ami" {} variable "instance_type" {} ``` **Using the Module in `dev/main.tf`** ``` module "dev_ec2" { source = "../modules/ec2" ami = "ami-123456" instance_type = "t2.micro" } ``` **Now, the same module can be used for `prod`, `staging`, etc.** --- ## **4. Managing Multiple Environments with Workspaces** When you need **multiple environments (dev, staging, prod)**, Terraform **workspaces** help avoid duplication. ### **Step 1: Create Workspaces** ``` terraform workspace new dev terraform workspace new prod ``` ### **Step 2: Reference Workspace in Terraform** ``` variable "environment" {} resource "aws_instance" "web" { ami = "ami-123456" instance_type = var.environment == "prod" ? "t3.large" : "t3.micro" } ``` **Now, switching environments is easy!** ``` bashCopyEdit``` terraform workspace select dev terraform apply ``` ``` --- ## **5. Using Remote State for Collaboration** If multiple people work on Terraform, **local state files (`terraform.tfstate`) don’t cut it.** **The Fix: Use Remote State** ``` terraform { backend "s3" { bucket = "my-terraform-state" key = "global/terraform.tfstate" region = "us-east-1" } } ``` **Now, state is stored in S3, making Terraform collaborative and safe!** --- ## **6. Automating Terraform with CI/CD** Once Terraform is modular and structured, **automate everything**! **Example: GitHub Actions for Terraform Automation** ``` name: Terraform CI/CD on: push: branches: - main jobs: terraform: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Setup Terraform uses: hashicorp/setup-terraform@v1 - name: Terraform Init run: terraform init - name: Terraform Plan run: terraform plan - name: Terraform Apply run: terraform apply -auto-approve ``` **Now, Terraform runs automatically on every commit!** --- ## **Terraform Evolution Cheat Sheet** **Stage****Changes****Basic Terraform**A single `main.tf` file.**Organized Terraform**Split resources into multiple `.tf` files.**Modular Terraform**Reuse components using **modules**.**Multiple Environments**Use **workspaces** to manage `dev`, `prod`, etc.**Remote State**Store Terraform state in **S3/Azure/GCS** for collaboration.**Automated Terraform**Run Terraform in **CI/CD pipelines**.**Follow this path, and your Terraform setup will be rock solid!** --- ## **Wrapping Up** Terraform setups **start small but grow over time**—and without structure, things **get out of control fast**. **Quick Recap:** - **Break up Terraform files for better organization.** - **Use modules to reuse and simplify Terraform code.** - **Manage environments with Terraform workspaces.** - **Use remote state for better collaboration.** - **Automate Terraform with CI/CD pipelines.** Now, go **refactor your Terraform setup and future-proof it!** --- ### **What’s Next?** What happens when things **go wrong**? In the next post, **“Automating Disaster Recovery with Terraform,”** we’ll cover how to **build self-healing infrastructure, backup state files, and ensure quick recovery from failures.** **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform in Action: Real-World Success Stories from the Cloud](https://www.woodruff.dev/terraform-in-action-real-world-success-stories-from-the-cloud/) **Published:** March 7, 2025 **Author:** Chris Woodruff **Content:** Terraform isn’t just a cool tool for spinning up infrastructure—it’s **transforming how companies build, scale, and secure their cloud environments**. But how does Terraform actually help **real companies**? In this post, we’ll explore **real-world case studies** where Terraform helped businesses: - **Automate infrastructure deployments** and save time. - **Reduce cloud costs** by optimizing resources. - **Scale seamlessly** across multiple cloud providers. - **Improve security & compliance** with Infrastructure as Code. Let’s dive into the **stories of companies that leveraged Terraform to power their cloud journeys!** --- ## **Case Study 1: Automating Deployments at a FinTech Startup** ### **The Challenge:** A FinTech startup had **slow, manual cloud deployments** that required engineers to: - **Manually configure AWS EC2 instances, databases, and security groups.** - **Manually update infrastructure when scaling up.** - **Struggle with inconsistencies between dev, staging, and production.** ### **How Terraform Helped:** - **Automated AWS Infrastructure Deployment** - **Created reusable modules for EC2, RDS, and IAM roles** - **Enabled rapid environment provisioning with `terraform apply`** ### **Example: Terraform Code for Auto-Provisioning AWS Resources** ``` module "ec2_instance" { source = "./modules/ec2" instance_type = "t3.medium" environment = "production" } module "rds" { source = "./modules/rds" db_engine = "postgres" } ``` **Impact:** - **Reduced deployment time from hours to minutes.** - **Eliminated manual errors & inconsistencies.** - **Enabled DevOps engineers to focus on innovation instead of setup.** --- ## **Case Study 2: Cutting Cloud Costs for an E-Commerce Giant** ### **The Challenge:** A global e-commerce company was **overpaying for cloud resources** because: - Engineers **manually provisioned VMs without tracking costs**. - Many **idle resources** were left running. - No cost visibility led to **budget overruns**. ### **How Terraform Helped:** - **Implemented Auto-Scaling for VMs** to match demand. - **Added Cost Estimation in CI/CD Pipelines** (using `infracost`). - **Scheduled shutdowns for unused resources** (dev environments). ### **Example: Terraform Auto-Scaling & Cost Estimation** ``` resource "aws_autoscaling_group" "web" { min_size = 2 max_size = 10 } # Cost estimation integration resource "null_resource" "cost_estimation" { provisioner "local-exec" { command = "infracost breakdown --path ." } } ``` **Impact:** - **Saved 30% on cloud costs** by auto-scaling & shutting down idle resources. - **Gained cost transparency** before deploying infrastructure. - **Eliminated unnecessary over-provisioning.** --- ## **Case Study 3: Scaling Multi-Cloud Infrastructure for a SaaS Company** ### **The Challenge:** A SaaS company needed to **scale globally across AWS, Azure, and GCP**, but: - **Each cloud provider had different APIs & configurations.** - **Manually managing multiple clouds was painful.** - **Networking between clouds was complex.** ### **How Terraform Helped:** - **Used Terraform modules to abstract cloud differences**. - **Managed networking across AWS, Azure, and GCP with a single config**. - **Deployed infrastructure faster across multiple regions.** ### **Example: Multi-Cloud Terraform Setup** ``` provider "aws" { region = "us-east-1" } provider "azurerm" { features {} } provider "google" { project = "my-gcp-project" } module "networking" { source = "./modules/network" aws_vpc_id = aws_vpc.main.id azure_vnet_id = azurerm_virtual_network.main.id gcp_network_id = google_compute_network.main.id } ``` **Impact:** - **Reduced deployment time from weeks to hours.** - **Standardized infrastructure across AWS, Azure, and GCP.** - **Eliminated cloud provider lock-in, improving flexibility.** --- ## **Case Study 4: Improving Security & Compliance for a Healthcare Provider** ### **The Challenge:** A healthcare provider needed to **comply with HIPAA regulations** but faced: - **Security misconfigurations leading to compliance risks.** - **Manually configured IAM roles & policies (error-prone).** - **No audit trail of infrastructure changes.** ### **How Terraform Helped:** - **Used Terraform to enforce security best practices.** - **Integrated `tfsec` and `Checkov` for automated security scanning.** - **Logged all infrastructure changes in GitHub for auditability.** ### **Example: Terraform Security Scan Using `tfsec`** ``` tfsec . ``` **Output:** ``` WARNING: S3 bucket allows public access! Fix required. ``` **Impact:** - **Achieved full HIPAA compliance** for cloud infrastructure. - **Automated security checks before applying changes.** - **Eliminated security misconfigurations & audit risks.** --- ## **5. Visualizing Terraform’s Impact: Before vs. After** **Before Terraform****After Terraform**Manual deployments taking **days or weeks**Automated infrastructure in **minutes** 🚀Inconsistent environments across teamsStandardized, version-controlled infrastructure ✅High cloud costs from over-provisioningCost-efficient auto-scaling & optimization 💰Security misconfigurations & compliance risksAutomated security enforcement 🔒**Terraform = Faster, Safer, and More Scalable Infrastructure!** --- ## **Wrapping Up** Terraform isn’t just a tool—it’s a **game-changer** for cloud infrastructure. From **automating deployments** to **cutting costs**, **scaling globally**, and **improving security**, real-world companies are proving Terraform’s power every day. **Quick Recap:** - **FinTech Startup:** Automated AWS deployments, reducing errors. - **E-Commerce Giant:** Saved 30% on cloud costs with auto-scaling. - **SaaS Company:** Scaled across AWS, Azure, and GCP. - **Healthcare Provider:** Enforced security & compliance with Terraform. Now, it’s your turn! **How will you use Terraform to transform your infrastructure?** --- ### **What’s Next?** If these case studies inspired you, but you’re **new to Terraform**, don’t worry! In the next post, **“Terraform for Beginners,”** we’ll break down Terraform from scratch—so you can start automating infrastructure **step by step**. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Power-Ups: The Best Tools to Supercharge Your IaC Workflow](https://www.woodruff.dev/terraform-power-ups-the-best-tools-to-supercharge-your-iac-workflow/) **Published:** March 6, 2025 **Author:** Chris Woodruff **Content:** Terraform is already an **amazing** tool for automating infrastructure, but what if I told you it could get **even better**? Yes, Terraform has an entire **ecosystem of tools** that: - **Improve collaboration** (no more breaking each other’s state files!). - **Automate approvals and deployments** (less clicking, more coding!). - **Enhance security and compliance** (because nobody wants a misconfigured S3 bucket…). In this post, we’ll explore the **must-know Terraform ecosystem tools** that **make infrastructure automation faster, safer, and way more efficient**. Let’s dive in! --- ## **1. Terragrunt: The Terraform Booster Pack** Terraform is great, but managing multiple environments (dev, staging, prod) can be **a headache**. Enter **Terragrunt**, a wrapper for Terraform that: - **Keeps configurations DRY** (Don’t Repeat Yourself). - **Manages remote state better**. - **Handles dependencies between modules automatically**. ### **Example: Using Terragrunt to Reuse Terraform Code** Instead of duplicating Terraform configs across multiple environments, **use Terragrunt**: **File: `terragrunt.hcl`** ``` terraform { source = "git::https://github.com/myorg/terraform-modules.git//networking" } inputs = { environment = "production" } ``` **Now, every environment reuses the same Terraform code!** --- ## **2. Atlantis: GitOps for Terraform** Tired of running `terraform apply` manually? **Atlantis automates Terraform inside pull requests**, making it easier to: - **Review changes before applying them**. - **Enforce approvals and workflows**. - **Keep Terraform state consistent across teams**. ### **How Atlantis Works** 1. **Developer opens a PR** with Terraform changes. 2. Atlantis **runs `terraform plan` automatically** and posts results in the PR. 3. **Team reviews the plan** and approves the changes. 4. **Atlantis applies the Terraform changes** when the PR is merged. **Now, Terraform runs automatically from Git—no more local scripts!** --- ## **3. OpenTofu: The Terraform Alternative** Terraform went commercial, and now there’s **OpenTofu**—an open-source alternative that: - **Works exactly like Terraform** (HCL, providers, etc.). - **Is fully open-source** (no vendor lock-in). - **Has better community-driven features**. ### **Switching from Terraform to OpenTofu** If you already use Terraform, migrating to OpenTofu is easy: ``` brew install opentofu tofu init tofu plan tofu apply ``` **Same Terraform commands—just fully open-source!** --- ## **4. tfsec: Security Scanner for Terraform** Terraform makes it easy to deploy cloud resources, but **what if you accidentally leave an S3 bucket public**? **tfsec scans Terraform code for security vulnerabilities** before you deploy. ### **Running tfsec** ``` tfsec . ``` - **Finds misconfigured security groups, public resources, and weak IAM policies**. - **Works with AWS, Azure, GCP, and Kubernetes**. **Now, Terraform is secure before you apply it!** --- ## **5. Checkov: Policy Enforcement for Terraform** Need **strict security and compliance rules**? Checkov enforces **policy-as-code** for Terraform. ### **Example: Checkov Warning for an Unencrypted S3 Bucket** ``` checkov -d . ``` **Output:** ``` WARNING: S3 bucket encryption is disabled! ``` - **Ensures Terraform configurations meet security policies.** - **Prevents accidental non-compliant deployments.** **Use Checkov in CI/CD to catch security issues before merging code!** --- ## **6. Terraformer: Reverse Engineer Cloud Resources** Already have cloud resources but **didn’t use Terraform**? **Terraformer generates Terraform configs from existing infrastructure**. ### **Example: Generating Terraform Configs from AWS** ``` terraformer import aws --resources=ec2,s3,vpc ``` - **Creates Terraform files from AWS, Azure, or GCP resources.** - **Perfect for migrating legacy infrastructure to Terraform.** **Now, even manually created resources can be managed with Terraform!** --- ## **7. Scalr: Enterprise Terraform Collaboration** For large teams, **managing Terraform state, policies, and compliance** can get messy. **Scalr** provides: - **Multi-cloud state management**. - **Role-based access control (RBAC)**. - **Team collaboration features**. **Think of Scalr as Terraform Cloud but with more flexibility!** --- ## **8. Terraform CI/CD: Automate Everything** Integrate Terraform into CI/CD pipelines with **GitHub Actions, GitLab CI, and Azure DevOps**. ### **Example: GitHub Actions for Terraform** ``` name: Terraform CI/CD on: push: branches: - main jobs: terraform: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Setup Terraform uses: hashicorp/setup-terraform@v1 - name: Terraform Init run: terraform init - name: Terraform Plan run: terraform plan - name: Terraform Apply run: terraform apply -auto-approve ``` **Now, Terraform runs automatically on every commit!** --- ## **Terraform Ecosystem Cheat Sheet** **Tool****Purpose****Terragrunt**Manages Terraform modules & environments.**Atlantis**Automates Terraform in pull requests.**OpenTofu**Open-source Terraform alternative.**tfsec**Security scanning for Terraform.**Checkov**Enforces security policies.**Terraformer**Converts existing cloud resources into Terraform.**Scalr**Enterprise Terraform collaboration.**Use these tools to level up your Terraform workflow!** --- ## **Wrapping Up** Terraform is powerful, but **its ecosystem of tools makes it unstoppable**! By using **Terragrunt, Atlantis, tfsec, OpenTofu, and more**, you can: - **Manage multiple environments efficiently.** - **Automate Terraform deployments & approvals.** - **Enforce security policies before applying changes.** - **Turn existing cloud resources into Terraform code.** Now, go **supercharge your Terraform workflow**! --- ### **What’s Next?** The best way to learn Terraform? **See it in action!** In the next post, **“Real-World Case Studies,”** we’ll explore how companies use Terraform to **automate infrastructure, cut costs, and improve reliability**. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform + Monitoring: Keeping an Eye on Your Infrastructure 24/7](https://www.woodruff.dev/terraform-monitoring-keeping-an-eye-on-your-infrastructure-24-7/) **Published:** March 5, 2025 **Author:** Chris Woodruff **Excerpt:** You’ve built your cloud infrastructure with Terraform—awesome! But now what? How do you know if your VMs are running smoothly? What if your databases are overloading or your Kubernetes cluster is on fire? Infrastructure isn’t set it and forget it—you need real-time monitoring to catch issues before users notice. Good news: Terraform can deploy and configure monitoring tools so you can track metrics, set up alerts, and visualize performance effortlessly! **Content:** You’ve **built your cloud infrastructure with Terraform**—awesome! But now what? How do you know if your **VMs are running smoothly**? What if your **databases are overloading** or your **Kubernetes cluster is on fire**? Infrastructure isn’t **set it and forget it**—you need **real-time monitoring** to catch issues **before users notice**. **Good news:** Terraform can **deploy and configure monitoring tools** so you can **track metrics, set up alerts, and visualize performance** effortlessly! In this post, we’ll cover: - **Why monitoring Terraform-managed infrastructure is crucial.** - **How to set up monitoring in AWS, Azure, and GCP.** - **Using Grafana, Prometheus, and other Terraform-powered monitoring tools.** - **Setting up alerts so you know when things go wrong.** Let’s get your **Terraform infrastructure under 24/7 surveillance!** --- ## **1. Why Monitor Terraform-Provisioned Infrastructure?** Terraform is **great at deploying infrastructure**, but once resources are live, Terraform **doesn’t manage their health**. Without monitoring, you risk: - **Unexpected downtime** because you didn’t track resource failures. - **Over-provisioned resources** leading to wasted cloud spend. - **Security vulnerabilities** due to missing audit logs. **With monitoring, you can detect failures early, optimize performance, and reduce costs.** --- ## **2. Deploying Cloud Monitoring with Terraform** Most cloud providers have **built-in monitoring tools**. Terraform can configure them automatically! --- ### **AWS: Terraform + CloudWatch for Logs & Metrics** AWS **CloudWatch** tracks logs, metrics, and alerts for your resources. Let’s configure Terraform to **monitor an EC2 instance**. #### **Step 1: Enable CloudWatch Monitoring for an EC2 Instance** ``` resource "aws_instance" "web" { ami = "ami-123456" instance_type = "t2.micro" monitoring = true # Enables detailed monitoring } ``` #### **Step 2: Create a CloudWatch Alarm for High CPU Usage** ``` resource "aws_cloudwatch_metric_alarm" "high_cpu" { alarm_name = "HighCPUAlarm" comparison_operator = "GreaterThanThreshold" threshold = 80 evaluation_periods = 2 metric_name = "CPUUtilization" namespace = "AWS/EC2" period = 60 statistic = "Average" alarm_actions = ["arn:aws:sns:us-east-1:123456789012:alerts"] } ``` **Now, if CPU usage exceeds 80%, Terraform triggers an alarm!** --- ### **Azure: Terraform + Azure Monitor** Azure Monitor collects **logs and metrics** for **VMs, databases, and network traffic**. Let’s set up Terraform to **monitor an Azure VM**. #### **Step 1: Enable Monitoring for an Azure VM** ``` resource "azurerm_monitor_diagnostic_setting" "vm_monitor" { name = "vm-monitor" target_resource_id = azurerm_virtual_machine.example.id log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id log { category = "Administrative" enabled = true } metric { category = "AllMetrics" enabled = true } } ``` #### **Step 2: Set Up an Alert for High Memory Usage** ``` resource "azurerm_monitor_metric_alert" "high_memory" { name = "HighMemoryUsage" resource_group_name = azurerm_resource_group.example.name scopes = [azurerm_virtual_machine.example.id] criteria { metric_name = "Percentage CPU" aggregation = "Average" operator = "GreaterThan" threshold = 85 } } ``` **Terraform now monitors Azure VMs and triggers alerts when memory usage is high!** --- ### **GCP: Terraform + Stackdriver (Cloud Monitoring)** Google’s **Cloud Monitoring (Stackdriver)** collects logs and metrics across GCP services. #### **Step 1: Enable Cloud Monitoring for a GCP VM** ``` resource "google_monitoring_dashboard" "vm_dashboard" { dashboard_json = **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Testing: Catching Bugs Before They Break Your Cloud](https://www.woodruff.dev/terraform-testing-catching-bugs-before-they-break-your-cloud/) **Published:** March 4, 2025 **Author:** Chris Woodruff **Excerpt:** You wouldn’t deploy code without testing it, right? So why would you push infrastructure changes without making sure they work? Terraform makes it easy to define infrastructure, but one wrong line of code can delete everything. That’s why testing Terraform configurations is essential—to catch issues before they reach production. **Content:** You wouldn’t **deploy code without testing it**, right? So why would you push **infrastructure changes** without making sure they work? Terraform makes it easy to define infrastructure, but **one wrong line of code can delete everything**. That’s why **testing Terraform configurations is essential**—to catch issues before they reach production. In this post, we’ll cover: - **Why testing Terraform matters.** - **Unit testing, validation, and security checks for Terraform.** - **How to automate Terraform testing in CI/CD.** Let’s make sure your Terraform deployments **don’t explode in production**. --- ## **1. Why Test Terraform Configurations?** Terraform’s **powerful**, but also **dangerous** if you’re not careful. Without proper testing, you risk: - **Deleting critical infrastructure** because of a typo. - **Breaking production environments** with bad config changes. - **Misconfiguring security settings**, exposing your cloud to attacks. **With testing, you can deploy Terraform confidently.** --- ## **2. Terraform Built-in Testing: Validate & Plan** Terraform has **two built-in ways** to check your configs before applying changes: ### **Step 1: Validate Your Terraform Configs** ``` terraform validate ``` **Checks for syntax errors and invalid configurations.** ### **Step 2: Run `terraform plan` Before `apply`** ``` terraform plan ``` **Shows what Terraform will change—before making any modifications.** **Pro Tip:** If `terraform plan` shows unintended deletions, **fix your state file before applying changes!** --- ## **3. Unit Testing Terraform with `terraform test`** Terraform 1.6+ introduced **built-in unit testing** using `terraform test`! ### **Example: Terraform Test File (`test.tf`)** ``` test "check_vm_size" { condition = resource.aws_instance.example.instance_type == "t2.micro" error_message = "Instance type should be t2.micro!" } ``` ### **Run Terraform Tests** ``` terraform test ``` **Pass? Great! Fail? Fix your config before applying.** --- ## **4. Advanced Testing with `terratest` (Go-Based Testing)** For deeper testing, use **Terratest**, a **Go-based testing framework** for Terraform. ### **Step 1: Install Go & Terratest** ``` go mod init my-terraform-tests go get github.com/gruntwork-io/terratest ``` ### **Step 2: Write a Terraform Test (`main_test.go`)** ``` package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" ) func TestTerraformDeployment(t *testing.T) { options := &terraform.Options{ TerraformDir: "../terraform", } terraform.InitAndApply(t, options) } ``` ### **Step 3: Run the Test** ``` go test -v ``` **Runs Terraform, checks for failures, and destroys infra afterward.** **Why Use Terratest?** - **Runs Terraform apply and verify outputs.** - **Checks for real-world infrastructure issues.** - **Prevents bad deployments before they happen.** --- ## **5. Security Testing for Terraform** Terraform security misconfigurations **can expose your cloud** to threats. **Use these tools to prevent security issues!** ### **Step 1: Scan for Security Risks with `tfsec`** ``` tfsec . ``` **Finds misconfigurations, like open S3 buckets or weak IAM policies.** ### **Step 2: Enforce Compliance with Checkov** ``` checkov -d . ``` **Ensures your Terraform meets security best practices.** ### **Example Checkov Warning:** ``` WARNING: S3 Bucket allows public access! Fix your policy. ``` **Now, Terraform won’t expose sensitive resources!** --- ## **6. Automating Terraform Tests in CI/CD** Integrate **Terraform testing** into CI/CD pipelines to catch issues **before deployment**. ### **Example: Terraform Testing in GitHub Actions** ``` name: Terraform Testing on: pull_request: branches: - main jobs: test: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Install Terraform uses: hashicorp/setup-terraform@v1 - name: Validate Terraform run: terraform validate - name: Terraform Plan run: terraform plan - name: Run Security Scans run: tfsec . ``` **Now, Terraform changes are tested automatically!** --- ## **7. Common Terraform Testing Pitfalls & How to Avoid Them** **Issue****Solution**Forgetting to run `terraform validate`Always validate configs before applying changes.Skipping `terraform plan`Run a plan before every apply to catch surprises.Deploying untested changesUse **`terraform test` and Terratest** for validation.Ignoring security risksUse **`tfsec` and Checkov** to scan for vulnerabilities.Not integrating Terraform tests in CI/CDAutomate testing with GitHub Actions, Azure DevOps, or Jenkins.**Pro Tip:** If Terraform **wants to delete something unexpected**, stop and check the state file before proceeding! --- ## **Wrapping Up** Terraform testing isn’t optional—it’s **essential** for preventing costly infrastructure failures. **Quick Recap:** - **Use `terraform validate` to catch syntax errors.** - **Run `terraform plan` before applying changes.** - **Use `terraform test` for unit testing Terraform configs.** - **Run security scans with `tfsec` and Checkov.** - **Automate Terraform testing in CI/CD pipelines.** Now, go **test your Terraform before it tests you!** --- ### **What’s Next?** Testing is great, but how do you **monitor** Terraform-provisioned infrastructure **after deployment**? In the next post, **“Monitoring Infrastructure Provisioned with Terraform,”** we’ll explore how to use **CloudWatch, Azure Monitor, Prometheus, and Grafana** to keep track of Terraform-managed resources. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform on Autopilot: Building Dynamic, Self-Scaling Infrastructure](https://www.woodruff.dev/terraform-on-autopilot-building-dynamic-self-scaling-infrastructure/) **Published:** March 3, 2025 **Author:** Chris Woodruff **Excerpt:** What if your infrastructure could scale itself up when traffic spikes and shrink when demand drops? What if it could self-heal when things break—without you lifting a finger? Sounds futuristic? It’s possible today with Terraform! With dynamic infrastructure, Terraform can: Auto-scale compute resources based on demand. Adjust networking and storage dynamically. Trigger infrastructure changes using event-driven automation. **Content:** What if your infrastructure could **scale itself up when traffic spikes** and **shrink when demand drops**? What if it could **self-heal when things break**—without you lifting a finger? Sounds futuristic? **It’s possible today with Terraform!** With **dynamic infrastructure**, Terraform can: - **Auto-scale compute resources** based on demand. - **Adjust networking and storage** dynamically. - **Trigger infrastructure changes** using event-driven automation. In this post, we’ll cover **how to build dynamic, self-adjusting infrastructure using Terraform**—so you can **stop babysitting your cloud** and let Terraform handle the heavy lifting. Let’s go! --- ## **1. What is Dynamic Infrastructure?** Dynamic infrastructure **automatically adapts** to changes in usage, performance, and availability **without manual intervention**. ### **How Does Terraform Make Infrastructure Dynamic?** **Auto-scaling:** Terraform can create new servers when demand increases and remove them when demand drops. **Event-driven triggers:** Infrastructure changes can happen automatically in response to real-world events. **Self-healing:** Terraform can detect failed instances and replace them. **Why It’s Awesome:** **No more waking up at 2 AM to fix a broken server!** --- ## **2. Auto-Scaling with Terraform** Let’s set up a **Terraform auto-scaling group** in AWS that: - **Adds servers** when CPU usage is high. - **Removes servers** when demand is low. ### **Step 1: Define an AWS Auto Scaling Group** ``` resource "aws_autoscaling_group" "web" { launch_configuration = aws_launch_configuration.web.id min_size = 2 max_size = 5 desired_capacity = 2 tag { key = "Name" value = "autoscaled-instance" propagate_at_launch = true } } ``` ### **Step 2: Add Auto-Scaling Policies** ``` resource "aws_autoscaling_policy" "scale_up" { name = "scale_up" scaling_adjustment = 1 adjustment_type = "ChangeInCapacity" cooldown = 300 autoscaling_group_name = aws_autoscaling_group.web.name } resource "aws_autoscaling_policy" "scale_down" { name = "scale_down" scaling_adjustment = -1 adjustment_type = "ChangeInCapacity" cooldown = 300 autoscaling_group_name = aws_autoscaling_group.web.name } ``` **Now, Terraform will automatically adjust the number of servers based on traffic!** --- ## **3. Event-Driven Infrastructure with Terraform** What if Terraform could **react to real-world events**—like an outage or a cost threshold being exceeded? **Event-driven infrastructure makes this possible!** ### **Example: Automatically Deploying New Resources on an Alert** #### **Step 1: Use AWS CloudWatch to Detect High CPU Usage** ``` resource "aws_cloudwatch_metric_alarm" "high_cpu" { alarm_name = "high-cpu-alarm" comparison_operator = "GreaterThanThreshold" threshold = 75 evaluation_periods = 2 metric_name = "CPUUtilization" namespace = "AWS/EC2" period = 300 statistic = "Average" alarm_actions = [aws_autoscaling_policy.scale_up.arn] } ``` **Terraform will now trigger auto-scaling when CPU usage exceeds 75%!** --- ## **4. Self-Healing Infrastructure with Terraform** What happens if **one of your VMs crashes**? Terraform can detect it and **automatically replace it**—ensuring **zero downtime**. ### **Example: Replacing Failed Virtual Machines in Azure** ``` resource "azurerm_virtual_machine_scale_set" "example" { name = "vmss-example" location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name upgrade_policy_mode = "Automatic" sku { name = "Standard_DS1_v2" capacity = 3 } automatic_instance_repair { enabled = true } } ``` **Now, Terraform will automatically replace failed instances.** --- ## **5. Managing Dynamic Storage with Terraform** Storage requirements **fluctuate**—why pay for storage you **aren’t using**? Terraform can **automatically resize storage** based on demand. ### **Example: Auto-Expanding AWS EBS Volumes** ``` resource "aws_cloudwatch_metric_alarm" "low_disk_space" { alarm_name = "low-disk-space" comparison_operator = "LessThanThreshold" threshold = 20 metric_name = "FreeStorageSpace" namespace = "AWS/EBS" period = 300 evaluation_periods = 2 statistic = "Average" alarm_actions = [aws_lambda_function.expand_ebs.arn] } ``` **When free storage drops below 20%, Terraform triggers a Lambda function to expand the volume!** --- ## **6. Dynamic Networking: Load Balancing & Traffic Routing** Traffic isn’t **always predictable**. Terraform can dynamically **adjust traffic routing** and **scale load balancers**. ### **Example: Auto-Scaling an Azure Load Balancer** ``` resource "azurerm_lb" "example" { name = "myLoadBalancer" location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name sku = "Standard" } resource "azurerm_lb_rule" "example" { loadbalancer_id = azurerm_lb.example.id name = "http-rule" protocol = "Tcp" frontend_port = 80 backend_port = 80 frontend_ip_configuration_name = "PublicIP" } ``` **Now, traffic is dynamically distributed across healthy instances!** --- ## **7. Common Pitfalls & How to Avoid Them** **Issue****Solution****Scaling too aggressively**Set **cooldown periods** to prevent rapid scaling up/down.**Accidental resource deletion**Use `prevent_destroy` in Terraform to block accidental deletes.**Slow response to events**Optimize **CloudWatch/Prometheus** alerts to trigger Terraform changes faster.**Pro Tip:** Always **test auto-scaling in a staging environment** before deploying to production! --- ## **Wrapping Up** Terraform **isn’t just about provisioning infrastructure**—it can make your infrastructure **smarter, faster, and self-healing**. **Quick Recap:** - **Use Auto-Scaling Groups** to add/remove servers dynamically. - **Trigger Terraform actions based on cloud events.** - **Replace failed VMs automatically** for self-healing infrastructure. - **Scale storage up/down** based on demand. - **Balance traffic dynamically** across multi-cloud environments. Now, go **put your infrastructure on autopilot with Terraform!** --- ### **What’s Next?** Dynamic infrastructure is powerful, but **how do you ensure Terraform changes won’t break production?** In the next post, **“Testing Terraform Configurations,”** we’ll dive into **unit tests, validation tools, and CI/CD best practices for testing Terraform before deploying**. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Mastering Multi-Cloud: Deploying Across AWS, Azure, and GCP with Terraform](https://www.woodruff.dev/mastering-multi-cloud-deploying-across-aws-azure-and-gcp-with-terraform/) **Published:** March 2, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine this: Your company just merged with another, and suddenly, you're managing infrastructure across AWS, Azure, and Google Cloud. Or maybe your team wants multi-cloud resilience—so if one provider has an outage, your apps keep running elsewhere. Smart move! But managing multiple clouds manually? That’s a nightmare. Fortunately, Terraform makes multi-cloud deployments easy, repeatable, and scalable. **Content:** Imagine this: Your company just merged with another, and suddenly, you’re managing infrastructure **across AWS, Azure, and Google Cloud**. Or maybe your team wants **multi-cloud resilience**—so if one provider has an outage, your apps keep running elsewhere. Smart move! But managing multiple clouds manually? **That’s a nightmare.** Fortunately, Terraform makes **multi-cloud deployments easy, repeatable, and scalable**. In this post, we’ll cover: - Why **multi-cloud** matters and when to use it. - How to **deploy to AWS, Azure, and GCP** from the same Terraform config. - Managing **multi-cloud networking, authentication, and state files**. - Common **challenges and how to solve them**. Let’s Terraform the clouds! --- ## **1. Why Go Multi-Cloud?** Multi-cloud isn’t for **every** team, but it offers **big advantages** when done right: ### **Avoid Vendor Lock-In** > What if your cloud provider **suddenly hikes prices** or **removes a key service** you rely on? **Multi-cloud gives you options.** ### **Disaster Recovery & High Availability** > If **AWS East goes down**, your app keeps running on **Azure or GCP**. **No downtime, no panic.** ### **Use the Best of Each Cloud** > Need **Azure AI services**, but your data team loves **AWS Redshift**? **Multi-cloud lets you pick the best tools.** --- ## **2. Setting Up Terraform for Multi-Cloud** Terraform uses **providers** to manage resources in different clouds. Here’s how to set up **AWS, Azure, and GCP** in one Terraform configuration. ### **Example: Defining Multi-Cloud Providers in Terraform** ``` provider "aws" { region = "us-east-1" } provider "azurerm" { features {} } provider "google" { project = "my-gcp-project" region = "us-central1" } ``` **Now, Terraform can manage resources in all three clouds!** --- ## **3. Deploying Resources Across Multiple Clouds** Let’s deploy a **VM in AWS, a storage account in Azure, and a database in GCP**—all from one Terraform config. ``` # AWS: Launch an EC2 Instance resource "aws_instance" "web" { ami = "ami-123456" instance_type = "t2.micro" } # Azure: Create a Storage Account resource "azurerm_storage_account" "storage" { name = "mystorage" resource_group_name = "myRG" location = "East US" account_tier = "Standard" } # GCP: Deploy a Cloud SQL Database resource "google_sql_database_instance" "db" { name = "mydb" database_version = "MYSQL_8_0" settings { tier = "db-f1-micro" } } ``` **One `terraform apply`, and you’ve deployed to three clouds.** --- ## **4. Managing Multi-Cloud Networking** Deploying to multiple clouds means **connecting everything securely**. Here’s how: ### **Option 1: Use a Global VPN** - AWS, Azure, and GCP all support **site-to-site VPNs**. - **AWS Transit Gateway**, **Azure Virtual WAN**, and **GCP Cloud Router** help route traffic. ### **Option 2: Use a Multi-Cloud Load Balancer** - Cloudflare, F5, and Aviatrix provide **global traffic management** across clouds. - DNS-based routing via **AWS Route 53**, **Azure Traffic Manager**, or **GCP Cloud DNS**. --- ## **5. Handling Authentication Across Clouds** Each cloud requires **different authentication methods**, but Terraform handles them smoothly. ### **How to Authenticate Terraform in Multiple Clouds** **AWS:** Use environment variables ``` export AWS_ACCESS_KEY_ID="your-key" export AWS_SECRET_ACCESS_KEY="your-secret" ``` **Azure:** Use a Service Principal ``` az login --service-principal -u CLIENT_ID -p CLIENT_SECRET --tenant TENANT_ID ``` **GCP:** Use a JSON key file ``` export GOOGLE_APPLICATION_CREDENTIALS="path-to-key.json" ``` **Now, Terraform can authenticate with all three clouds!** --- ## **6. Managing Terraform State in a Multi-Cloud World** Each cloud **shouldn’t have its own Terraform state**—you need **one source of truth**. ### **Best Option: Use a Remote State Backend** #### **Example: Storing Terraform State in an Azure Storage Account** ``` terraform { backend "azurerm" { resource_group_name = "myRG" storage_account_name = "mystorage" container_name = "tfstate" key = "multi-cloud.tfstate" } } ``` **Why It’s Cool:** - **Keeps state consistent** across all clouds. - **Supports state locking** to prevent conflicts. - **Enables collaboration** for multi-cloud teams. --- ## **7. Challenges & How to Solve Them** **Challenge****Solution****Different APIs & Services**Use Terraform **modules** to abstract cloud differences.**Security Complexity**Centralize authentication (e.g., HashiCorp Vault, AWS IAM Roles).**Networking Headaches**Use a **multi-cloud VPN or load balancer**.**State Management**Use a **remote backend** like Azure Storage, AWS S3, or Terraform Cloud.**Pro Tip:** Keep your Terraform code **modular** so different clouds can be managed independently! --- ## **8. Should You Go Multi-Cloud?** Multi-cloud is **powerful**, but it’s not always necessary. Here’s when to **use** it—and when to **avoid it**. **Go Multi-Cloud If:** - You **need redundancy** across multiple providers. - You **want to avoid vendor lock-in**. - You need **specific services** from different clouds. **Stick to One Cloud If:** - You don’t have a **dedicated DevOps team**. - Your workloads **don’t need global redundancy**. - Your team **is new to Terraform**—master **single-cloud first**. **The sweet spot? Start with one cloud, then expand to multi-cloud when it makes sense.** --- ## **Wrapping Up** Terraform makes **multi-cloud deployments possible and manageable**—but only if you **plan properly**. **Quick Recap:** - **Use Terraform providers** to manage AWS, Azure, and GCP from one config. - **Connect multi-cloud networking** with VPNs or load balancers. - **Store Terraform state in a remote backend** for consistency. - **Handle authentication across clouds** with environment variables or service principals. - **Start simple**, then expand multi-cloud as needed. Now, go **Terraform across the clouds!** --- ### **What’s Next?** Deploying across multiple clouds is great, but **what if your infrastructure could scale and adapt dynamically?** In the next post, **“Dynamic Infrastructure with Terraform,”** we’ll explore how to create **auto-scaling, self-healing, and event-driven infrastructure** using Terraform. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [From Chaos to Code: Migrating Legacy Infrastructure to Terraform](https://www.woodruff.dev/from-chaos-to-code-migrating-legacy-infrastructure-to-terraform/) **Published:** March 1, 2025 **Author:** Chris Woodruff **Excerpt:** Raise your hand if you’ve ever inherited a mess of manually created cloud resources. Maybe your team has been clicking around in cloud consoles for years, and now your infrastructure is an unmanageable, undocumented monster. Or perhaps you have hundreds of resources, and nobody knows exactly how they were configured. Sound familiar? Don’t worry—Terraform can bring order to the madness! **Content:** **Raise your hand if you’ve ever inherited a mess of manually created cloud resources.** Maybe your team has been **clicking around in cloud consoles for years**, and now your infrastructure is an **unmanageable, undocumented monster**. Or perhaps you have **hundreds of resources**, and nobody knows exactly how they were configured. Sound familiar? Don’t worry—Terraform can **bring order to the madness**! In this post, we’ll cover: - Why you should migrate legacy infrastructure to Terraform. - How to **import existing cloud resources** into Terraform. - How to **avoid downtime and disasters** during migration. - Tips for making the transition **as painless as possible**. Let’s get your infrastructure **under control**! --- ## **1. Why Migrate Legacy Infrastructure to Terraform?** If your infrastructure was **built manually**, you might be thinking: > “If it ain’t broke, why fix it?” Here’s why moving to **Infrastructure as Code (IaC) with Terraform** is **worth it**: **No Documentation** – If your infrastructure only exists in a cloud portal, good luck figuring out what’s running where. **Inconsistent Configurations** – Without Terraform, different environments can **drift apart** over time. **Manual Mistakes** – Manually provisioning resources leads to **errors and inconsistencies**. **No Version Control** – You can’t track changes, roll back mistakes, or collaborate effectively. **With Terraform, you get repeatable, version-controlled infrastructure that just works.** --- ## **2. Preparing for Migration: Don’t Skip This Step!** Before jumping into Terraform, take some time to **audit your current infrastructure**. ### **Step 1: List All Existing Resources** Run cloud provider commands to **get an inventory** of what’s currently deployed: #### **Azure:** ``` az resource list --output table ``` #### **AWS:** ``` aws resourcegroupstaggingapi get-resources ``` #### **GCP:** ``` gcloud compute instances list ``` **Why It’s Important:** You need to know **what exists before you start migrating**. --- ## **3. Importing Legacy Infrastructure into Terraform** The easiest way to bring existing resources under Terraform’s control? **Use `terraform import`.** ### **Example: Importing an Azure Storage Account** ``` terraform import azurerm_storage_account.example /subscriptions/xxxx/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorage ``` ### **Example: Importing an AWS EC2 Instance** ``` terraform import aws_instance.example i-1234567890abcdef0 ``` **What This Does:** - Terraform **adds the resource** to its state file. - Terraform **won’t try to recreate** the resource—it will just start managing it. --- ## **4. Generating Terraform Configuration (No Manual Writing!)** Manually writing Terraform for **hundreds of resources**? **No thanks!** Luckily, you can **auto-generate Terraform configs** with: - **Terraformer** (supports AWS, Azure, GCP) - **Azure Resource Manager (ARM) to Terraform Converter** ### **Example: Using Terraformer to Export AWS Infrastructure** ``` terraformer import aws --resources=ec2,s3,vpc ``` **Now, Terraform will generate HCL code for your existing resources!** - **Why It’s Important:** **Saves time and prevents human error** when writing Terraform configs. --- ## **5. Avoiding Downtime: The Safe Migration Plan** **DO NOT** just start applying Terraform blindly—you might accidentally **delete or overwrite production resources**. ### **Step 1: Plan & Test in a Dev Environment** - Import a **small subset** of resources first (e.g., one VM). - Validate the Terraform plan **before applying any changes**. ### **Step 2: Use `terraform plan` to Check for Changes** ``` terraform plan ``` If Terraform **tries to replace existing resources**, you may need to: - Update the **Terraform configuration** to match real-world settings. - Use **lifecycle policies** to prevent Terraform from touching certain resources: ``` resource "azurerm_storage_account" "example" { name = "mystorage" location = "East US" lifecycle { ignore_changes = [tags] } } ``` **Why It’s Important:** Terraform **won’t overwrite or delete resources accidentally**. --- ## **6. Enforcing Terraform-Managed Infrastructure** Once your infrastructure is managed by Terraform, **block manual changes** to prevent configuration drift! ### **Option 1: Use Terraform Cloud Policy Enforcement** Terraform Cloud allows you to **enforce security & compliance rules**: ``` policy "enforce_terraform_changes" { rule { main = if resource_changes.aws_instance.any then error("All changes must go through Terraform.") } } ``` ### **Option 2: Restrict Console Access** Lock down cloud console access **so people can’t manually edit resources**. **Why It’s Important:** Keeps **Terraform as the single source of truth**. --- ## **7. Automating Terraform with CI/CD** Once your infrastructure is in Terraform, **automate deployments using Azure DevOps, GitHub Actions, or Jenkins**. ### **Example: Azure DevOps Terraform Pipeline** ``` trigger: - main pool: vmImage: 'ubuntu-latest' steps: - script: terraform init displayName: "Terraform Init" - script: terraform plan -out=tfplan displayName: "Terraform Plan" - script: terraform apply -auto-approve tfplan displayName: "Terraform Apply" ``` **Why It’s Cool:** No more manual `terraform apply`—Terraform **runs automatically** when you push changes! --- ## **Wrapping Up** Migrating legacy infrastructure to Terraform **isn’t just a nice-to-have**—it’s a **game-changer for automation, security, and scalability**. **Quick Recap:** - **Import existing resources** using `terraform import`. - **Generate Terraform configs automatically** with Terraformer. - **Avoid downtime** by using `terraform plan` before applying changes. - **Block manual changes** to enforce Terraform as the **single source of truth**. - **Automate deployments** with Terraform CI/CD pipelines. Now, go **Terraform your legacy infrastructure into a modern, code-driven system!** --- ### **What’s Next?** Migrating to Terraform is great, but **what if you want to manage infrastructure across AWS, Azure, and GCP?** In the next post, **“Multi-Cloud Deployments with Terraform,”** we’ll explore **how to create portable, cloud-agnostic Terraform configurations**—so you can deploy infrastructure anywhere, anytime. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Troubleshooting: Fixing Fails, Errors, and Cloud Chaos](https://www.woodruff.dev/terraform-troubleshooting-fixing-fails-errors-and-cloud-chaos/) **Published:** February 28, 2025 **Author:** Chris Woodruff **Excerpt:** Terraform is amazing—until it isn’t. One moment, you're deploying infrastructure effortlessly, and the next, you're staring at an error message that makes no sense. We’ve all been there. Whether it’s state file conflicts, provider issues, or mysterious dependency errors, Terraform troubleshooting can be frustrating—but it doesn’t have to be! **Content:** Terraform is amazing—until it **isn’t**. One moment, you’re deploying infrastructure effortlessly, and the next, you’re staring at an **error message that makes no sense**. We’ve all been there. Whether it’s **state file conflicts, provider issues, or mysterious dependency errors**, Terraform **troubleshooting can be frustrating**—but it doesn’t have to be! In this post, we’ll cover: - **Common Terraform errors** and how to fix them. - **Debugging failed deployments** (without pulling your hair out). - **Best practices to avoid Terraform nightmares** in the first place. Let’s fix Terraform together! --- ## **1. Terraform Apply Failed? Start with `terraform plan`** Running `terraform apply` only to **watch it explode**? Before you apply any changes, **always run `terraform plan` first**! ### **Why?** `terraform plan` **shows what Terraform intends to do before applying changes**. This helps you catch: - **Unintended deletions** (Terraform thinks a resource no longer exists). - **Configuration issues** (Invalid syntax, missing variables, etc.). - **Dependency problems** (Resources out of order). ### **Example: Running Terraform Plan** ``` terraform plan ``` If Terraform **shows unexpected deletions**, check if the **state file is out of sync**. **Fix:** If Terraform wants to delete something that still exists, **import it** back into state: ``` terraform import azurerm_storage_account.example /subscriptions/xxxx/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorage ``` **Problem solved!** --- ## **2. State File Conflicts? Lock It Down!** Terraform’s **state file** (`terraform.tfstate`) tracks everything about your infrastructure. If multiple people or processes **modify it at the same time**, you get **conflicts** like this: ``` Error: Error acquiring the state lock ``` ### **Fix: Use Remote State with Locking** Using **local state** in a team environment is **asking for trouble**. Instead, **store Terraform state remotely** in **Azure Blob Storage, AWS S3, or Terraform Cloud**. #### **Example: Remote State with Azure** ``` terraform { backend "azurerm" { resource_group_name = "myRG" storage_account_name = "mystorage" container_name = "tfstate" key = "terraform.tfstate" } } ``` **Now, Terraform automatically locks the state file to prevent conflicts!** --- ## **3. Provider Version Errors? Pin Your Providers** Ever seen an error like this? ``` Error: Provider configuration not found ``` Or worse: ``` Error: Incompatible provider version ``` ### **What Happened?** Terraform **updated a provider version**, and your infrastructure no longer works. **Ouch.** ### **Fix: Lock Provider Versions** Always **pin provider versions** to avoid unexpected updates! #### **Example: Pinning Azure Provider Version** ``` terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "=3.5.0" } } } ``` **Now, Terraform won’t accidentally update your provider and break things!** --- ## **4. Resource Already Exists? Import It Instead of Recreating** Terraform sometimes **tries to recreate resources that already exist** in your cloud environment. When that happens, you get errors like: ``` Error: Resource already exists ``` ### **Fix: Import Existing Resources** Instead of deleting and recreating the resource, **import it into Terraform**. #### **Example: Importing an Azure Storage Account** ``` terraform import azurerm_storage_account.example /subscriptions/xxxx/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorage ``` **Now, Terraform knows the resource exists and won’t try to create a duplicate!** --- ## **5. Dependency Errors? Use `depends_on` to Fix Order Issues** Terraform **builds resources in parallel**, which is great—until **dependencies break**. Ever seen something like this? ``` Error: Resource not found ``` ### **What Happened?** Terraform tried to **create a resource before its dependency was ready**. ### **Fix: Use `depends_on`** Manually define dependencies so Terraform builds things **in the correct order**. #### **Example: Ensuring a VM Waits for a Resource Group** ``` resource "azurerm_resource_group" "example" { name = "myRG" location = "East US" } resource "azurerm_virtual_machine" "example" { name = "myVM" resource_group_name = azurerm_resource_group.example.name depends_on = [azurerm_resource_group.example] } ``` **Now, Terraform waits for the resource group before creating the VM!** --- ## **6. Debugging Terraform with Logging** Terraform’s **error messages aren’t always helpful**. If you’re dealing with a weird issue, **enable detailed logging** to find out what’s going on. ### **Step 1: Enable Terraform Debug Logging** ``` export TF_LOG=DEBUG terraform apply ``` Terraform will **output detailed logs**—helpful for tracking down problems! ### **Step 2: Redirect Logs to a File** ``` export TF_LOG=DEBUG terraform apply 2> debug.log ``` **Now, you have a full Terraform debug log to analyze!** --- ## **7. Common Terraform Errors and Fixes** **Error Message****Cause****Fix**`Error: Resource not found`Terraform tried to use a resource **before** it was created.Use `depends_on` to enforce build order.`Error: Provider version not found`Terraform updated a provider version that broke compatibility.Pin provider versions (`version = "=x.x.x"`).`Error: State lock conflict`Multiple users modifying Terraform state at the same time.Use **remote state** with locking.`Error: Resource already exists`Terraform tried to create a duplicate resource.Use `terraform import` to manage existing resources.`Error: Connection timeout`Terraform couldn’t reach a cloud API.Check **firewall settings & API permissions**.**Pro Tip:** If you’re stuck, **run `terraform validate` to check for syntax issues!** ``` terraform validate ``` --- ## **Wrapping Up** Terraform troubleshooting **doesn’t have to be painful**. By following these **fixes and best practices**, you’ll **resolve errors faster and keep your deployments running smoothly**. **Quick Recap:** - **Run `terraform plan` before `apply`** to catch issues early. - **Use remote state** to avoid conflicts. - **Pin provider versions** to prevent unexpected updates. - **Import existing resources** instead of recreating them. - **Use `depends_on` to fix dependency issues.** - **Enable logging** to debug tricky Terraform problems. Now, go **fix your Terraform deployments** with confidence! --- ### **What’s Next?** Fixing Terraform errors is great, but what if you could **migrate your entire legacy infrastructure into Terraform** smoothly? In the next post, **“Migrating Legacy Infrastructure to Terraform,”** we’ll cover **how to transition existing cloud resources into Terraform without breaking everything**. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Cut Cloud Costs with Terraform: Automate, Optimize, and Save Money](https://www.woodruff.dev/cut-cloud-costs-with-terraform-automate-optimize-and-save-money/) **Published:** February 27, 2025 **Author:** Chris Woodruff **Excerpt:** Cloud bills piling up faster than you expected? Terraform isn't just for deploying infrastructure—it’s also an awesome cost optimization tool! Instead of manually tracking expenses and hoping for the best, Terraform helps you: Monitor and control cloud costs before they spiral out of control. Automate cost-efficient resource provisioning (no more oversized VMs!). Enforce budget limits and alerts so there are no nasty surprises. **Content:** Cloud bills piling up faster than you expected? Terraform isn’t just for deploying infrastructure—it’s also an awesome **cost optimization tool**! Instead of manually tracking expenses and **hoping for the best**, Terraform helps you: - **Monitor and control cloud costs** before they spiral out of control. - **Automate cost-efficient resource provisioning** (no more oversized VMs!). - **Enforce budget limits and alerts** so there are no nasty surprises. Let’s dive into how **Terraform can help you keep your cloud spending in check—without sacrificing performance**. --- ## **1. Use Terraform to Right-Size Your Resources** One of the biggest cost mistakes? **Overprovisioning resources**. If your infrastructure runs on **expensive, oversized VMs**, you’re **burning cash** for no reason. ### **How Terraform Helps** Terraform makes **right-sizing easy** by allowing you to: - Define **resource sizes dynamically** based on environment needs. - Scale infrastructure **up or down automatically** using variables. ### **Example: Dynamically Choosing Cost-Effective VM Sizes** ``` variable "environment" { description = "Deployment environment" default = "dev" } variable "vm_sizes" { type = map(string) default = { dev = "Standard_B2s" # Cheap for dev/testing prod = "Standard_D4s_v3" # More power for production } } resource "azurerm_virtual_machine" "example" { name = "myVM" vm_size = var.vm_sizes[var.environment] } ``` **Why It’s Cool:** Terraform **automatically selects the right VM size**, so you don’t end up using a **high-performance (and high-cost) VM** for a basic dev environment! --- ## **2. Track Costs with Terraform Cost Estimation** Wouldn’t it be great to **see the cost impact of your Terraform changes** before deploying? **Good news—Terraform can estimate costs for you!** ### **Step 1: Install Terraform Cost Estimation Plugin** Run: ``` terraform init terraform plan -out=tfplan infracost breakdown --path=tfplan ``` **`infracost`** integrates with Terraform and shows estimated cloud costs **before** you apply changes. ### **Example Output from `infracost`** ``` +----------------------------------+-----------+----------+ | Resource | Monthly | Change | +----------------------------------+-----------+----------+ | azurerm_virtual_machine.example | $50.00 | +$50.00 | | azurerm_storage_account.example | $10.00 | +$10.00 | +----------------------------------+-----------+----------+ ``` **Why It’s Cool:** You can **see and approve costs before deploying**, preventing unexpected surprises! --- ## **3. Set Budget Limits and Alerts with Terraform** Ever received a **shocking cloud bill**? **Let’s make sure that never happens again.** ### **How Terraform Helps** - **Define budget thresholds** and get alerts when costs exceed limits. - **Automatically trigger actions** (like scaling down) when budgets are hit. ### **Example: Setting an Azure Budget Alert** ``` resource "azurerm_consumption_budget_subscription" "budget" { name = "monthly-budget" amount = 500 # Set budget limit ($500) time_grain = "Monthly" notification { threshold = 80 # Alert when 80% budget is used operator = "GreaterThan" contact_emails = ["admin@example.com"] } } ``` **Why It’s Cool:** You’ll get **early warnings** before exceeding your budget! --- ## **4. Automate Cost Control with Terraform Policies** Terraform lets you **enforce cost-saving policies** across your infrastructure using **Sentinel (Terraform Cloud) or Open Policy Agent (OPA)**. ### **How Terraform Helps** - **Prevent unnecessary costs** by blocking expensive resource types. - **Ensure compliance** with cost policies across teams. ### **Example: Enforcing Cost Policies in Terraform Cloud** ``` policy "block_expensive_vm_types" { rule { main = if tfplan.resource_changes.azurerm_virtual_machine.vm_size == "Standard_D8s_v3" then error("D8s_v3 is too expensive! Use a smaller VM.") } } ``` **Why It’s Cool:** No more costly mistakes—Terraform **blocks high-cost resources before they deploy**! --- ## **5. Shut Down Idle Resources to Save Money** One of the easiest ways to **reduce cloud costs**? **Turn off unused resources automatically!** ### **How Terraform Helps** - **Schedule shutdowns for non-essential VMs** (e.g., dev/test environments). - **Automatically deallocate resources during off-hours**. ### **Example: Terraform Script to Stop VMs at Night** ``` resource "azurerm_automation_schedule" "shutdown" { name = "vm-shutdown" frequency = "Day" interval = 1 timezone = "UTC" start_time = "22:00" } resource "azurerm_automation_job_schedule" "shutdown_vm" { schedule_name = azurerm_automation_schedule.shutdown.name automation_account_name = azurerm_automation_account.example.name runbook_name = "Stop-AzureVM" } ``` **Why It’s Cool:** Terraform **automatically turns off VMs when they’re not in use**, reducing wasteful spending! --- ## **6. Use Reserved Instances for Long-Term Savings** If you’re running **long-lived workloads**, you can save **30-70% on cloud costs** by reserving instances for **1 or 3 years** instead of paying on-demand prices. ### **How Terraform Helps** - Automates **purchasing Reserved Instances** for long-term savings. - Ensures **right-sized commitments** based on historical usage. ### **Example: Reserving an Azure VM Instance** ``` resource "azurerm_reserved_virtual_machine_instance" "example" { name = "my-reserved-instance" location = "East US" reserved_vm_type = "Standard_D4s_v3" term = "3 Year" } ``` **Why It’s Cool:** You **lock in lower prices** for long-term workloads. --- ## **Wrapping Up** Terraform isn’t just about **deploying infrastructure**—it’s a **powerful cost management tool** that helps you: - **Right-size resources** to avoid wasteful spending. - **Track and forecast costs** before deploying infrastructure. - **Enforce cost controls and alerts** to prevent surprises. - **Automate cost-saving actions** like turning off idle resources. Now, go **Terraform smarter and save that cloud budget!** --- ### **What’s Next?** Even with the best cost controls, **Terraform deployments can still break**. In the next post, **“Troubleshooting Terraform Deployments,”** we’ll cover **common Terraform issues, debugging techniques, and best practices for fixing failed deployments—so you can keep your infrastructure running smoothly**. **Categories:** Terraform **Tags:** Azure, devops, IaC, infrastructure, terraform --- ### [Terraform + Azure DevOps: Automate Your Cloud Deployments the Smart Way](https://www.woodruff.dev/terraform-azure-devops-automate-your-cloud-deployments-the-smart-way/) **Published:** February 26, 2025 **Author:** Chris Woodruff **Excerpt:** Manually deploying infrastructure is so last decade. If you’re still running terraform apply on your local machine, it’s time to step up your game with Azure DevOps Pipelines! **Content:** Manually deploying infrastructure was the last decade**.** If you’re still running `terraform apply` on your local machine, it’s time to **step up your game** with **Azure DevOps Pipelines**! In this post, we’ll cover how to: **Automate Terraform deployments** using Azure DevOps **Set up an Azure DevOps pipeline** for Terraform **Store Terraform state securely** in Azure **Integrate Terraform into CI/CD workflows** Let’s build an automated, **version-controlled, and scalable** Terraform pipeline! --- ## **1. Why Use Terraform with Azure DevOps?** If you’re managing infrastructure manually, you risk: **Inconsistent deployments** – Human error is real! **State conflicts** – Running Terraform from different machines can cause chaos. **No audit trail** – Who deployed what, and when? By integrating **Terraform with Azure DevOps**, you get: **Automated infrastructure changes** with CI/CD. **Version-controlled deployments** using Git. **Secure, shared Terraform state** across teams. --- ## **2. Setting Up Terraform in Azure DevOps** Before we create a pipeline, we need a **Terraform backend** to store our state. **Azure Storage is perfect for this**! ### **Step 1: Create an Azure Storage Account for Terraform State** Run the following commands to set up **Azure Blob Storage** for storing Terraform state: ``` az group create --name myTerraformRG --location eastus az storage account create --name mytfstorage --resource-group myTerraformRG --location eastus --sku Standard_LRS az storage container create --name tfstate --account-name mytfstorage ``` Now, configure **Terraform’s backend** to use this storage: ``` terraform { backend "azurerm" { resource_group_name = "myTerraformRG" storage_account_name = "mytfstorage" container_name = "tfstate" key = "terraform.tfstate" } } ``` **Why It’s Important:** This ensures **state consistency** and allows multiple team members to use Terraform safely. --- ## **3. Creating an Azure DevOps Pipeline for Terraform** Azure DevOps **pipelines** let you run Terraform automatically every time you push a change to Git. Let’s create one! ### **Step 1: Set Up Azure DevOps** 1. **Create a new repository** in Azure DevOps for your Terraform code. 2. **Enable Pipelines** in your project. 3. **Store Terraform configuration** (`main.tf`, `variables.tf`, etc.) in the repo. ### **Step 2: Define the Azure DevOps Pipeline (`azure-pipelines.yml`)** Create a **pipeline file** (`azure-pipelines.yml`) in the root of your repo: ``` trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: TerraformInstaller@0 displayName: "Install Terraform" inputs: terraformVersion: '1.5.0' - script: terraform init displayName: "Initialize Terraform" - script: terraform plan -out=tfplan displayName: "Terraform Plan" - script: terraform apply -auto-approve tfplan displayName: "Terraform Apply" ``` ### **Breaking It Down:** **Installs Terraform** on the Azure DevOps agent. **Initializes Terraform** (`terraform init`). **Runs Terraform Plan** (`terraform plan`) to preview changes. **Applies changes** (`terraform apply`) automatically. **Now, every commit to `main` triggers Terraform!** --- ## **4. Using Terraform Variables in Azure DevOps** Instead of hardcoding values, **use Azure DevOps variables** for dynamic configurations! ### **Step 1: Define Variables in Azure DevOps** 1\. Go to **Pipelines** → **Library** → **Add a Variable Group**. 2\. Add variables like: - `TF_VAR_location = eastus` - `TF_VAR_environment = dev` ### **Step 2: Reference Variables in Terraform (`variables.tf`)** ``` variable "location" {} variable "environment" {} ``` ### **Step 3: Modify the Pipeline to Use Variables** ``` steps: - script: terraform apply -var="location=$(TF_VAR_location)" -auto-approve displayName: "Apply Terraform with Variables" ``` **Why It’s Cool:** Now you can deploy to **different environments** without changing Terraform code! --- ## **5. Adding Terraform Security Checks to the Pipeline** Infrastructure security **shouldn’t be an afterthought**! Add security scans to Terraform pipelines using **tfsec** and **Checkov**. ### **Step 1: Add `tfsec` to Your Pipeline** Modify `azure-pipelines.yml` to include a **security scan** before applying Terraform: ``` - script: | curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash tfsec . displayName: "Run tfsec Security Scan" ``` **Now, Terraform will fail if there are security issues!** --- ## **6. Using Terraform Workspaces for Multi-Environment Deployments** Instead of maintaining **separate Terraform configurations** for dev, staging, and prod, **use Terraform workspaces**. ### **Step 1: Add Workspaces to Your Pipeline** Modify `azure-pipelines.yml` to dynamically switch workspaces: ``` - script: terraform workspace select $(TF_VAR_environment) || terraform workspace new $(TF_VAR_environment) displayName: "Select or Create Terraform Workspace" ``` **Now, one pipeline manages multiple environments!** --- ## **7. Best Practices for Terraform in Azure DevOps** **Use remote state** – Store Terraform state in Azure Blob Storage. **Secure credentials** – Use **Azure Key Vault** for sensitive secrets. **Implement approval gates** – Require manual approval before applying changes. **Run security scans** – Use **tfsec** and **Checkov** in pipelines. **Use workspaces** – Manage multiple environments without duplication. --- ## **Wrapping Up** By integrating **Terraform with Azure DevOps**, you can: - **Automate cloud deployments** with CI/CD. - **Maintain a secure, version-controlled Terraform workflow**. - **Deploy infrastructure safely to multiple environments**. Now, go build something **awesome and automated!** --- ### **What’s Next?** Terraform automation is great, but **what about cloud cost optimization?** In the next post, **“Cost Management with Terraform,”** we’ll explore how to **track, analyze, and reduce cloud costs** using Terraform’s built-in features and third-party tools—so you can save money while keeping your infrastructure scalable. **Categories:** Terraform **Tags:** Azure, devops, IaC, infrastructure, terraform --- ### [Lock It Down: Security Ideas for Terraform Deployments](https://www.woodruff.dev/lock-it-down-security-ideas-for-terraform-deployments/) **Published:** February 25, 2025 **Author:** Chris Woodruff **Excerpt:** Terraform makes infrastructure automation easy, but if you’re not careful, it can also open the door to security risks—misconfigured permissions, exposed secrets, and unintended data leaks. Let’s talk about how to secure your Terraform deployments so you don’t end up as the next cloud security horror story. **Content:** Terraform makes infrastructure automation easy, but if you’re not careful, it can also **open the door to security risks**—misconfigured permissions, exposed secrets, and unintended data leaks. Let’s talk about **how to secure your Terraform deployments** so you don’t end up as the next cloud security horror story. In this post, we’ll cover: - Keeping **secrets safe** (no hardcoded passwords!) - Enforcing **least privilege access** - Securing your **Terraform state** - Using **security tools** to catch vulnerabilities Let’s lock it down! --- ## **1. Never, Ever Hardcode Secrets** One of the biggest Terraform mistakes? **Hardcoding API keys, passwords, or credentials directly in your `.tf` files`.** Imagine this: ``` resource "aws_instance" "web" { ami = "ami-123456" instance_type = "t2.micro" user_data = "export DB_PASSWORD='supersecret123'" } ``` Oops. Now, anyone with access to this file (or its Git history) has your **database password**. ### **How to Store Secrets Securely** Use **environment variables**: ``` export TF_VAR_db_password="supersecret123" ``` Use **Terraform Vault, AWS Secrets Manager, or Azure Key Vault**: ``` data "azurerm_key_vault_secret" "db_password" { name = "db-password" key_vault_id = azurerm_key_vault.example.id } ``` Use `sensitive = true` in Terraform outputs to **hide secrets** in logs: ``` output "db_password" { value = azurerm_key_vault_secret.db_password.value sensitive = true } ``` **Pro Tip:** Add `*.tfstate` and `terraform.tfvars` to your `.gitignore` file to **avoid committing secrets by accident!** --- ## **2. Secure Your Terraform State** Terraform **state files contain sensitive data**, including resource IDs, credentials, and network configurations. If an attacker gets access to your `terraform.tfstate` file, **they own your infrastructure**. ### **Best Practices for Securing Terraform State** - **Use remote state storage** (Azure Blob Storage, AWS S3) instead of local files. - **Enable encryption** for state files. - **Use state locking** to prevent multiple users from overwriting changes. ### **Example: Secure State Storage in Azure Blob Storage** ``` terraform { backend "azurerm" { resource_group_name = "myResourceGroup" storage_account_name = "mystorageaccount" container_name = "tfstate" key = "terraform.tfstate" } } ``` **Pro Tip:** If you’re using Terraform Cloud, enable **state access control** to restrict who can view and edit state files. --- ## **3. Enforce Least Privilege Access** Terraform needs access to **cloud resources**, but it shouldn’t have **god-mode permissions**. Following the **principle of least privilege (PoLP)** ensures Terraform **only has access to what it absolutely needs**. ### **Best Practices for Least Privilege Access** - **Use IAM roles & service accounts** instead of personal credentials. - **Limit scope** (avoid giving Terraform full admin rights). - **Rotate access keys regularly**. ### **Example: Least Privilege Role in Azure** ``` resource "azurerm_role_assignment" "terraform_role" { scope = azurerm_resource_group.example.id role_definition_name = "Contributor" principal_id = azuread_service_principal.example.id } ``` **Pro Tip:** Avoid using **root/admin accounts** for Terraform deployments—create a dedicated Terraform service principal instead! --- ## **4. Use Security Scanners to Catch Misconfigurations** Even the best Terraform developers make mistakes. That’s why you should **automate security checks** with tools like: - **Tfsec** – Scans Terraform code for security vulnerabilities. - **Checkov** – Policy-as-code tool that enforces security rules. - **Terraform Cloud Sentinel** – Enforces security policies before applying changes. ### **Example: Run `tfsec` to Scan for Security Risks** ``` tfsec . ``` Output: ``` WARNING: Hardcoded credentials found in main.tf ``` **Pro Tip:** Add security scanning to your **CI/CD pipeline** to catch security issues **before deployment!** --- ## **5. Implement Role-Based Access Control (RBAC)** If multiple team members are using Terraform, you **don’t want everyone making changes**. **RBAC** lets you control who can apply, plan, or modify Terraform configurations. ### **Best Practices for Terraform RBAC** - **Developers**: Can run `terraform plan`, but not apply changes. - **Admins**: Can approve and apply infrastructure changes. - **Security Teams**: Can audit and review configurations. ### **Example: Enforcing RBAC in Terraform Cloud** Terraform Cloud lets you set **role-based permissions** per team: ``` - Dev Team: "Plan Only" - Ops Team: "Apply and Manage Workspaces" - Security Team: "Read-Only Access" ``` **Pro Tip:** Always enable **audit logs** to track who made changes in Terraform Cloud. --- ## **6. Use Multi-Factor Authentication (MFA) for Cloud Access** Terraform deployments require **API keys and cloud credentials**. If those get compromised, **attackers can take control of your infrastructure**. ### **How to Protect Cloud Credentials** - **Enable MFA** for AWS, Azure, and GCP accounts. - **Rotate API keys** every 90 days. - **Use short-lived access tokens** instead of long-lived keys. ### **Example: AWS Session-Based Authentication with MFA** ``` aws configure set aws_session_token $(aws sts get-session-token --serial-number arn:aws:iam::123456789012:mfa/user --token-code 123456) ``` **Pro Tip:** Store **secrets in HashiCorp Vault** instead of environment variables. --- ## **7. Secure Network Configurations** Terraform **provisions cloud networking resources**, but if you’re not careful, you could accidentally expose **databases, VMs, or Kubernetes clusters** to the public internet. ### **Best Practices for Secure Network Configurations** - **Use private subnets** instead of public subnets for sensitive resources. - **Restrict inbound/outbound traffic** with security groups. - **Limit access to Terraform servers** (only trusted IPs). ### **Example: Lock Down an Azure VM with a Security Group** ``` resource "azurerm_network_security_group" "example" { name = "nsg-example" resource_group_name = "myResourceGroup" location = "East US" security_rule { name = "allow-ssh" priority = 100 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" source_address_prefix = "192.168.1.0/24" destination_address_prefix = "*" } } ``` **Pro Tip:** Use **VPC peering and private endpoints** for secure cloud networking. --- ## **Wrapping Up** Terraform security isn’t optional—it’s **essential**. By following these **best practices**, you’ll keep your infrastructure **secure, resilient, and breach-proof**. **Quick Recap:** - **Never hardcode secrets**—use Vault or Key Management Systems. - **Secure Terraform state** with encryption and remote storage. - **Enforce least privilege access**—no over-permissioned Terraform accounts! - **Scan Terraform code** for security issues before deployment. - **Use MFA, API key rotation, and RBAC** for better cloud security. Now, go **Terraform safely!** --- ### **What’s Next?** Now that you’ve locked down Terraform, it’s time to integrate it with **Azure DevOps for automated deployments**. In the next post, **“Using Terraform with Azure DevOps,”** we’ll walk through setting up Terraform pipelines in **Azure DevOps**, automating deployments, and managing infrastructure with **version-controlled CI/CD workflows**. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Good Practices: Avoiding Chaos and Building with Confidence](https://www.woodruff.dev/terraform-good-practices-avoiding-chaos-and-building-with-confidence/) **Published:** February 24, 2025 **Author:** Chris Woodruff **Excerpt:** Terraform is an amazing tool for managing infrastructure, but without best practices, things can get messy—fast. Imagine a world where: Terraform state files vanish into thin air. Untracked changes wreck your deployments. Infrastructure drifts into an unknown state. Sounds like a nightmare, right? **Content:** Terraform is a fantastic tool for managing infrastructure, but **without good practices, things can get messy—fast**. Imagine a world where: - Terraform state files vanish into thin air. - Untracked changes wreck your deployments. - Infrastructure drifts into an unknown state. Sounds like a nightmare, right? In this post, we’ll cover **Terraform best practices** that will keep your infrastructure **secure, scalable, and maintainable**—without turning into a DevOps horror story. Let’s get started! --- ## **1. Use Remote State to Prevent Disaster** Terraform’s **state file** (`terraform.tfstate`) is the **single source of truth** for your infrastructure. Losing or corrupting it is like losing the **map to your treasure**—and that treasure is your entire cloud environment! ### **Bad Idea: Storing State Locally (Don’t Do This!)** By default, Terraform stores state **on your local machine**, which means: - If your laptop dies, so does your state file. - You can’t collaborate with a team. - Risk of accidentally deleting infrastructure. ### **Best Practice: Use a Remote State Backend** Store your Terraform state in a **remote backend** like **Azure Blob Storage, AWS S3, or Terraform Cloud** to: - Enable **team collaboration**. - Keep **state files secure** and backed up. - Prevent **accidental loss** of infrastructure data. #### **Example: Storing State in Azure Blob Storage** ``` terraform { backend "azurerm" { resource_group_name = "myResourceGroup" storage_account_name = "mystorageaccount" container_name = "tfstate" key = "terraform.tfstate" } } ``` **Bonus:** Use **state locking** to prevent multiple people from modifying Terraform at the same time. --- ## **2. Keep Your Terraform Code DRY (Don’t Repeat Yourself)** Copy-pasting Terraform code across multiple projects or environments is **a disaster waiting to happen**. Instead, make your code **modular and reusable**. ### **Best Practice: Use Modules** Terraform **modules** let you define **reusable infrastructure components** that can be called from different projects. #### **Example: A Module for Deploying an Azure Resource Group** Create a **module folder**: ``` mkdir -p modules/resource_group ``` Inside `modules/resource_group/main.tf`: ``` resource "azurerm_resource_group" "rg" { name = var.resource_group_name location = var.location } ``` Now, **call the module** in your Terraform config: ``` module "my_resource_group" { source = "./modules/resource_group" resource_group_name = "TerraformRG" location = "East US" } ``` **Result:** Clean, reusable, and maintainable Terraform configurations! --- ## **3. Use Variables and Outputs for Flexibility** Hardcoding values in Terraform is a **recipe for chaos**. Instead, use **variables and outputs** to make your configurations dynamic. ### **Example: Using Variables for Environment-Specific Configs** ``` variable "environment" { description = "Deployment environment" default = "dev" } variable "vm_size" { type = map(string) default = { dev = "Standard_DS1_v2" prod = "Standard_DS3_v2" } } resource "azurerm_virtual_machine" "example" { name = "myVM" vm_size = var.vm_size[var.environment] } ``` **Now, switching environments is as easy as changing one variable!** --- ## **4. Use Workspaces for Multi-Environment Deployments** Instead of maintaining **separate Terraform configs for dev, staging, and prod**, use **Terraform workspaces** to manage them dynamically. ### **Best Practice: Workspaces for Dev, Staging, and Prod** ``` terraform workspace new dev terraform apply terraform workspace new prod terraform apply ``` **Benefit:** One **codebase, multiple environments**, no duplication! --- ## **5. Implement Terraform CI/CD Pipelines** **Would you manually run Terraform commands every time you deploy?** Heck no! Instead, automate Terraform with **CI/CD pipelines** like GitHub Actions, Azure DevOps, or GitLab CI/CD. ### **Example: GitHub Actions Terraform Pipeline** ``` name: Terraform Deployment on: push: branches: - main jobs: terraform: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Setup Terraform uses: hashicorp/setup-terraform@v1 - name: Terraform Init run: terraform init - name: Terraform Plan run: terraform plan - name: Terraform Apply run: terraform apply -auto-approve ``` **Benefit:** **No manual deployments** → Terraform runs automatically when code changes! --- ## **6. Validate and Format Your Code Before Applying** Before applying Terraform, **always validate and format your code** to catch errors early. ### **Run These Checks Before Deployment** **1. Format your Terraform code** (fixes indentation & styling issues): ``` terraform fmt ``` **2. Validate your configuration** (checks for syntax errors): ``` terraform validate ``` **3. Preview what Terraform will do** before applying: ``` terraform plan ``` **Why It’s Important:** Prevents accidental mistakes before making changes! --- ## **7. Secure Secrets: NEVER Hardcode API Keys or Passwords!** Terraform configurations **often require sensitive credentials** (e.g., database passwords, API keys). **Never hardcode these values**—use **Terraform Vault, AWS Secrets Manager, or Azure Key Vault** instead. ### **Example: Storing Secrets in Azure Key Vault** ``` resource "azurerm_key_vault_secret" "example" { name = "db-password" value = "super-secret-password" key_vault_id = azurerm_key_vault.example.id } ``` **Benefit:** No hardcoded secrets, improved security! --- ## **Wrapping Up** Terraform is **powerful**, but only if used **correctly**. By following these best practices, you’ll build **scalable, secure, and maintainable** infrastructure **without the headaches**. **Quick Recap:** - **Use Remote State** → Never store state locally! - **Write Reusable Modules** → No copy-pasting infrastructure code! - **Use Variables & Outputs** → Make configs dynamic! - **Use Workspaces** → Manage multiple environments cleanly! - **Automate Terraform with CI/CD** → No manual work needed! - **Validate Before Applying** → Avoid mistakes! - **Secure Secrets** → No hardcoded passwords! Now, go **Terraform smarter, not harder!** --- ### **What’s Next?** Following best practices is great, but security should always be a top priority in Terraform deployments. In the next post, **“Security in Terraform Deployments,”** we’ll explore how to protect sensitive data, enforce least privilege access, and secure your infrastructure from misconfigurations—so your cloud stays safe from threats. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform in the Wild: Real-World Use Cases That Make Cloud Magic Happen](https://www.woodruff.dev/terraform-in-the-wild-real-world-use-cases-that-make-cloud-magic-happen/) **Published:** February 23, 2025 **Author:** Chris Woodruff **Excerpt:** Terraform isn't just a fancy tool for spinning up VMs—it’s the backbone of modern cloud automation. Whether you're managing multi-cloud environments, automating disaster recovery, or deploying Kubernetes clusters, Terraform has got you covered. In this post, we’ll dive into practical use cases where Terraform truly shines, proving that Infrastructure as Code (IaC) isn’t just a buzzword—it’s a game-changer. **Content:** Terraform isn’t just a fancy tool for spinning up VMs—it’s the backbone of modern cloud automation. Whether you’re managing **multi-cloud environments, automating disaster recovery, or deploying Kubernetes clusters**, Terraform has got you covered. In this post, we’ll explore **practical use cases** where Terraform truly shines, proving that **Infrastructure as Code (IaC) isn’t just a buzzword—it’s a game-changer**. Ready to see Terraform in action? Let’s go! --- ## **1. Multi-Cloud Deployments: One Config, Any Cloud** Did you ever want to **run workloads on** **AWS and Azure** without losing your mind? Terraform makes it easy by **abstracting cloud-specific APIs** and giving you a **consistent way to define infrastructure**. ### **Example: Deploying a VM in AWS and Azure with the Same Code** ``` provider "aws" { region = "us-east-1" } provider "azurerm" { features {} } resource "aws_instance" "web" { ami = "ami-123456" instance_type = "t2.micro" } resource "azurerm_virtual_machine" "web" { name = "azureVM" location = "East US" resource_group_name = "myResourceGroup" vm_size = "Standard_DS1_v2" } ``` **Why It’s Cool:** - Use **one tool** for multiple cloud providers. - Avoid vendor lock-in. - Create **portable, scalable** infrastructure. **Pro Tip:** Use Terraform **workspaces** to manage different cloud environments with a single configuration. --- ## **2. Automating Disaster Recovery: Never Lose Sleep Again** Imagine your entire infrastructure crashes. With Terraform, you can **rebuild everything automatically**, reducing downtime and panic. ### **How Terraform Helps with DR:** - **Backup Terraform State** in remote storage (e.g., Azure Blob, AWS S3). - **Use modules** to spin up an identical environment in a different region. - **Leverage remote backends** so your team can recover infra from anywhere. ### **Example: Storing Terraform State in Azure Blob Storage** ``` terraform { backend "azurerm" { resource_group_name = "myResourceGroup" storage_account_name = "mystorageaccount" container_name = "tfstate" key = "terraform.tfstate" } } ``` **Why It’s Cool:** - **Fast recovery** from outages. - **Consistent infrastructure** across regions. - Automate **failover strategies** with IaC. --- ## **3. Scaling Kubernetes Clusters on Demand** Terraform makes Kubernetes (K8s) **easy to deploy and scale**, whether you’re on **Azure Kubernetes Service (AKS), Amazon EKS, or Google Kubernetes Engine (GKE)**. ### **Example: Deploying an AKS Cluster with Terraform** ``` resource "azurerm_kubernetes_cluster" "aks" { name = "myAKSCluster" location = "East US" resource_group_name = "myResourceGroup" dns_prefix = "myaks" default_node_pool { name = "agentpool" node_count = 3 vm_size = "Standard_DS2_v2" } } ``` **Why It’s Cool:** - **Easily scale clusters** by changing node count. - Automate **rolling updates** for zero-downtime deployments. - Consistently deploy **K8s across cloud providers**. Pro Tip: Use Terraform’s for\_each to create multiple node pools based on workload needs dynamically. --- ## **4. Infrastructure as Code + CI/CD: Deploy Faster, Smarter** Terraform can be **integrated with CI/CD pipelines** to deploy infrastructure changes when code is updated automatically. ### **Example: Terraform in GitHub Actions** ``` name: Terraform Deployment on: push: branches: - main jobs: terraform: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 - name: Terraform Init run: terraform init - name: Terraform Plan run: terraform plan - name: Terraform Apply run: terraform apply -auto-approve ``` **Why It’s Cool:** - **Automates deployments**—no manual steps! - Reduces **human error** in infrastructure changes. - Works with **Azure DevOps, GitHub Actions, GitLab CI/CD, and Jenkins**. **Pro Tip:** Use **Terraform Cloud** to trigger runs when new commits are pushed. --- ## **5. Managing Multi-Environment Deployments** Tired of **manually maintaining separate Terraform configs** for dev, staging, and prod? Terraform’s **workspaces** let you manage multiple environments from a **single codebase**. ### **Example: Using Workspaces for Dev & Prod** ``` terraform workspace new dev terraform workspace select dev terraform apply terraform workspace new prod terraform workspace select prod terraform apply ``` **Why It’s Cool:** - No need for **duplicate code** across environments. - Keep **state files isolated** per workspace. - Simplifies **environment-specific settings**. **Pro Tip:** Use **Terraform variables** to dynamically change configurations based on the workspace. --- ## **Wrapping Up** Terraform isn’t just for **spinning up a few VMs**—it’s a powerful tool that can automate, scale, and manage **complex infrastructure at any level**. Whether you’re building for **AWS, Azure, Kubernetes, or multi-cloud environments**, Terraform’s capabilities **make your cloud operations smoother and smarter**. **Quick Recap:** - **Multi-Cloud Deployments** → Run infra across AWS, Azure, GCP. - **Disaster Recovery** → Auto-rebuild infra when disaster strikes. - **Scaling Kubernetes** → Deploy & scale K8s clusters with ease. - **CI/CD Pipelines** → Automate infra changes with GitHub Actions. - **Multi-Environment Setups** → Use workspaces for dev, staging, prod. Now, go **Terraform your world!** --- ### **What’s Next?** In the next post, we’ll break down **Terraform’s Best Practices for Large-Scale Deployments** so you can manage enterprise-grade infrastructure with confidence. Stay tuned! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Power Moves: Unlocking Advanced Features for Smarter Infrastructure](https://www.woodruff.dev/terraform-power-moves-unlocking-advanced-features-for-smarter-infrastructure/) **Published:** February 22, 2025 **Author:** Chris Woodruff **Excerpt:** So, you’ve mastered Terraform basics, spun up some resources, and maybe even dabbled with modules. But what if I told you Terraform can do even more? In this post, we’re going to explore Advanced Terraform Features that will make your infrastructure smarter, more dynamic, and easier to manage. **Content:** So, you’ve mastered Terraform basics, spun up some resources, and maybe even dabbled with modules. But **what if I told you Terraform can do even more?** In this post, we’re going to explore **Advanced Terraform Features** that will make your infrastructure smarter, more dynamic, and easier to manage. We’ll cover: - **Provisioners** (for running scripts on resources) - **Functions** (for making configs dynamic) - **Workspaces** (for managing multiple environments) Let’s go beyond the basics and Terraform like a pro! --- ## **1. Terraform Provisioners: Running Commands on Your Resources** Terraform **provisioners** allow you to **execute scripts or commands** on a resource **after** it’s been created. Think of it as Terraform’s way of saying: > “Hey, I deployed your VM. Now, let me install Nginx on it.” ### **Example: Remote Provisioner (SSH into a VM & Install Nginx)** ``` resource "azurerm_virtual_machine" "example" { name = "myVM" resource_group_name = "myResourceGroup" location = "East US" vm_size = "Standard_DS1_v2" os_profile { computer_name = "myVM" admin_username = "adminuser" } provisioner "remote-exec" { connection { type = "ssh" user = "adminuser" private_key = file("~/.ssh/id_rsa") host = azurerm_public_ip.example.ip_address } inline = [ "sudo apt-get update", "sudo apt-get install nginx -y" ] } } ``` ### **When to Use Provisioners (and When NOT to)** **Good Use Cases:** - Installing software on VMs **after** creation. - Running configuration scripts **after** resource deployment. **Bad Use Cases:** - Managing infrastructure dependencies (use modules instead). - Orchestrating multiple servers (use Ansible or Chef for that). **Pro Tip:** Use **cloud-init** or **VM images** instead of provisioners when possible. --- ## **2. Terraform Functions: Adding Logic to Your Configurations** Terraform **functions** allow you to **manipulate data dynamically** inside your configuration files. Think of them as Terraform’s built-in Swiss Army knife. ### **Example 1: Using `join()` to Create a Comma-Separated List** ``` output "environments" { value = join(", ", ["dev", "staging", "prod"]) } ``` **Output:** ``` "dev, staging, prod" ``` ### **Example 2: Using `length()` to Count Items in a List** ``` output "vm_count" { value = length(["web1", "web2", "web3"]) } ``` **Output:** ``` 3 ``` ### **Example 3: Conditional Logic with the `lookup()` Function** ``` variable "environment" { default = "dev" } output "vm_size" { value = lookup( { dev = "Standard_DS1_v2", prod = "Standard_DS3_v2" }, var.environment ) } ``` For `environment = "prod"`, the output would be: ``` "Standard_DS3_v2" ``` 💡 **Pro Tip:** Functions make your Terraform configs **smarter and more adaptable**! --- ## **3. Terraform Workspaces: Managing Multiple Environments** If you’re managing **multiple environments** (e.g., `dev`, `staging`, `prod`), Terraform **workspaces** let you use the **same configuration** but maintain **separate state files**. Instead of maintaining **separate folders**, you can **switch workspaces** dynamically! ### **How to Use Workspaces** #### **Step 1: Create a New Workspace** ``` terraform workspace new dev ``` #### **Step 2: Check Your Current Workspace** ``` terraform workspace show ``` #### **Step 3: Switch Workspaces** ``` terraform workspace select prod ``` ### **Example: Using Workspaces in Your Configuration** You can reference the workspace inside `main.tf`: ``` resource "azurerm_storage_account" "example" { name = "storage-${terraform.workspace}" resource_group_name = "myResourceGroup" location = "East US" account_tier = "Standard" } ``` For the **`dev` workspace**, this would create: ``` storage-dev ``` For the **`prod` workspace**, this would create: ``` storage-prod ``` **Workspaces keep environments separate without duplicating Terraform code!** --- ## **4. Dynamic Configuration with Count & For-Each Loops** Terraform allows **looping** using `count` and `for_each` to create multiple resources **dynamically**. ### **Example: Creating Multiple VMs with `count`** ``` resource "azurerm_virtual_machine" "example" { count = 3 name = "web-${count.index}" resource_group_name = "myResourceGroup" location = "East US" } ``` This will create: ``` web-0 web-1 web-2 ``` ### **Example: Using `for_each` with Maps** ``` variable "vm_names" { default = { dev = "web-dev" prod = "web-prod" } } resource "azurerm_virtual_machine" "example" { for_each = var.vm_names name = each.value resource_group_name = "myResourceGroup" } ``` This dynamically creates a VM for **each environment**! --- ## **Terraform Advanced Features: Quick Recap** FeatureWhat It Does**Provisioners**Run commands/scripts on resources after creation**Functions**Add logic & transformations (e.g., conditionals, string manipulation)**Workspaces**Manage multiple environments easily**Count & For-Each**Dynamically create multiple resources--- ## **Wrapping Up** Now that you’ve unlocked Terraform’s **advanced features**, your infrastructure can be **more flexible, automated, and efficient**. **Quick Recap:** - **Provisioners** automate post-deployment scripts. - **Functions** make your configurations dynamic. - **Workspaces** separate environments without duplicate code. - **Loops (`count` & `for_each`)** create multiple resources dynamically. **Now go Terraform like a boss!** --- ### **What’s Next?** In the next post, we’ll tackle **“Scaling Terraform Projects: Best Practices for Large-Scale Deployments”**. Stay tuned! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Modules: Stop Copy-Pasting and Start Reusing Like a Pro](https://www.woodruff.dev/terraform-modules-stop-copy-pasting-and-start-reusing-like-a-pro/) **Published:** February 21, 2025 **Author:** Chris Woodruff **Excerpt:** Ever feel like you’re copy-pasting the same Terraform code over and over again? Deploying resource groups, VMs, networks—all with slightly different names? Yeah, that’s a nightmare waiting to happen. Luckily, Terraform modules exist to make our lives easier! Modules let you write infrastructure code once and reuse it across different projects, environments, or even teams. **Content:** Ever feel like you’re copy-pasting the same Terraform code over and over again? Deploying **resource groups, VMs, networks**—all with slightly different names? Yeah, that’s a nightmare waiting to happen. Luckily, Terraform **modules** exist to make our lives easier! Modules let you **write infrastructure code once** and reuse it across different projects, environments, or even teams. In this post, we’ll explore **how Terraform modules work, why they’re awesome, and how to create your own**. Let’s go! --- ## **Why Use Modules?** Imagine you’re a **pizza chef**. Would you manually mix the dough, make the sauce, and prep toppings from scratch every single time? Or would you **use pre-made components** to speed things up? Terraform modules are like **pre-made pizza bases**—they save time, enforce consistency, and make managing infrastructure a breeze. - **Reuse configurations** across projects. - **Reduce errors** by following best practices. - **Make infrastructure modular** and easy to manage. - **Keep code DRY** (Don’t Repeat Yourself). --- ## **Types of Terraform Modules** There are **two main types** of Terraform modules: 1\. **Root Module** (your main Terraform configuration). 2\. **Child Modules** (reusable components you call from the root module). ### **Where Can You Find Modules?** - **Terraform Registry**: Pre-built modules you can use instantly → registry.terraform.io - **Your Own Modules**: Custom modules specific to your organization. --- ## **Creating Your First Terraform Module** Let’s say you want to create a **module for an Azure Resource Group** that can be reused across multiple environments. ### **Step 1: Create a Module Directory** Inside your Terraform project, create a `modules/resource_group` folder: ``` mkdir -p modules/resource_group ``` ### **Step 2: Define the Resource (`main.tf`)** Inside `modules/resource_group/main.tf`, define a resource group: ``` resource "azurerm_resource_group" "rg" { name = var.resource_group_name location = var.location } ``` ### **Step 3: Define Input Variables (`variables.tf`)** ``` variable "resource_group_name" { description = "The name of the resource group" type = string } variable "location" { description = "The Azure region" type = string default = "East US" } ``` ### **Step 4: Define Outputs (`outputs.tf`)** ``` output "resource_group_name" { value = azurerm_resource_group.rg.name } ``` --- ## **Using the Module in Your Main Terraform Config** Now, let’s call this module from our **root Terraform configuration** (`main.tf`): ``` module "my_resource_group" { source = "./modules/resource_group" resource_group_name = "TerraformRG" location = "West US" } ``` Now when you run `terraform apply`, Terraform will **call the module** and create the resource group dynamically! --- ## **Working with Public Modules** Want to **reuse** someone else’s module? No problem! Terraform has a **public registry** with pre-built modules. For example, to create an **Azure Storage Account**, you can use a module from the Terraform Registry: ``` module "storage_account" { source = "Azure/azurerm/terraform" version = "3.0.0" storage_account_name = "myterraformstorage" location = "East US" } ``` That’s it—Terraform **fetches the module, configures it, and deploys** it for you. --- ## **Best Practices for Terraform Modules** - **Keep modules small** – Each module should handle **one** task (e.g., VMs, networks). - **Use input variables** – Allow customization when calling the module. - **Define outputs** – Make it easy to pass values between modules. - **Store modules in Git** – Version control your infrastructure just like application code. - **Use the Terraform Registry** – Don’t reinvent the wheel if a module already exists. --- ## **Wrapping Up** Terraform modules are **game-changers** when it comes to managing infrastructure efficiently. They **save time, improve code quality, and keep your configurations modular**. **Quick Recap:** - **Modules = Reusable Terraform code** - **Use input variables & outputs** - **Call modules from your root Terraform config** - **Use the Terraform Registry for pre-built solutions** Now, go forth and **modularize your Terraform projects**! --- ### **What’s Next?** In the next post, we’ll dive into **Advanced Terraform Features** like **provisioners, functions, and workspaces** to level up your infrastructure skills. Stay tuned! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Kill the Bloat: The Controversial Clash Between SPAs, Server-Side Rendering, and the Power of Simplicity](https://www.woodruff.dev/kill-the-bloat-the-controversial-clash-between-spas-server-side-rendering-and-the-power-of-simplicity/) **Published:** February 20, 2025 **Author:** Chris Woodruff **Excerpt:** Ever feel like half your development time goes to chasing down strange bugs in a colossal JavaScript stack that only a wizard could appreciate? You’re not alone. In the realm of web apps, we’re always trying to determine whether it’s better to handle everything on the server or shift as much as possible to the browser. This discussion isn’t just about technology—it’s about maintaining manageable code, keeping our sanity intact, and ensuring our users are happy. Enter the Simplicity-First philosophy, which reminds us that while fancy frameworks and cutting-edge libraries are appealing, they become useless if they overwhelm our code with excessive complexity. Simplicity-First is the sidekick that shows up with a utility belt full of best practices: naming things well, writing readable functions, and structuring elements so that future you (or your teammates) don’t have to pull out their hair. Buckle up as we embark on a journey through the history of server-side vs. client-side, explore why understanding your domain is crucial, and discover how new tools like htmx or server-first frameworks might bring peace to the galaxy. **Content:** Ever feel like half your development time goes to chasing down strange bugs in a colossal JavaScript stack that only a wizard could appreciate? You’re not alone. In the realm of web apps, we’re always trying to determine whether it’s better to handle everything on the server or shift as much as possible to the browser. This discussion isn’t just about technology—it’s about maintaining manageable code, keeping our sanity intact, and ensuring our users are happy. Enter the Simplicity-First philosophy, which reminds us that while fancy frameworks and cutting-edge libraries are appealing, they become useless if they overwhelm our code with excessive complexity. Simplicity-First is the sidekick that shows up with a utility belt full of best practices: naming things well, writing readable functions, and structuring elements so that future you (or your teammates) don’t have to pull out their hair. Buckle up as we embark on a journey through the history of server-side vs. client-side, explore why understanding your domain is crucial, and discover how new tools like htmx or server-first frameworks might bring peace to the galaxy. #### [Read more at Simplicity-First](https://simplicity-first.dev/kill-the-bloat/) **Categories:** Blog **Tags:** architectur, design, development, simplicity-first, software philosophy --- ### [Terraform Variables & Outputs: The Secret Sauce of Reusable Infrastructure](https://www.woodruff.dev/terraform-variables-outputs-the-secret-sauce-of-reusable-infrastructure/) **Published:** February 20, 2025 **Author:** Chris Woodruff **Excerpt:** If you've ever hardcoded values in Terraform, I have some bad news… and some great news! The bad news? Hardcoding is a one-way ticket to frustration and messy code. The great news? Terraform variables and outputs can save your sanity by making your infrastructure code dynamic, reusable, and scalable. **Content:** If you’ve ever hardcoded values in Terraform, I have some bad news… and some great news! The **bad news**? Hardcoding is a one-way ticket to frustration and messy code. The **great news**? Terraform variables and outputs can **save your sanity** by making your infrastructure code **dynamic, reusable, and scalable**. In this post, we’ll break down **how to use Terraform variables** like a pro and how **outputs** can help you retrieve useful information from your deployments. Buckle up! --- ## **Why Should You Use Variables?** Imagine you’re deploying infrastructure for **multiple environments** (dev, staging, prod). If you **hardcode values**, you’ll have to maintain **separate Terraform files** for each environment—yikes! **Terraform variables fix this by:** 1. Making configurations **reusable** across multiple environments. 2. **Reducing code duplication** and maintenance headaches. 3. Allowing **easy customization** via command-line, environment variables, or files. --- ## **Types of Variables in Terraform** Terraform supports **three types of variables**, and each serves a unique purpose: **Variable Type****What It Stores****Example****String**Text values`"eastus"`**Number**Numeric values`2`**Boolean**True/False values`true`**List**A collection of values`["dev", "staging", "prod"]`**Map**Key-value pairs`{ env = "prod", region = "us-west" }`--- ## **Declaring and Using Variables** Terraform variables are **declared** in a `variables.tf` file. Here’s how you define a **string variable** for an Azure region: ``` variable "location" { description = "The Azure region for deployment" type = string default = "East US" } ``` Now, instead of hardcoding the location, reference it like this in `main.tf`: ``` resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = var.location } ``` ### **Passing Variables in Terraform** You can set variable values in **multiple ways**: 1. **Command-Line Flags**bashCopyEdit`terraform apply -var="location=West US"` 2. **Environment Variables**bashCopyEdit`export TF_VAR_location="West US"` 3. **Variable Files (`.tfvars`)** Create a `terraform.tfvars` file:hclCopyEdit`location = "West US" `Then apply it:bashCopyEdit`terraform apply -var-file="terraform.tfvars"` **Pro Tip:** Use `.tfvars` files for different environments (`dev.tfvars`, `prod.tfvars`) to easily switch configurations. --- ## **Using Lists and Maps** Want to make your Terraform configs even more **powerful**? Use **lists** and **maps** to store multiple values. #### **Example: Using a List** A list allows multiple values to be stored dynamically. ``` variable "allowed_locations" { description = "List of approved Azure regions" type = list(string) default = ["East US", "West US", "Central US"] } ``` To use a list value: ``` resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = var.allowed_locations[0] # Selects "East US" } ``` #### **Example: Using a Map** A map stores key-value pairs for better organization. ``` variable "environment_configs" { description = "Settings per environment" type = map(string) default = { dev = "Standard_DS1_v2" staging = "Standard_DS2_v2" prod = "Standard_DS3_v2" } } ``` To retrieve values dynamically: ``` resource "azurerm_virtual_machine" "example" { name = "myVM" vm_size = var.environment_configs["prod"] # "Standard_DS3_v2" } ``` --- ## **Outputs: Getting Useful Info from Terraform** Terraform **outputs** let you **retrieve key details** from your infrastructure after deployment—like the name of a resource group or the public IP of a VM. ### **Defining Outputs** Outputs are declared in an `outputs.tf` file like this: ``` output "resource_group_name" { value = azurerm_resource_group.example.name } ``` ### **Running Terraform Apply** After running `terraform apply`, Terraform displays the outputs: ``` Apply complete! Resources: 1 added. Outputs: resource_group_name = "myResourceGroup" ``` Now, you can **reference this output** in other scripts or Terraform configurations! --- ## **Chaining Outputs & Variables** You can **use outputs as inputs** in another Terraform module. Let’s say you have **two Terraform configurations**, one that creates a **resource group** and another that provisions a **VM**. 1\. **First, define the output in `resource_group/outputs.tf`:** ``` output "resource_group_name" { value = azurerm_resource_group.example.name } ``` 2\. **Then, use it in another Terraform module (`virtual_machine/main.tf`):** ``` variable "resource_group_name" {} resource "azurerm_virtual_machine" "example" { name = "myVM" resource_group_name = var.resource_group_name } ``` 3\. **Pass the output as a variable:** ``` terraform apply -var="resource_group_name=$(terraform output -raw resource_group_name)" ``` **Result?** Fully **modular Terraform configurations** that pass values dynamically! --- ## **Best Practices for Using Variables & Outputs** - **Use `.tfvars` files** for managing multiple environments. - **Prefix variables with categories** (e.g., `network_subnet`, `app_instance_count`) to stay organized. - **Never hardcode secrets!** Use Terraform’s `sensitive = true` for secrets. - **Output only what you need**—avoid exposing sensitive data. --- ## **Wrapping Up** Variables and outputs are the **building blocks of scalable Terraform configurations**. They **reduce duplication, improve reusability, and make deployments more flexible**. ### **Quick Recap** - **Variables** help you **parameterize your infrastructure**. - **Outputs** let you **retrieve important resource details**. - **Lists and Maps** allow you to **store multiple values efficiently**. Now, go forth and **Terraform smarter, not harder**! --- ### **What’s Next?** In the next post, we’ll explore **Terraform modules**—how to organize your infrastructure into reusable components. Stay tuned! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform Workflow: Plan It, Build It, Rule the Cloud](https://www.woodruff.dev/terraform-workflow-plan-it-build-it-rule-the-cloud/) **Published:** February 19, 2025 **Author:** Chris Woodruff **Excerpt:** If you’ve ever tried assembling IKEA furniture without looking at the instructions (we’ve all been there), you know how chaotic it can get. Terraform is no different—if you don’t follow the right workflow, you’ll end up with a cloud mess instead of a well-structured infrastructure. Thankfully, Terraform has a straightforward workflow that helps you move from defining your infrastructure to deploying it seamlessly. In this post, we’ll break down the key steps of the Terraform workflow and how to use them like a pro! **Content:** If you’ve ever tried assembling IKEA furniture without looking at the instructions (we’ve all been there), you know how chaotic it can get. Terraform is no different—if you don’t follow the right workflow, you’ll end up with a cloud mess instead of a well-structured infrastructure. Thankfully, Terraform has a **straightforward workflow** that helps you move from **defining your infrastructure to deploying it seamlessly**. In this post, we’ll break down the key steps of the Terraform workflow and how to use them like a pro! --- ## **Terraform Workflow: The Big Picture** Terraform follows a predictable pattern for managing infrastructure: 1. **Write**: Define your infrastructure in Terraform configuration files. 2. **Initialize**: Set up Terraform by downloading the necessary providers. 3. **Plan**: Preview what Terraform will do before making changes. 4. **Apply**: Deploy your infrastructure. 5. **Destroy**: Clean up when needed. You can think of it like baking a cake: - You write the recipe (**Write**) - Gather ingredients and preheat the oven (**Initialize**) - Check if you have everything (**Plan**) - Bake the cake (**Apply**) - Eat the cake (and clean up after) (**Destroy**) Let’s break each step down. --- ## **Step 1: Write Your Terraform Code** Terraform uses **HCL (HashiCorp Configuration Language)** to define infrastructure. Your `.tf` files describe what you want to create. Here’s a simple example that creates a **resource group in Azure**: ``` provider "azurerm" { features {} } resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = "East US" } ``` **Tip:** Keep your Terraform code **organized** by separating providers, variables, and resources into different `.tf` files. --- ## **Step 2: Initialize Terraform** Before you can use Terraform, you need to **initialize** it. This step: - Downloads required provider plugins. - Sets up the backend for storing state. - Prepares Terraform for execution. Run: ``` terraform init ``` Expected output: ``` Initializing provider plugins... Terraform has been successfully initialized! ``` **Pro Tip:** Always run `terraform init` when starting a new project or changing provider versions. --- ## **Step 3: Plan Your Deployment** Before you apply any changes, you should **preview** them using `terraform plan`. Run: ``` terraform plan ``` Terraform will show you **what it’s about to do**—what resources will be created, modified, or destroyed. Example output: ``` + azurerm_resource_group.example will be created name: "myResourceGroup" location: "East US" ``` The + sign means Terraform will create the resource. The `~` sign means Terraform will update an existing resource. The `-` sign means Terraform will destroy a resource. **Why It’s Important:** Running `terraform plan` first prevents **accidental changes** to your infrastructure! --- ## **Step 4: Apply Your Changes** Once you’re happy with the plan, **apply the changes** to actually create the infrastructure. Run: ``` terraform apply ``` Terraform will ask for confirmation before proceeding: ``` Do you want to perform these actions? (yes/no) ``` Type **yes**, and Terraform will create your resources. Expected output: ``` Apply complete! Resources: 1 added, 0 changed, 0 destroyed. ``` Your cloud infrastructure is now live! --- ## **Step 5: Destroy Your Infrastructure (When Needed)** If you ever need to **tear everything down**, use: ``` terraform destroy ``` This will **delete all resources** created by Terraform. Useful for cleaning up after testing! Confirmation prompt: ``` Do you really want to destroy all resources? (yes/no) ``` Type **yes**, and watch Terraform clean up. --- ## **Bonus: Automating the Terraform Workflow** If you’re working in a **CI/CD pipeline**, you can automate Terraform’s workflow: 1. **Run `terraform fmt`** to format code. 2. **Use `terraform validate`** to check for syntax errors. 3. **Use `terraform apply -auto-approve`** for automated deployments. For **team collaboration**, store the **Terraform state remotely** in Azure Blob Storage or Terraform Cloud. --- ## **Wrapping Up** The Terraform workflow is **predictable and easy to follow** once you get the hang of it. Whether you’re deploying a simple resource group or an entire cloud infrastructure, following these steps ensures **smooth and error-free deployments**. **Quick Recap:** 1. **Write** → Define infrastructure in `.tf` files. 2. **Init** → Initialize the project. 3. **Plan** → Preview changes. 4. **Apply** → Deploy resources. 5. **Destroy** → Remove resources when needed. Now, go forth and **Terraform like a pro**! --- ### **What’s Next?** In the next post, we’ll dive into **Terraform variables and outputs**, so you can build even **more flexible and reusable** configurations. Stay tuned! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [The State of Terraform: Keeping Your Cloud Empire in Check](https://www.woodruff.dev/the-state-of-terraform-keeping-your-cloud-empire-in-check/) **Published:** February 18, 2025 **Author:** Chris Woodruff **Excerpt:** Managing infrastructure can feel like herding cats—if you don’t keep track of what’s going on, chaos ensues. That’s where Terraform state comes in. It’s like the master ledger for your cloud empire, tracking every resource Terraform manages so you don’t lose your mind. **Content:** Managing infrastructure can feel like herding cats—if you don’t keep track of what’s going on, chaos ensues. That’s where Terraform state comes in. It’s like the master ledger for your cloud empire, tracking every resource Terraform manages so you don’t lose your mind. In this post, we’ll break down what Terraform state is, why it’s important, and how to manage it like a pro. Grab your coffee, and let’s dive in! --- ### **What Is Terraform State?** Imagine Terraform as a contractor building a house. The **state file** is the blueprint that tracks every nail, beam, and coat of paint. It ensures Terraform knows what’s been done and what still needs doing. - It **tracks the current status** of your infrastructure. - It’s used by Terraform to determine what changes need to be applied. - It’s stored as a file named `terraform.tfstate` by default. Without state, Terraform would be guessing every time you run `terraform plan` or `terraform apply`—and nobody wants that kind of guesswork in their cloud! --- ### **Why Does State Matter?** Terraform state is crucial for keeping your infrastructure consistent and preventing accidental chaos. Here’s why it’s a big deal: 1. **Resource Tracking**: The state file knows what resources exist, their configurations, and how they relate to each other. 2. **Change Detection**: It lets Terraform compare your code to the actual infrastructure and figure out what needs updating, deleting, or creating. 3. **Team Collaboration**: With shared remote state, multiple team members can work on the same infrastructure without stepping on each other’s toes. --- ### **Where Is the State Stored?** By default, Terraform saves the state file locally on your machine. That’s fine for testing, but it’s not ideal for production setups. Why? - **Collaboration Nightmare**: Local state doesn’t work well when multiple people need access. - **Risk of Data Loss**: If you lose your laptop, your state file goes with it. The solution? **Remote state backends!** --- ### **Remote State with Azure Blob Storage** If you’re deploying to Azure, using Blob Storage for remote state is a no-brainer. It keeps your state file secure, accessible, and versioned. Here’s how to set it up. #### **Step 1: Create an Azure Storage Account** 1. In the Azure Portal, create a new storage account. 2. Add a Blob container named `tfstate`. #### **Step 2: Update Your Terraform Configuration** In your Terraform code, define the backend like this: ``` terraform { backend "azurerm" { resource_group_name = "myResourceGroup" storage_account_name = "mystorageaccount" container_name = "tfstate" key = "terraform.tfstate" } } ``` #### **Step 3: Initialize Terraform** Run: ``` terraform init ``` Terraform will migrate your state to the remote backend. Easy peasy! --- ### **Workspaces: Managing Multiple Environments** Do you have separate environments (e.g., dev, staging, prod)? Workspaces let you manage multiple states within the same configuration. #### **Create a New Workspace** ``` terraform workspace new dev ``` #### **Switch Between Workspaces** ``` terraform workspace select prod ``` Each workspace gets its own state file, so you can keep environments isolated. Think of it like having different save slots in your favorite video game. --- ### **Best Practices for Terraform State** 1. **Always Use Remote State for Teams** Whether it’s Azure Blob Storage, AWS S3, or Terraform Cloud, remote state is essential for collaboration. 2. **Secure Your State File** Your state file contains sensitive information like resource IDs and keys. Use encryption for remote state backends. 3. **Lock State Files** Prevent simultaneous updates by enabling state locking. Azure Blob Storage supports this out of the box. 4. **Back Up Your State** Even with remote state, create regular backups to avoid nasty surprises. --- ### **Common State Management Commands** Here are some handy Terraform commands for managing state: - **View State**:bashCopyEdit`terraform show `Displays the current state in a readable format. - **Manually Edit State**:bashCopyEdit`terraform state pull > state.json `Edit carefully, then push back:bashCopyEdit`terraform state push state.json` - **Remove a Resource**: If you need to remove a resource without deleting it:bashCopyEdit`terraform state rm ` --- ### **Wrapping Up** Terraform state might sound like a behind-the-scenes thing, but it’s the backbone of your cloud management. With proper state management—especially using remote backends—you’ll ensure your infrastructure is consistent, secure, and ready for collaboration. So, the next time you run `terraform plan`, thank the humble state file for making it all possible. --- **What’s Next?** In the next post, we’ll dive into **provisioning infrastructure** with Terraform, so you can spin up VMs, databases, and more with ease. Stay tuned, and happy Terraforming! **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Speaking Terraform: A Crash Course in HCL](https://www.woodruff.dev/speaking-terraform-a-crash-course-in-hcl/) **Published:** February 17, 2025 **Author:** Chris Woodruff **Excerpt:** Learning a new tool is like learning a new language—you’ve got to understand the grammar before you can start forming sentences. Terraform’s native tongue is HCL (HashiCorp Configuration Language), and trust me, it’s way easier than high school French (no weird verb conjugations here). In this post, we’ll break down the basics of HCL so you can write Terraform configurations that are clean, dynamic, and downright fun. **Content:** Learning a new tool is like learning a new language—you’ve got to understand the grammar before you can start forming sentences. Terraform’s native tongue is **HCL (HashiCorp Configuration Language)**, and trust me, it’s way easier than high school French (no weird verb conjugations here). In this post, we’ll break down the basics of HCL so you can write Terraform configurations that are clean, dynamic, and downright fun. --- ### **What’s HCL, Anyway?** HCL is Terraform’s declarative language, which means you just describe *what* you want, and Terraform figures out *how* to make it happen. It’s like saying, “I want a pizza,” and someone else deals with the toppings, crust, and delivery. Here’s what makes HCL awesome: - **Human-Readable**: It’s like writing simple instructions in plain English. - **Reusable**: You can use variables and modules to avoid repetitive code. - **Modular**: HCL configurations are easy to organize into small, manageable pieces. --- ### **The Building Blocks of HCL** #### 1. **Providers** Providers are the plugins Terraform uses to interact with cloud platforms (like Azure, AWS, or GCP). Think of them as the bridge between your code and the cloud. Here’s how you declare an Azure provider: ``` provider "azurerm" { features {} } ``` #### 2. **Resources** Resources are the meat of your configuration—they define what you’re creating. For example, a resource group in Azure looks like this: ``` resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = "East US" } ``` - `azurerm_resource_group` is the type. - `"example"` is the name you assign for reference. - The block contains all the settings for that resource. #### 3. **Variables** Variables let you avoid hardcoding values, making your code more dynamic and reusable. Declare a variable in `variables.tf`: ``` variable "location" { description = "The Azure region for the resource group" default = "East US" } ``` Use the variable in your resource: ``` resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = var.location } ``` #### 4. **Outputs** Outputs let you extract useful information about your resources once Terraform runs. Example: ``` output "resource_group_name" { value = azurerm_resource_group.example.name } ``` When you apply your configuration, Terraform will display the resource group name. --- ### **Putting It All Together** Here’s a full example of a simple Terraform configuration: ``` provider "azurerm" { features {} } variable "location" { description = "The Azure region for the resource group" default = "East US" } resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = var.location } output "resource_group_name" { value = azurerm_resource_group.example.name } ``` Run these steps to test it: 1. **Initialize Terraform**:bashCopyEdit`terraform init` 2. **Preview the Plan**:bashCopyEdit`terraform plan` 3. **Apply the Configuration**:bashCopyEdit`terraform apply` Boom! You’ve got a resource group and some shiny Terraform output to admire. --- ### **Pro Tips for Writing HCL** 1. **Indent for Clarity**: Neatly formatted code is happy code. 2. **Comment Often**: Use `#` for comments to explain what your code does.hclCopyEdit`# This is my Azure resource group resource "azurerm_resource_group" "example" { name = "myResourceGroup" location = var.location }` 3. **Use the Terraform Registry**: The Terraform Registry has tons of examples and pre-built modules to speed things up. **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Terraform 101: Your First Steps into Infrastructure as Code](https://www.woodruff.dev/terraform-101-your-first-steps-into-infrastructure-as-code/) **Published:** February 16, 2025 **Author:** Chris Woodruff **Excerpt:** So, you’ve heard about Terraform, but you’re wondering: "What the heck is Terraform, and why should I use it?" **Content:** So, you’ve heard about **Terraform**, but you’re wondering: > **“What the heck is Terraform, and why should I use it?”** Terraform is the **magic wand of infrastructure automation**—it lets you: - **Define cloud infrastructure as code** (instead of clicking around in AWS, Azure, or GCP). - **Automate deployments** (no more manually setting up VMs and databases). - **Make infrastructure changes safely** (version control FTW!). In this guide, we’ll take you from **Terraform newbie to deploying your first cloud resource**—step by step, no stress. Let’s get started! --- ## **1. What is Terraform? (And Why Should You Care?)** Terraform is an **Infrastructure as Code (IaC) tool** that lets you **define cloud resources using simple configuration files**. Instead of manually creating infrastructure through cloud dashboards, you **write code, apply it, and let Terraform do the work**. ### **How Terraform Works:** **1. You write a config file** (e.g., “I want an AWS EC2 instance”). **2. Terraform plans the changes** and shows what will happen. **3. Terraform applies the changes** and deploys your infrastructure. ### **Why Use Terraform?** - **No more manual clicks** – Automate infrastructure across AWS, Azure, GCP. - **Consistency** – No more “it works on my cloud” problems. - **Easier scaling** – Define infrastructure once and reuse it. - **Built-in state management** – Tracks what exists so you don’t have to. **In short: Terraform makes cloud management easy, scalable, and repeatable.** --- ## **2. Installing Terraform (The Easy Way)** Terraform runs **locally** on your machine, and installing it is **super simple**. ### **Step 1: Install Terraform** For **Mac/Linux**, run: ``` brew install terraform ``` For **Windows**, use Chocolatey: ``` choco install terraform ``` Or download it from terraform.io/downloads. ### **Step 2: Verify the Installation** ``` terraform -version ``` If you see a version number, **you’re good to go!** --- ## **3. Writing Your First Terraform Configuration** Let’s create a **Terraform configuration file** to deploy an **AWS EC2 instance**. ### **Step 1: Create a Terraform File (`main.tf`)** ``` provider "aws" { region = "us-east-1" } resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" } ``` **What’s Happening?** - **`provider "aws"`** – Tells Terraform we’re using AWS. - **`resource "aws_instance" "web"`** – Defines an EC2 instance. - **`ami` and `instance_type`** – Configures the instance type. --- ## **4. Initializing and Applying Terraform** Now that we have our Terraform config, let’s **deploy it!** ### **Step 1: Initialize Terraform** ``` terraform init ``` This downloads required plugins (AWS, Azure, GCP, etc.). ### **Step 2: Preview Changes with `terraform plan`** ``` terraform plan ``` Terraform will show **what it’s about to do** before applying changes. ### **Step 3: Apply the Changes** ``` terraform apply ``` Type **“yes”** when prompted. Terraform will now **create your EC2 instance!** --- ## **5. Managing Infrastructure with Terraform** ### **Making Changes** Want to **change the instance type**? Just **update `main.tf`**: ``` instance_type = "t3.micro" ``` Then, run: ``` terraform apply ``` **Terraform will detect the change and update the instance.** ### **Destroying Infrastructure** Need to **clean up** resources? Run: ``` terraform destroy ``` **Terraform removes everything safely.** **No more manually clicking “Delete” in cloud dashboards!** --- ## **6. Terraform State: How Terraform Remembers Everything** Terraform **keeps track of what it deployed** using a **state file (`terraform.tfstate`)**. ### **Why Terraform State Matters** - **Keeps track of existing resources** so Terraform doesn’t recreate them. - **Allows collaborative work** across teams. - **Supports remote state storage** (e.g., in S3 for team-based projects). **Think of it as Terraform’s memory—it knows what’s already running.** --- ## **7. Terraform Modules: Reusing Infrastructure Code** Instead of writing **the same Terraform code repeatedly**, use **modules** to **organize and reuse configurations**. ### **Example: A Simple Terraform Module** ``` module "ec2_instance" { source = "./modules/ec2" instance_type = "t2.micro" } ``` **Now, you can reuse this module across multiple environments!** --- ## **8. Terraform Workflow Cheat Sheet** 📖 **Command****What It Does**`terraform init`Initializes Terraform in a directory.`terraform plan`Shows what Terraform will change before applying.`terraform apply`Deploys or updates infrastructure.`terraform destroy`Removes infrastructure managed by Terraform.`terraform state list`Lists all Terraform-managed resources.**Master these commands, and you’ll be Terraforming like a champ!** --- ## **10. Common Beginner Mistakes & How to Avoid Them** **Mistake****Fix**Forgetting to run `terraform init`Always initialize Terraform before applying changes.Not checking `terraform plan`Always preview changes before applying.Deleting cloud resources manuallyUse `terraform destroy` instead.Hardcoding credentialsUse environment variables or Terraform Cloud.**Pro Tip:** If Terraform tries to delete something unexpected, **STOP and check your state file!** --- ## **Wrapping Up** Congrats! You just learned the **basics of Terraform** and even deployed your first cloud resource. **Quick Recap:** - **Terraform automates cloud infrastructure with code.** - **Use `terraform init`, `terraform plan`, and `terraform apply` to deploy resources.** - **Manage Terraform state to track cloud resources.** - **Use modules to reuse and organize Terraform code.** **Categories:** Terraform **Tags:** devops, IaC, infrastructure, terraform --- ### [Temporal Tables in EF Core: Bringing Time Travel to Your Data](https://www.woodruff.dev/temporal-tables-in-ef-core-bringing-time-travel-to-your-data/) **Published:** February 15, 2025 **Author:** Chris Woodruff **Excerpt:** What if you could go back in time and see exactly what your database looked like yesterday, last week, or even last year? Sounds like something out of a sci-fi movie, right? Well, Temporal Tables in SQL Server let you do exactly that! **Content:** What if you could go **back in time** and see exactly what your database looked like **yesterday, last week, or even last year**? Sounds like something out of a sci-fi movie, right? Well, **Temporal Tables** in SQL Server let you **do exactly that**! They enable **automatic historical tracking** of data changes, so you can: **Recover lost data** – Bring back deleted or modified records. **Audit changes** – Know who changed what and when. **Analyze trends over time** – Understand how your data evolved. And the best part? **Entity Framework Core (EF Core) fully supports Temporal Tables**, making it easy to integrate **time-traveling queries** into your application! Let’s explore how **Temporal Tables work, how to set them up in EF Core, and how you can query past data with ease.** --- ## **What Are Temporal Tables?** A **Temporal Table** is a **special table in SQL Server** that automatically keeps track of **all changes** to your data. Instead of just storing the **current state** of your records, it also maintains a **history of changes**, allowing you to query previous versions of your data. When a record is **inserted, updated, or deleted**, SQL Server: - **Keeps the current data in the main table.** - **Moves older versions into a history table.** This means you can **query data from any point in time**, making **auditing, debugging, and recovery much easier**. --- ## **Step 1: Creating a Temporal Table in EF Core** First, let’s create an entity that **uses a Temporal Table** in EF Core. ### **1. Define the Entity Model** ``` public class Employee { public int Id { get; set; } public string Name { get; set; } public string Position { get; set; } public decimal Salary { get; set; } } ``` ### **2. Configure Temporal Table in `OnModelCreating`** Now, we tell EF Core **to enable temporal table support**: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .ToTable("Employees", tb => tb.IsTemporal()); } ``` **That’s it!** EF Core will now create a **temporal table** for `Employees` when you run migrations. --- ## **Step 2: Applying Temporal Tables to the Database** After adding the configuration, run: ``` dotnet ef migrations add AddTemporalTables dotnet ef database update ``` **EF Core will generate the following SQL:** ``` CREATE TABLE Employees ( Id INT PRIMARY KEY IDENTITY, Name NVARCHAR(100), Position NVARCHAR(100), Salary DECIMAL(18,2), SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) ) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeesHistory)); ``` **SQL Server automatically:** - Adds **`SysStartTime` and `SysEndTime`** columns to track changes. - Creates an **EmployeesHistory** table to store old versions of records. - Enables **SYSTEM\_VERSIONING** to **automatically track changes**. No need to manually update history records—**SQL Server handles it for you!** --- ## **Step 3: Querying Past Data in EF Core** Now for the fun part—**retrieving historical data!** ### **1. Getting the Current Data** ``` var employees = await context.Employees.ToListAsync(); ``` This **only returns the latest records** (normal behavior). --- ### **2. Querying Historical Data** Want to see **all past versions** of a record? Use `.TemporalAll()`: ``` var allVersions = await context.Employees.TemporalAll() .Where(e => e.Id == 1) .ToListAsync(); ``` **This pulls data from both the main table AND the history table!** --- ### **3. Querying Data from a Specific Time** Want to see what your database looked like **last week**? Use `.TemporalAsOf(DateTime)`: ``` var lastWeekData = await context.Employees .TemporalAsOf(DateTime.UtcNow.AddDays(-7)) .ToListAsync(); ``` **Time-traveling to last week’s database state!** --- ### **4. Seeing Changes Over a Time Range** Need to **track how an employee’s salary changed over time**? Use `.TemporalBetween(start, end)`: ``` var salaryChanges = await context.Employees .TemporalBetween(DateTime.UtcNow.AddMonths(-3), DateTime.UtcNow) .Where(e => e.Name == "Alice") .ToListAsync(); ``` **Perfect for analyzing trends, auditing, and debugging!** --- ## **When Should You Use Temporal Tables?** **Auditing & Compliance** – Track **who changed what** and **when**. **Data Recovery** – Accidentally deleted data? **Retrieve it from history!** **Debugging & Troubleshooting** – Investigate **unexpected changes** in your data. **Business Intelligence & Trend Analysis** – See **how values changed over time**. --- ## **Things to Keep in Mind** **Storage Impact** – Since history tables store every change, large tables **can grow quickly**. **Read-Only History** – You **cannot modify or delete** history records directly. **Only in SQL Server** – Temporal Tables are **not available** in PostgreSQL, MySQL, or SQLite. --- ## **Wrap-Up: Let Your Database Remember Everything!** Temporal Tables in EF Core **bring time-traveling capabilities to your data**, letting you **see the past, analyze trends, and recover lost records effortlessly**. **With Temporal Tables, you can:** - Query past versions of records. - Retrieve deleted or modified data. - Easily audit database changes. Next time you need **historical tracking** in your EF Core app, **consider using Temporal Tables**! **Are you using Temporal Tables in your projects? Let’s discuss in the comments!** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, programming --- ### [JSON Columns in SQL Server: Storing & Querying JSON with EF Core](https://www.woodruff.dev/json-columns-in-sql-server-storing-querying-json-with-ef-core/) **Published:** February 14, 2025 **Author:** Chris Woodruff **Excerpt:** Ever wished you could store semi-structured data in your database without dealing with complex table relationships? Good news! SQL Server has native JSON support, and EF Core makes working with JSON columns easier than ever. Whether you’re handling dynamic configurations, logging data, or flexible user preferences, JSON columns let you mix structured and unstructured data in SQL Server—without creating dozens of extra tables. **Content:** Ever wished you could store **semi-structured data** in your database without dealing with complex table relationships? **Good news!** SQL Server has **native JSON support**, and **Entity Framework Core makes working with JSON columns easier than ever**. Whether you’re handling **dynamic configurations, logging data, or flexible user preferences**, **JSON columns let you mix structured and unstructured data in SQL Server**—without creating dozens of extra tables. Let’s explore how to **store, query, and manipulate JSON data** in SQL Server using **Entity Framework Core (EF Core)**! --- ## **Why Store JSON in SQL Server?** JSON columns are perfect when you need **flexibility** in your database structure without sacrificing **query performance**. **Avoid Table Explosion** – No need for hundreds of tiny tables just to store key-value data. **Dynamic Data Storage** – Perfect for **storing user preferences, metadata, and logs**. **Efficient Queries** – SQL Server lets you **query JSON data using standard SQL functions**. **Better Than TEXT Columns** – Unlike plain text, JSON fields **can be indexed and queried efficiently**. --- ## **Step 1: Creating a Table with a JSON Column** Let’s say we’re building an **e-commerce platform and** want to store **product attributes** (like size, color, and material) in a JSON column instead of separate columns. ### **1. Define the Entity Model** ``` public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string AttributesJson { get; set; } // Stores JSON data } ``` Instead of **creating multiple columns** for attributes, we **store them as a JSON string**. --- ## **Step 2: Configuring JSON Columns in EF Core** Now, configure EF Core **to use the JSON column**: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(p => p.AttributesJson) .HasColumnType("nvarchar(max)"); // Store as JSON } ``` **This ensures the JSON column is properly stored in SQL Server**. --- ## **Step 3: Inserting JSON Data into SQL Server** Let’s **save a product** with JSON attributes: ``` var product = new Product { Name = "Running Shoes", Price = 99.99m, AttributesJson = JsonSerializer.Serialize(new { Size = "10", Color = "Red", Material = "Mesh" }) }; context.Products.Add(product); await context.SaveChangesAsync(); ``` **Why Serialize to JSON?** - The **AttributesJson** column is a **string**, so we **serialize** the object before storing it. - This makes it easy to **store dynamic product attributes** without modifying the database schema. --- ## **Step 4: Querying JSON Data in SQL Server** Now that we’ve stored JSON, **how do we query it?** ### **1. Querying JSON Using EF Core** ``` var shoes = await context.Products .Where(p => p.AttributesJson.Contains("\"Color\":\"Red\"")) .ToListAsync(); ``` **This finds all products where `Color = Red` inside the JSON column!** --- ### **2. Querying JSON Using SQL Server JSON Functions** SQL Server has **native JSON functions**, so you can query JSON fields like structured data! ``` SELECT Name, Price, AttributesJson FROM Products WHERE JSON_VALUE(AttributesJson, '$.Color') = 'Red'; ``` **SQL Server JSON Functions to Know:** - **`JSON_VALUE(column, '$.key')`** – Extracts a single value. - **`JSON_QUERY(column, '$.key')`** – Extracts an object/array. - **`OPENJSON(column)`** – Converts JSON into **rows and columns**. --- ## **Step 5: Querying JSON Data in EF Core with SQL Functions** We can **combine EF Core with SQL Server’s JSON functions** for more efficient queries: ``` var products = await context.Products .Where(p => EF.Functions.JsonValue(p.AttributesJson, "$.Color") == "Red") .ToListAsync(); ``` **Why is this better?** - **Runs natively in SQL Server** (instead of filtering in memory). - **More efficient for large datasets**. --- ## **Step 6: Updating JSON Data in SQL Server** Need to **update a specific field inside JSON** without rewriting the whole string? SQL Server has your back! ``` UPDATE Products SET AttributesJson = JSON_MODIFY(AttributesJson, '$.Size', '12') WHERE Id = 1; ``` **No need to retrieve the entire JSON string first!** --- ## **Step 7: Indexing JSON Data for Faster Queries** Since JSON is stored as text, **how do we make queries faster?** ### **Create a Computed Column for JSON Values** ``` ALTER TABLE Products ADD Color AS JSON_VALUE(AttributesJson, '$.Color') PERSISTED; ``` Then, **add an index**: ``` CREATE INDEX IX_Products_Color ON Products(Color); ``` **Now, querying products by color is lightning fast!** --- ## **When Should You Use JSON Columns?** - **For Storing Dynamic Data** – Great for user settings, metadata, and logs. - **When Schema Changes Often** – Avoids frequent migrations for new fields. - **When Data is Read-Heavy, Write-Light** – JSON works best for **frequent reads and occasional writes**. --- ## **When NOT to Use JSON Columns** **Don’t use JSON for relational data!** - If you need **JOINs, foreign keys, or referential integrity**, stick to **normal relational tables**. - JSON is great for **flexible data** but **not a replacement for structured tables**. --- ## **Wrap-Up: JSON + SQL Server + EF Core = Flexible Data Storage** SQL Server’s **JSON column support** lets you **store, query, and manipulate semi-structured data** without breaking relational integrity. Combined with **EF Core**, it’s a powerful way to handle **dynamic, evolving data** without constantly altering your database schema. **Key Takeaways:** - **Store JSON in SQL Server** for flexible data storage. - **Query JSON efficiently** using SQL Server’s native JSON functions. - **Combine EF Core with SQL functions** for optimized queries. - **Use computed columns & indexes** for faster lookups. Next time you need to store **dynamic, schema-free data**, consider **JSON columns**! **How are you using JSON in your EF Core apps? Let’s chat in the comments!** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Keyless Entity Types in EF Core: Query Data Without Primary Keys](https://www.woodruff.dev/keyless-entity-types-in-ef-core-query-data-without-primary-keys/) **Published:** February 13, 2025 **Author:** Chris Woodruff **Excerpt:** Not everything in your database needs a primary key. Sometimes, you just want to query views, stored procedures, or raw SQL results without forcing a unique identifier on them. That’s where Keyless Entity Types in EF Core come in! If you’ve ever struggled with querying database views, reports, or read-only datasets, this feature is exactly what you need. Let’s dive into what Keyless Entity Types are, when to use them, and how to make them work in EF Core. **Content:** Not everything in your database needs a **primary key**. Sometimes, you just want to query **views, stored procedures, or raw SQL results** without forcing a unique identifier on them. That’s where **Keyless Entity Types** in Entity Framework Core come in! If you’ve ever struggled with querying **database views, reports, or read-only datasets**, this feature is **exactly what you need**. Let’s dive into what **Keyless Entity Types** are, when to use them, and how to make them work in EF Core. --- ## **What Are Keyless Entity Types?** In EF Core, a **Keyless Entity Type** is an entity that **does not require a primary key**. Unlike standard EF Core entities that map to tables with primary keys, keyless entities are ideal for **queries that don’t need identity tracking**—such as reports, read-only views, or raw SQL queries. ### **Key Features of Keyless Entity Types:** **No primary key required** – Useful for queries that don’t need a unique identifier. **Maps to database views, stored procedures, or raw SQL** – Ideal for reporting and read-only datasets. **Cannot be updated** – These entities are **read-only** in EF Core, preventing accidental modifications. --- ## **When Should You Use Keyless Entity Types?** **Database Views** – If you have a **SQL view** that aggregates data across multiple tables, EF Core can query it **without requiring a primary key**. **Stored Procedure Results** – If your app **executes a stored procedure** and expects complex results, Keyless Entity Types help map the output. **Raw SQL Queries** – When you need **custom SQL results** that don’t neatly fit into a single table structure. **Read-Only Reports** – If you’re generating reports that don’t modify data, Keyless Entity Types are **perfect for performance-friendly queries**. --- ## **How to Define a Keyless Entity Type in EF Core** Let’s say you have a **database view** that provides a summary of order details, but it doesn’t have a primary key. Here’s how you’d map it in EF Core. ### **1. Define the Keyless Entity** ``` public class OrderSummary { public int OrderId { get; set; } public string CustomerName { get; set; } public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } } ``` ### **2. Configure it in `DbContext`** ``` public class AppDbContext : DbContext { public DbSet OrderSummaries { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasNoKey() // This makes it a Keyless Entity Type .ToView("View_OrderSummary"); // Maps it to the SQL view } } ``` ### **3. Querying the Keyless Entity** Since **Keyless Entity Types are read-only**, you can only **query them**, not insert, update, or delete. ``` var summaries = await context.OrderSummaries.ToListAsync(); foreach (var summary in summaries) { Console.WriteLine($"Order {summary.OrderId} - {summary.CustomerName} - ${summary.TotalAmount}"); } ``` --- ## **Using Keyless Entities with Raw SQL Queries** Sometimes, you might need **custom SQL queries** instead of mapping to a database view. EF Core allows keyless entities to work with **raw SQL queries** using `FromSqlRaw()`. ### **1. Define the Keyless Entity** ``` public class ProductSales { public string ProductName { get; set; } public int UnitsSold { get; set; } } ``` ### **2. Configure It in `DbContext`** ``` public class AppDbContext : DbContext { public DbSet ProductSalesReports { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasNoKey(); } } ``` ### **3. Execute a Raw SQL Query** ``` var salesReport = await context.ProductSalesReports .FromSqlRaw("SELECT ProductName, SUM(Quantity) AS UnitsSold FROM Sales GROUP BY ProductName") .ToListAsync(); foreach (var sales in salesReport) { Console.WriteLine($"{sales.ProductName}: {sales.UnitsSold} units sold"); } ``` **This lets you run custom SQL queries directly in EF Core while maintaining type safety!** --- ## **Things to Keep in Mind** **Keyless Entity Types are Read-Only** – You **cannot** use them with `Add()`, `Update()`, or `Remove()`. **No Change Tracking** – EF Core **doesn’t track keyless entities**, so you can’t modify them in-memory and expect EF Core to persist changes. **Must Be Configured in `OnModelCreating()`** – Unlike regular entities, keyless types **must** be explicitly mapped in `OnModelCreating()`. --- ## **Wrap-Up: When You Need Data Without the Keys** Keyless Entity Types are a **powerful feature** in EF Core that lets you **query data from views, stored procedures, and raw SQL queries** without worrying about primary keys. They’re **perfect for reporting, analytics, and read-only scenarios** where identity tracking isn’t needed. So, next time you need to **query data that doesn’t fit neatly into an EF Core entity**, remember: **You don’t always need a key!** Are you using **Keyless Entity Types** in your EF Core projects? Let’s discuss in the comments! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Grouping Smarter: LINQ GroupBy Enhancements in EF Core](https://www.woodruff.dev/grouping-smarter-linq-groupby-enhancements-in-ef-core/) **Published:** February 12, 2025 **Author:** Chris Woodruff **Excerpt:** Grouping data in Entity Framework Core (EF Core) used to feel a little… clunky. Sometimes, LINQ’s GroupBy() worked beautifully in-memory but got lost in translation when executing SQL queries. You’d write a simple GroupBy(), and EF Core would pull all the data into memory before doing the grouping—not good! But things are getting smarter and more efficient in recent versions of EF Core! With LINQ GroupBy enhancements, EF Core now translates more grouping operations into optimized SQL queries, saving memory and improving performance. **Content:** Grouping data in **Entity Framework Core (EF Core)** used to feel a little… clunky. Sometimes, LINQ’s `GroupBy()` worked beautifully in-memory but got lost in translation when executing SQL queries. You’d write a simple `GroupBy()`, and EF Core would **pull all the data into memory** before doing the grouping—*not good!* But things are getting **smarter and more efficient** in recent versions of EF Core! With **LINQ GroupBy enhancements**, EF Core now translates more grouping operations into **optimized SQL queries**, saving memory and improving performance. Let’s explore what’s changed, how `GroupBy()` works in EF Core now, and how you can **write better, faster queries** for real-world scenarios! --- ## **What’s the Problem with LINQ GroupBy in EF Core?** Before the enhancements, using `GroupBy()` in EF Core often led to **unexpected behavior**. Unlike SQL’s `GROUP BY`, EF Core’s LINQ implementation sometimes **performed the grouping in-memory**, leading to: **Performance Issues** – If you’re grouping **thousands of rows**, pulling them all into memory is a nightmare. **Inefficient Queries** – Instead of optimizing grouping at the database level, EF Core used to fetch **all the records first**. **Hard-to-Debug Behavior** – Depending on how you structured your query, you might get **unexpected SQL translation issues**. Thankfully, **EF Core now translates more `GroupBy()` queries into SQL** instead of processing them in memory. --- ## **How LINQ GroupBy Works in EF Core Now** Let’s say we have a simple **Sales database**, where each order is tracked with: ``` public class Order { public int Id { get; set; } public string Product { get; set; } public decimal Price { get; set; } public DateTime OrderDate { get; set; } } ``` ### **1. Old Problem: GroupBy in Memory (Bad Performance)** Before the enhancements, this query **would not translate into SQL properly**: ``` var salesReport = context.Orders .GroupBy(o => o.Product) // Will bring back all rows to be grouped in memory .Select(g => new { Product = g.Key, TotalSales = g.Sum(o => o.Price) }) .ToList(); ``` **What EF Core Used to Do:** - Fetch **all rows** from the database. - Do the **grouping in application memory**. - Waste **RAM and CPU cycles**. --- ### **2. New Behavior: GroupBy Translates to SQL (Better Performance)** Now, in **EF Core 6+**, this query is **properly translated into SQL**: ``` var salesReport = await context.Orders .GroupBy(o => o.Product) .Select(g => new { Product = g.Key, TotalSales = g.Sum(o => o.Price) }) .ToListAsync(); ``` **SQL Translation (EF Core 6+):** ``` SELECT Product, SUM(Price) AS TotalSales FROM Orders GROUP BY Product; ``` **Why is this awesome?** - **Grouping happens in SQL, not in memory.** - **Efficient database execution, reducing data transfer.** - **Better performance for large datasets.** --- ## **Real-World Examples Using GroupBy Enhancements** ### 3. **Grouping Orders by Month** Let’s say we want to generate **monthly sales reports**. Instead of fetching every order and grouping it in-memory, we can do this: ``` var monthlySales = await context.Orders .GroupBy(o => new { o.OrderDate.Year, o.OrderDate.Month }) .Select(g => new { Year = g.Key.Year, Month = g.Key.Month, TotalRevenue = g.Sum(o => o.Price) }) .ToListAsync(); ``` **SQL Translation:** ``` SELECT YEAR(OrderDate) AS Year, MONTH(OrderDate) AS Month, SUM(Price) AS TotalRevenue FROM Orders GROUP BY YEAR(OrderDate), MONTH(OrderDate); ``` **Now, grouping happens directly in SQL, making this much faster!** --- ### **4. Counting Orders per Product** Need to know how many orders were placed for each product? ``` var productCounts = await context.Orders .GroupBy(o => o.Product) .Select(g => new { Product = g.Key, OrderCount = g.Count() }) .ToListAsync(); ``` **SQL Translation:** ``` SELECT Product, COUNT(*) AS OrderCount FROM Orders GROUP BY Product; ``` No more unnecessary **in-memory calculations**—everything is done efficiently at the database level! --- ### **5. Finding the Most Expensive Order per Product** Want to find the **highest-priced order** for each product? Easy! ``` var mostExpensiveOrders = await context.Orders .GroupBy(o => o.Product) .Select(g => new { Product = g.Key, MaxPrice = g.Max(o => o.Price) }) .ToListAsync(); ``` **SQL Translation:** ``` SELECT Product, MAX(Price) AS MaxPrice FROM Orders GROUP BY Product; ``` **Fast, efficient, and no unnecessary in-memory processing.** --- ## **When Does EF Core Still Struggle with GroupBy?** While **EF Core now translates more `GroupBy()` queries into SQL**, there are still **some scenarios where in-memory execution might happen**: **Grouping with Complex Object Projections** – If you’re selecting **entire entity objects**, EF Core might **switch to in-memory processing**. **Grouping with Navigation Properties** – If you’re trying to group by a related entity (`o.Customer.Name` instead of `o.CustomerId`), EF Core may struggle. **Mixing Client and Server Operations** – If a part of your LINQ query **cannot be translated into SQL**, EF Core **might switch the whole query to in-memory execution**. --- ## **Final Thoughts: Grouping in EF Core is Now Smarter!** Grouping data **used to be a headache in EF Core**, but thanks to **new enhancements**, more `GroupBy()` queries now **run at the database level** instead of clogging up memory. **With these improvements, you can now:** - Write **faster and more efficient queries**. - Reduce **application memory usage**. - Ensure **grouping happens where it should—in SQL, not in memory**. So next time you need to group data in EF Core, **trust the database to do the heavy lifting**! **Have you tried the new GroupBy enhancements in EF Core? Let’s chat in the comments!** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Transactional Savepoints in EF Core: Rollback Just What You Need!](https://www.woodruff.dev/transactional-savepoints-in-ef-core-rollback-just-what-you-need/) **Published:** February 11, 2025 **Author:** Chris Woodruff **Excerpt:** We’ve all been there—you’re halfway through a multi-step transaction, and boom! 💥 Something fails. You don’t want to roll back everything, just the part that went wrong. That’s where Transactional Savepoints come in! Savepoints let you partially roll back transactions, keeping the good stuff while undoing just the problematic parts. If you've ever wished for a "Ctrl + Z" in database operations, this is it. **Content:** We’ve all been there—you’re halfway through a **multi-step transaction**, and **boom!** 💥 Something fails. You don’t want to roll back **everything**, just the part that went wrong. That’s where **Transactional Savepoints** come in! Savepoints let you **partially roll back** transactions, keeping the good stuff while undoing just the problematic parts. If you’ve ever wished for a *“Ctrl + Z”* in database operations, this is it. Let’s dive into what **savepoints** are, why they’re useful, and how to use them in **Entity Framework Core (EF Core)**! --- ## **Why Use Transactional Savepoints?** By default, transactions in EF Core follow the **“all or nothing”** rule—either everything commits successfully, or the entire transaction gets rolled back. But sometimes, **you don’t want to lose everything** just because of a small issue. **With savepoints, you can:** - **Rollback specific parts** of a transaction instead of the whole thing. - **Handle errors more gracefully** instead of restarting everything. - **Improve performance** by avoiding full rollbacks and reprocessing. - **Keep long-running transactions stable** by fixing issues in steps. Imagine you’re processing **a batch of payments**: - 9 payments succeed - 1 fails - **With savepoints, you can roll back just the failed one and keep the rest!** --- ## **Step 1: Using Savepoints in EF Core Transactions** Let’s say we have a **banking app** where users can transfer money between accounts. If one transfer fails, we don’t want to cancel all the transactions—just the one that failed. ### **1. Start a Transaction** ``` using var transaction = await context.Database.BeginTransactionAsync(); try { await TransferMoney(1, 2, 500); // Transfer $500 from Account 1 to 2 await context.Database.ExecuteSqlRawAsync("SAVEPOINT BeforeSecondTransfer"); await TransferMoney(3, 4, 1000); // Transfer $1000 from Account 3 to 4 await context.Database.ExecuteSqlRawAsync("SAVEPOINT BeforeThirdTransfer"); await TransferMoney(5, 6, 2000); // Oops! This one might fail await transaction.CommitAsync(); // If everything is good, commit! } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); await context.Database.ExecuteSqlRawAsync("ROLLBACK TO SAVEPOINT BeforeThirdTransfer"); // Roll back just the last one } ``` **What’s happening here?** 1. **Start a transaction** 2. **Make some transfers** 3. **Create savepoints** before risky operations **(like large transfers)** 4. **Rollback only the problematic step** instead of **losing everything** --- ## **Step 2: Handling Savepoints in EF Core Using Transaction APIs** If you prefer **EF Core’s built-in transaction API**, you can do this: ``` using var transaction = await context.Database.BeginTransactionAsync(); try { await TransferMoney(1, 2, 500); await transaction.CreateSavepointAsync("BeforeSecondTransfer"); await TransferMoney(3, 4, 1000); await transaction.CreateSavepointAsync("BeforeThirdTransfer"); await TransferMoney(5, 6, 2000); // This might fail await transaction.CommitAsync(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); await transaction.RollbackToSavepointAsync("BeforeThirdTransfer"); // Undo only the last step await transaction.CommitAsync(); // Keep the rest! } ``` **Now, you can rollback part of a transaction** without discarding everything! --- ## **When Should You Use Savepoints?** ### **1. Batch Processing (Payments, Orders, Inventory Updates)** If you’re processing **multiple orders/payments** in a single transaction, use savepoints to **rollback only failed ones** while keeping the rest. ### **2. Long-Running Transactions** Large transactions risk **locking database resources for too long**. Savepoints help **recover faster** without restarting the entire process. ### **3. Handling Conditional Logic in Transactions** If certain operations depend on previous ones, use **savepoints to undo bad steps** without breaking the rest. ### **4. Preventing Partial Data Corruption** If one step fails, but the rest are fine, **rolling back everything might be unnecessary**. Savepoints let you **recover selectively**. --- ## **Savepoints vs. Full Rollback: When to Use Each** ScenarioSavepointsFull RollbackA single step fails in a multi-step transaction**Yes**NoA critical issue occurs, and everything must be undoneNo**Yes**Some operations should be committed while others should not**Yes**NoThe database state must return to the exact point before the transaction startedNo**Yes****Savepoints are great when you want a “soft rollback” instead of a complete undo.** --- ## **Common Issues & How to Fix Them** **Not All Databases Support Savepoints** - SQL Server, PostgreSQL, and MySQL support them. - SQLite does **not** support savepoints in the same way. **Savepoints Must Be Created Inside Transactions** - Always **begin a transaction first** before using savepoints. **Avoid Too Many Savepoints** - Each savepoint increases transaction overhead. - Use them only for **critical operations that might fail**. --- ## **Wrap-Up: Smarter Rollbacks with Savepoints** Transactional Savepoints in EF Core **let you undo only what you need**, keeping successful operations intact while recovering from failures. Instead of **rolling back everything**, savepoints let you: - **Fix only the failing parts** of a transaction. - **Keep successful operations intact.** - **Improve performance** by avoiding full rollbacks. Next time you’re dealing with **multi-step transactions**, consider **using savepoints to make them more reliable!** **Have you used savepoints in EF Core? Let’s chat in the comments!** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Tapping into Database Views with EF Core: Reverse Engineering Made Easy](https://www.woodruff.dev/tapping-into-database-views-with-ef-core-reverse-engineering-made-easy/) **Published:** February 10, 2025 **Author:** Chris Woodruff **Excerpt:** Not all database tables are created equal! Sometimes, you don’t need direct access to raw data—you need a refined, read-only version that makes querying easier. That’s where database views come in! **Content:** Not all database tables are created equal! Sometimes, you don’t need direct access to raw data—you need **a refined, read-only version** that makes querying easier. That’s where **database views** come in! Database views are **predefined queries** stored in your database that let you access data **without dealing with complex joins or filters every time**. If you’re working with **Entity Framework Core (EF Core)** and need to bring those views into your app, you might be wondering: - **How do I map a view in EF Core?** - **Can I generate code for it automatically?** - **Do I need a primary key for views?** Good news! **Reverse engineering database views in EF Core is simple**—let’s dive in and explore how to do it. --- ## **Why Use Database Views in EF Core?** Database views are **super useful** for a variety of reasons: **Predefined Queries** – Simplifies complex joins and aggregations. **Security & Data Access Control** – Restrict access to sensitive data while still allowing queries. **Performance Boosts** – Reduce redundant computations by letting the database handle data transformations. **Read-Only Access** – Perfect for reporting and analytics dashboards. --- ## **Step 1: Set Up a Database View** Let’s assume we have a simple **e-commerce database** with `Orders`, `Customers`, and `Products`. Instead of joining these tables every time we want to see order details, we can create a **database view**: ``` CREATE VIEW View_OrderSummary AS SELECT o.Id AS OrderId, c.Name AS CustomerName, p.Name AS ProductName, o.OrderDate, o.TotalAmount FROM Orders o JOIN Customers c ON o.CustomerId = c.Id JOIN Products p ON o.ProductId = p.Id; ``` This view **pre-joins** the data for us so we don’t have to write this query repeatedly. --- ## **Step 2: Reverse Engineer the View in EF Core** We need to bring this **view** into our **EF Core model** so we can query it easily. Instead of manually creating a model, **let’s use EF Core’s `Scaffold-DbContext` tool** to reverse engineer the view. ### **Run the EF Core Reverse Engineering Command** Open your terminal and run: ``` dotnet ef dbcontext scaffold "Your_Connection_String" Microsoft.EntityFrameworkCore.SqlServer -o Models -t View_OrderSummary ``` **What This Does:** - Connects to the database (`Your_Connection_String`) - Uses SQL Server provider (`Microsoft.EntityFrameworkCore.SqlServer`) - Scaffolds models into the `Models` folder - Includes the `View_OrderSummary` view --- ## **Step 3: Modify the Generated Model** After scaffolding, EF Core generates a **class for the view** inside your `Models` folder: ``` public class ViewOrderSummary { public int OrderId { get; set; } public string CustomerName { get; set; } public string ProductName { get; set; } public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } } ``` However, **EF Core assumes everything is a table**—so we need to tell it that this is a **database view**. ### **Modify `DbContext` to Configure the View** Inside your `AppDbContext.cs`, add this inside `OnModelCreating`: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasNoKey() // Views don't have primary keys .ToView("View_OrderSummary"); // Map it to the actual view } ``` **Why Use `HasNoKey()`?** - Unlike tables, views **don’t have a primary key**. - EF Core doesn’t track changes in views (since they’re read-only). --- ## **Step 4: Query the View in EF Core** Now, we can query the **view just like any other DbSet**: ``` var orderSummaries = await context.ViewOrderSummaries.ToListAsync(); foreach (var summary in orderSummaries) { Console.WriteLine($"{summary.CustomerName} bought {summary.ProductName} for ${summary.TotalAmount}"); } ``` **That’s it!** We can now **fetch data from a database view** in EF Core **without dealing with complex joins every time!** --- ## **When Should You Use Database Views in EF Core?** - **For Reports & Dashboards** – Views make it easier to **precompute aggregations** and retrieve **read-only** reports. - **When Using Complex Joins Frequently** – If you find yourself writing the same joins repeatedly, use a view to simplify the query. - **For Security & Data Access Control** – Restrict access to **sensitive columns** by exposing a limited view instead of the full table. - **Performance Optimization** – Views allow the database **to pre-optimize queries**, reducing execution time. --- ## **Common Issues & How to Fix Them** **EF Core Requires a Key for Views** - Fix it with `.HasNoKey()` in `OnModelCreating()`. **Can’t Update Views** - Views are **read-only** in EF Core, so you **can’t use `.Add()`, `.Update()`, or `.Remove()`**. - If updates are needed, use **INSTEAD OF triggers** or a stored procedure. **Not Showing in Reverse Engineering?** - Some databases **hide views** from introspection. Try running: ``` dotnet ef dbcontext scaffold "Your_Connection_String" Microsoft.EntityFrameworkCore.SqlServer -o Models --force ``` Adding `--force` ensures **EF Core retrieves all objects** from the database. --- ## **Wrap-Up: Simplify Queries with EF Core and Database Views** Database views are an **awesome tool** to simplify queries, improve performance, and make your data access **more efficient**. With **EF Core’s reverse engineering**, you can **quickly generate models for views** and start querying **without writing complex joins every time**. **Key Takeaways:** - Use **database views** for reports, security, and performance optimization. - **Reverse engineer** views using `dotnet ef dbcontext scaffold`. - Tell EF Core that the entity **has no primary key** using `.HasNoKey()`. - Views are **read-only**—no inserts, updates, or deletes. Next time you find yourself **writing the same joins over and over**, consider **reverse engineering a database view instead!** **Are you using views in EF Core? Let’s chat in the comments!** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Mapping the World with EF Core: Working with Spatial Data](https://www.woodruff.dev/mapping-the-world-with-ef-core-working-with-spatial-data/) **Published:** February 9, 2025 **Author:** Chris Woodruff **Excerpt:** Have you ever needed to store coordinates, track locations, or perform distance calculations in your database? Whether you're building a ride-sharing app, a location-based service, or an interactive map, working with spatial data is essential. Luckily, EF Core supports spatial data types, allowing you to store, query, and manipulate geographic data seamlessly. No more treating latitude and longitude as simple numbers—let’s bring real GIS (Geographic Information System) power to your EF Core apps! **Content:** Have you ever needed to store **coordinates, track locations, or perform distance calculations** in your database? Whether you’re building a **ride-sharing app, a location-based service, or an interactive map**, working with **spatial data** is essential. Luckily, **EF Core supports spatial data types**, allowing you to **store, query, and manipulate geographic data seamlessly**. No more treating latitude and longitude as simple numbers—let’s bring real GIS (Geographic Information System) power to your EF Core apps! --- ## **What is Spatial Data?** Spatial data represents **geographic locations** and features on the Earth’s surface. Instead of dealing with raw latitude/longitude values, spatial data provides **rich functionality** for working with **points, lines, polygons, and even complex geometries**. Think of it like this: **Points** – Represent single locations (e.g., a store location). **Lines** – Define paths or routes (e.g., roads, trails). **Polygons** – Represent areas (e.g., city boundaries, country borders). Most modern databases, like **SQL Server, PostgreSQL, and MySQL**, provide **native spatial data support**, allowing you to run **geospatial queries** efficiently. --- ## **Setting Up Spatial Data in EF Core** Before we start playing with coordinates, we need to **set up EF Core to support spatial data**. ### **1. Install the Required Packages** If you’re using **SQL Server**, you need the **NetTopologySuite** package, which enables spatial data support. ``` dotnet add package Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite ``` For **PostgreSQL**, install: ``` dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite ``` ### **2. Configure DbContext to Use Spatial Support** Modify your `DbContext` configuration to enable **NetTopologySuite**: ``` public class AppDbContext : DbContext { public DbSet Locations { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Your_Connection_String", x => x.UseNetTopologySuite()); // Enable spatial support } } ``` This tells EF Core to **use spatial capabilities** when working with SQL Server (or PostgreSQL). --- ## **Defining Spatial Data in EF Core** Now that **EF Core is ready**, let’s create an entity with a **geographic location**. ### **3. Create an Entity with Spatial Data** ``` using NetTopologySuite.Geometries; public class Location { public int Id { get; set; } public string Name { get; set; } public Point Coordinates { get; set; } // Stores latitude/longitude } ``` **What’s happening here?** - We **import `NetTopologySuite.Geometries`** to use spatial types. - The **`Point` type** is used to store a **geographic location** (latitude & longitude). --- ## **Storing and Querying Spatial Data** Now that we’ve defined our `Location` entity, let’s **add some data**! ### **4. Inserting Spatial Data** ``` using NetTopologySuite.Geometries; var location = new Location { Name = "Central Park", Coordinates = new Point(-73.9654, 40.7829) { SRID = 4326 } // Longitude, Latitude }; context.Locations.Add(location); await context.SaveChangesAsync(); ``` **What is `SRID = 4326`?** - [`SRID` (Spatial Reference Identifier)](https://spatialreference.org/) **defines the coordinate system**. - `4326` is **[WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)**, the standard for latitude/longitude (used by GPS). --- ### **5. Querying Nearby Locations** Let’s say you want to **find locations within 5 kilometers of a user**: ``` using NetTopologySuite.Geometries; using NetTopologySuite.Geometries.Prepared; var userLocation = new Point(-73.9851, 40.7580) { SRID = 4326 }; // Times Square var nearbyLocations = await context.Locations .Where(l => l.Coordinates.IsWithinDistance(userLocation, 5000)) // 5km radius .ToListAsync(); foreach (var location in nearbyLocations) { Console.WriteLine($"Nearby: {location.Name}"); } ``` **How does this work?** - **`IsWithinDistance()`** checks if a location is **within 5000 meters (5km)** of the user. - This is **way more efficient** than manually filtering lat/lon values! --- ## **Working with Polygons: Defining Regions** Let’s say you need to **store city boundaries** and check whether a location is inside a region. ### **6. Create a Polygon Entity** ``` public class CityBoundary { public int Id { get; set; } public string CityName { get; set; } public Polygon Area { get; set; } // Stores city boundary } ``` ### **7. Query Locations Inside a City** ``` var newYorkBoundary = context.CityBoundaries .FirstOrDefault(c => c.CityName == "New York"); var locationsInNYC = await context.Locations .Where(l => newYorkBoundary.Area.Contains(l.Coordinates)) .ToListAsync(); foreach (var location in locationsInNYC) { Console.WriteLine($"{location.Name} is inside New York!"); } ``` **Why is this cool?** - **`Contains()`** lets you check if a location is inside a polygon (city, park, etc.). - This is **super useful for geofencing, city-based filtering, and spatial searches**. --- ## **When Should You Use Spatial Data in EF Core?** **Location-Based Apps** – Track users, restaurants, stores, and landmarks. **Routing & Navigation** – Calculate distances, find nearby places, optimize paths. **Geofencing** – Detect when users enter or leave an area (e.g., delivery zones). **Real Estate & Mapping** – Store city boundaries, zip codes, and regions. --- ## **Wrap-Up: Bringing GIS Power to EF Core** Spatial data in EF Core **isn’t just for maps—it’s for anything that involves locations, distances, and geographic relationships**. By using **NetTopologySuite** and EF Core’s spatial capabilities, you can: **Store real-world locations with proper geospatial types** **Run optimized queries for nearby locations and distances** **Use polygons for geofencing, city boundaries, and more** If you’re building anything with **location tracking, maps, or spatial analytics**, EF Core has the **built-in tools** to make it easy! **Are you using spatial data in your projects? Let’s talk in the comments**! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming, spatial --- ### [Global Query Filters: Setting the Rules Once, Querying Like a Pro](https://www.woodruff.dev/global-query-filters-setting-the-rules-once-querying-like-a-pro/) **Published:** February 2, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re running a café. Every customer who walks in gets a free cookie—no need to ask for it, no need for your staff to remember. It just happens, automatically. That’s the magic of Global Query Filters in EF Core. Once you set them up, every query automatically follows the rules, making your code simpler and your life easier. Global Query Filters are all about efficiency and consistency. Let’s dive into how they work, why they’re awesome, and how to sprinkle them into your EF Core projects like those cookies at your café. **Content:** Imagine you’re running a café. Every customer who walks in gets a free cookie—no need to ask for it, no need for your staff to remember. It just happens automatically. That’s the magic of **Global Query Filters** in EF Core. Once you set them up, every query automatically follows the rules, making your code simpler and your life easier. Global Query Filters are all about efficiency and consistency. Let’s dive into how they work, why they’re awesome, and how to sprinkle them into your EF Core projects like those cookies at your café. --- ## **What Are Global Query Filters?** Global Query Filters are rules that EF Core applies to every query involving a specific entity type. These rules filter out data you don’t want by default—like inactive users, deleted records, or inventory that’s out of stock. Here’s an example. Say you have an `Album` entity, and you only want to fetch albums that are in stock. Instead of adding a `.Where()` clause to every query, you define a global filter once and let EF Core handle it for you: ``` modelBuilder.Entity() .HasQueryFilter(a => !a.IsOutOfStock); ``` Now, every time you query `Album`, EF Core automatically applies this filter. No extra work, no forgotten conditions, no surprises. --- ## **Why Use Global Query Filters?** Here’s why Global Query Filters are a game-changer: 1. **Simplify Your Code** Forget repetitive `.Where()` clauses. With global filters, you set the rule once and let EF Core handle the rest. 2. **Ensure Consistency** Global filters ensure that all queries follow the same rules, reducing the risk of errors and missed conditions. 3. **Boost Performance** By filtering at the database level, you reduce the amount of data EF Core needs to process, saving time and resources. 4. **Make Soft Deletes a Breeze** Global filters are perfect for soft deletes, where you mark records as deleted without actually removing them from the database. --- ## **How to Add Global Query Filters** Adding a global query filter is as easy as pie (or cookies 🍪). Here’s how you can do it: ### Step 1: Define Your Entity Make sure your entity has the necessary property to filter on. For example: ``` public class Album { public int Id { get; set; } public string Title { get; set; } public bool IsOutOfStock { get; set; } } ``` ### Step 2: Configure the Filter In your `DbContext`, use `HasQueryFilter` to define the global rule: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasQueryFilter(a => !a.IsOutOfStock); } ``` That’s it! Now every query involving `Album` will automatically exclude out-of-stock records. --- ## **When to Use Global Query Filters** Global Query Filters are perfect for: **Soft Deletes** Automatically exclude records marked as deleted: ``` modelBuilder.Entity() .HasQueryFilter(u => !u.IsDeleted); ``` **Multi-Tenant Applications** Filter data by tenant ID for multi-tenant systems: ``` modelBuilder.Entity() .HasQueryFilter(o => o.TenantId == _currentTenantId); ``` **Default Visibility Rules** Exclude inactive users, unpublished content, or anything else that shouldn’t appear by default. --- ## **When NOT to Use Global Query Filters** While global filters are fantastic, they’re not a one-size-fits-all solution. Avoid using them when: - **You Need Flexibility** If you often need to bypass the filter, it might be better to use explicit query conditions. - **The Filter is Complex** Global filters work best for simple conditions. Complex filters can lead to messy queries and performance issues. - **You Need Full Control** For queries requiring precision and custom behavior, skip the global filter. --- ## **Bypassing Global Query Filters** Need to ignore the global filter for a specific query? EF Core’s got your back: ``` var allAlbums = await context.Albums .IgnoreQueryFilters() .ToListAsync(); ``` This bypasses the filter and fetches all the data. Use it sparingly to keep your app consistent. --- ## **A Real-World Example** Imagine you’re building an e-commerce app. Your `Product` entity has a `IsDiscontinued` flag, and you want to hide discontinued products from most queries. Here’s how you’d set it up: ``` public class Product { public int Id { get; set; } public string Name { get; set; } public bool IsDiscontinued { get; set; } } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasQueryFilter(p => !p.IsDiscontinued); } ``` Now, every query on `Product` automatically excludes discontinued items—no extra code is needed. --- ## **Wrap-Up: Set It and Forget It** Global Query Filters are like setting your app on autopilot for filtering. Once you configure them, EF Core handles the heavy lifting, making your queries cleaner, faster, and more consistent. Whether you’re managing soft deletes, multi-tenant data, or visibility rules, global filters make your life easier. So, what are you waiting for? Set the rules once and let EF Core do the rest. Your app will thank you, your database will thank you, and you’ll have more time for what matters—like enjoying those free cookies. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Unlocking EF Core Performance: How to Track Queries with Event Counters](https://www.woodruff.dev/unlocking-ef-core-performance-how-to-track-queries-with-event-counters/) **Published:** February 8, 2025 **Author:** Chris Woodruff **Excerpt:** If you've ever wondered why your EF Core queries feel sluggish or why your database is working overtime, you're not alone. Performance issues can creep in quietly, and before you know it, your app is struggling to keep up. Wouldn’t it be great if EF Core had built-in performance tracking so you could monitor database activity in real time? Good news—it does! **Content:** If you’ve ever wondered **why your EF Core queries feel sluggish** or why your database is working overtime, you’re not alone. Performance issues can creep in quietly, and before you know it, your app is struggling to keep up. Wouldn’t it be great if **EF Core had built-in performance tracking** so you could monitor database activity in real time? Good news—it does! Enter **Event Counters**—a built-in way to measure EF Core performance **without third-party tools**. Let’s dive into **how Event Counters work, what they track, and how you can use them to optimize your database interactions.** --- ## **What Are Event Counters in EF Core?** Event Counters are **lightweight, built-in telemetry tools** that help you track EF Core’s behavior, including: - **Query Execution Time** – See how long queries take to complete. - **Command Execution Count** – Track the number of database calls. - **Connection Pooling Metrics** – Check how efficiently connections are being used. - **Cache Hits & Misses** – Understand how EF Core is optimizing query results. Instead of guessing why your EF Core performance is slow, **Event Counters give you real data** to analyze. --- ## **How to Enable Event Counters for EF Core** Event Counters are part of **.NET’s diagnostic tools**, making them easy to enable with **dotnet-counters**. ### **Step 1: Start Your Application** Run your EF Core app as you normally would: ``` dotnet run ``` ### **Step 2: Attach `dotnet-counters`** In another terminal, run: ``` dotnet-counters monitor --providers Microsoft.EntityFrameworkCore ``` Now you’re **watching real-time EF Core performance metrics**. ### **Sample Output** ``` [Microsoft.EntityFrameworkCore] active-dbcontexts 5 queries-executed 120 execution-time (ms) 15 connection-pool-in-use 3 cache-hits 90 ``` Here’s what this means: - **Active DbContexts:** How many instances of `DbContext` are currently in use. - **Queries Executed:** The total number of queries since the app started. - **Execution Time (ms):** The average time per query. - **Connection Pool In Use:** How many connections are currently active. - **Cache Hits:** How often EF Core finds a cached result instead of querying the database. --- ## **Using Event Counters in Code** Want to **log these metrics in your app** instead of running them from the terminal? You can capture event counters programmatically using **EventListener**. ### **Step 1: Create a Custom Event Listener** Add this class to your project: ``` using System; using System.Diagnostics.Tracing; public class EfCoreEventListener : EventListener { protected override void OnEventSourceCreated(EventSource eventSource) { if (eventSource.Name == "Microsoft.EntityFrameworkCore") { EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { Console.WriteLine($"[EF Core Event] {eventData.EventName}: {string.Join(", ", eventData.Payload ?? new object[0])}"); } } ``` ### **Step 2: Enable the Listener in Your App** Modify your `Program.cs` or `Startup.cs` file: ``` var listener = new EfCoreEventListener(); ``` Now, EF Core **automatically logs event data** in your application console. --- ## **How to Use Event Counters to Improve Performance** Now that you’re collecting data, let’s **turn insights into action**. ### **1. Identify Slow Queries** - If **execution time is high**, check your LINQ queries and indexes. - Use `.AsNoTracking()` for read-only queries. ### **2. Reduce Unnecessary Queries** - If **queries executed per second is high**, check if your app is making redundant calls. - Consider using **compiled queries** for frequently executed queries. ### **3. Optimize DbContext Usage** - If **active DbContexts** keep increasing, you might have a **memory leak**. - Use `IDbContextFactory` for thread-safe DbContext management. ### **4. Improve Connection Pooling** - If **connection pool in use is consistently maxed out**, consider increasing pool size in your `DbContextOptions`. --- ## **Wrap-Up: Make EF Core Work Smarter, Not Harder** With **Event Counters**, you don’t have to guess why EF Core is slow—you can **see the numbers** and make data-driven optimizations. So, fire up `dotnet-counters`, start tracking your queries, and **make EF Core work for you**. Your database (and your users) will thank you! Have you used **Event Counters** in EF Core yet? Let’s talk in the comments! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core --- ### [Cracking the Code: Decoding Query Plans Like a Pro](https://www.woodruff.dev/cracking-the-code-decoding-query-plans-like-a-pro/) **Published:** February 7, 2025 **Author:** Chris Woodruff **Excerpt:** like at first glance—cryptic, overwhelming, and maybe a little intimidating. But don’t worry! Once you embark on the journey to analyze and understand query plans, you’ll be unlocking hidden performance treasures like a true database pirate, and you'll be amazed at how much you can grow and learn in the process. **Content:** Ever tried reading a treasure map that looks more like a kid’s doodle? That’s what SQL query plans feel like at first glance—cryptic, overwhelming, and maybe a little intimidating. But don’t worry! Once you embark on the journey to analyze and understand query plans, you’ll be unlocking hidden performance treasures like a true database pirate, and you’ll be amazed at how much you can grow and learn in the process. Let’s dive into the world of query plans, break down what they mean, and learn how to use them to optimize your EF Core queries. --- ## **What is a Query Plan?** A **query plan** is like a GPS for your SQL query. It shows your database engine’s route to retrieve your data, including every join, filter, and sort operation. The goal? To understand where your query might be taking unnecessary detours and make it faster, smoother, and more efficient. --- ## **Why Should You Care About Query Plans?** Here’s why query plans are worth your time: 1. **Find Bottlenecks** Query plans highlight expensive operations, like table scans or overly complex joins. 2. **Improve Performance** Understanding the plan helps you rewrite queries or tweak your schema to make things faster. 3. **Show Off Your Nerd Skills** Knowing your way around a query plan makes you the database hero your team didn’t know they needed. --- ## **How to Get a Query Plan** You must get your hands on one before you can analyze a query plan. Here’s how: ### Using SQL Server Management Studio To view a **query execution plan** in **SQL Server Management Studio (SSMS)**, simply open a new query window, write your SQL query, and click on the **“Include Actual Execution Plan”** button in the toolbar (or press **Ctrl + M**). Then, execute your query by pressing **F5**. After the query runs, you’ll see the **execution plan tab** alongside your results. This visual plan shows how SQL Server processed your query, including details like **index usage, join types, and cost percentages** for each operation. It’s a powerful tool for identifying performance bottlenecks and optimizing queries. ![](https://woodruff.dev/wp-content/uploads/2025/02/image-1024x219.png)### Using JetBrains Rider To view a **query execution plan** in a **JetBrains IDE** like **Rider**, first connect to your SQL Server database using the built-in **Database** tool window. Write your SQL query in the query console, then right-click anywhere in the query editor and select **“Explain Plan”** or click the **“Explain Plan”** button (usually represented by an execution plan icon in the toolbar). Rider will generate a **visual execution plan**, showing details like **index usage, join operations, and cost distribution** for each query step. This helps you analyze performance issues and optimize your queries efficiently. ![](https://woodruff.dev/wp-content/uploads/2025/02/image-1-1024x699.png)--- ## **Decoding the Query Plan** At first glance, query plans look like a wall of text or a confusing diagram. Here’s how to break it down: ### 1. **Identify the Starting Point** Every query plan starts with the root operation, usually a `SELECT`. Trace the flow from top to bottom or left to right, depending on the tool. ### 2. **Look for Scans** - **Table Scan:** Reads every row in a table. Bad news for performance. - **Index Scan:** Reads data via an index. Better, but it can still be slow for large datasets. - **Index Seek:** The gold standard! Finds specific rows using an index, like a librarian with a Dewey Decimal number. ### 3. **Check the Joins** Joins can be performance killers. Look for: - **Nested Loops:** Good for small datasets but can slow down with large ones. - **Hash Joins:** Better for larger datasets. - **Merge Joins:** Great for sorted data. ### 4. **Sort and Filter Operations** Sorting and filtering can be expensive. If they show up frequently, consider: - Indexes are added to the columns being sorted or filtered. - Reviewing the query to see if the sort is necessary. --- ## **Common Performance Killers and Fixes** Here are some common issues you might spot in a query plan and how to fix them: ### **Problem 1: Table Scans** *What it means:* The database reads every row in the table. *Fix:* Add an index to the column(s) being queried. ### **Problem 2: Missing Index Warnings** *What it means:* The database is hinting you could speed things up with an index. *Fix:* Listen to it! Add the suggested index. ### **Problem 3: Expensive Sorts** *What it means:* The database is sorting a ton of rows. *Fix:* Add an index to the sorted column or sort less data by filtering first. ### **Problem 4: Too Many Joins** *What it means:* Your query is juggling a lot of relationships. *Fix:* Consider splitting the query into smaller, simpler ones. --- ## **How EF Core Queries Affect Query Plans** EF Core generates SQL queries based on your LINQ expressions, but those LINQ queries can sometimes be… inefficient. For example: ### Inefficient LINQ ``` var users = context.Users .Where(u => u.IsActive) .ToList() .OrderBy(u => u.LastName); ``` This pulls all active users into memory and then sorts them. ### Efficient LINQ ``` var users = context.Users .Where(u => u.IsActive) .OrderBy(u => u.LastName) .ToList(); ``` This pushes the sorting to the database, reducing memory usage and query time. Always test how your LINQ translates into SQL, and adjust your queries for better plans. --- ## **Tools to Help Analyze Query Plans** Here are some tools to make your life easier: - **SQL Server Management Studio:** View graphical query plans. - **JetBrains DataGrip:** View graphical query plans. - **PostgreSQL’s EXPLAIN Tool:** Provides text-based query plans. - **EF Core Logging:** Helps you trace SQL directly from your application. --- ## **Wrap-Up: Decode the Mystery** Query plans might seem daunting, but they become a treasure map to better performance with a bit of practice. Whether spotting bottlenecks, tuning indexes, or rewriting queries, understanding query plans will make you the Sherlock Holmes of your database. So, grab your magnifying glass and start investigating those queries. Your app (and your DBA) will thank you. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Tracking Every Change: Using SaveChanges Interception for EF Core Auditing](https://www.woodruff.dev/tracking-every-change-using-savechanges-interception-for-ef-core-auditing/) **Published:** February 6, 2025 **Author:** Chris Woodruff **Excerpt:** Ever wonder who changed what and when in your database? Or maybe you’ve had that “uh-oh” moment where data was updated, but no one knows how? Good news: EF Core has a built-in way to track changes—without modifying every query manually! SaveChanges Interception lets you hook into EF Core’s SaveChanges() pipeline and log inserts, updates, and deletes automatically. **Content:** Ever wonder **who changed what and when** in your database? Or maybe you’ve had that “uh-oh” moment where **data was updated, but no one knows how**? **Good news:** EF Core has a built-in way to track changes—without modifying every query manually! **SaveChanges Interception** lets you hook into EF Core’s `SaveChanges()` pipeline and log **inserts, updates, and deletes** automatically. Let’s dive into how **SaveChanges Interceptors** work, why they’re perfect for **auditing**, and how you can use them to **keep track of every database change** like a detective. --- ## **What is SaveChanges Interception?** Interceptors in EF Core **allow you to execute custom logic before or after** `SaveChanges()` or `SaveChangesAsync()`. Think of it like **a security camera for your database**—whenever data is added, updated, or deleted, you can **log the change, capture metadata, and store audit records**. --- ## **Why Use SaveChanges Interceptors for Auditing?** - **Track Who Made the Change** – Capture `UserId` or `IP Address`. - **Log Old vs. New Values** – Great for debugging or compliance. - **Enforce Business Rules** – Prevent unwanted updates before they hit the database. - **Works Automatically** – No need to modify every DbContext call. --- ## **How to Implement SaveChanges Interception in EF Core** ### **Step 1: Create a Custom Interceptor** EF Core provides an interface called `ISaveChangesInterceptor`. We’ll create a class that **logs every change** before saving it. ``` using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; public class AuditInterceptor : SaveChangesInterceptor { public override InterceptionResult SavingChanges( DbContextEventData eventData, InterceptionResult result) { var context = eventData.Context; if (context == null) return result; LogChanges(context); return result; } public override async ValueTask SavingChangesAsync( DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { var context = eventData.Context; if (context == null) return result; LogChanges(context); return await base.SavingChangesAsync(eventData, result, cancellationToken); } private void LogChanges(DbContext context) { foreach (var entry in context.ChangeTracker.Entries()) { if (entry.State == EntityState.Added) { Console.WriteLine($"[Audit] INSERT: {entry.Entity.GetType().Name}"); } else if (entry.State == EntityState.Modified) { Console.WriteLine($"[Audit] UPDATE: {entry.Entity.GetType().Name}"); } else if (entry.State == EntityState.Deleted) { Console.WriteLine($"[Audit] DELETE: {entry.Entity.GetType().Name}"); } } } } ``` **What’s Happening Here?** - **Intercept `SaveChanges()` and `SaveChangesAsync()`** before the data is written. - **Loop through tracked entities** to check if they are **Added, Modified, or Deleted**. - **Log every change** to the console (or later, to a database table). --- ### **Step 2: Register the Interceptor in Your DbContext** Once the interceptor is ready, we **register it in the `DbContext` configuration**: ``` public class AppDbContext : DbContext { private readonly AuditInterceptor _auditInterceptor; public AppDbContext(DbContextOptions options, AuditInterceptor auditInterceptor) : base(options) { _auditInterceptor = auditInterceptor; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.AddInterceptors(_auditInterceptor); } } ``` --- ### **Step 3: Add the Interceptor to Dependency Injection** Now, register the `AuditInterceptor` in **`Program.cs`** (or `Startup.cs` if using an older .NET version): ``` services.AddSingleton(); services.AddDbContext(options => options.UseSqlServer("Your_Connection_String")); ``` **That’s it!** Every call to `SaveChanges()` will now **log all database changes automatically**. --- ## **Storing Audit Logs in a Database** Logging changes to the console is great for debugging, but **storing audit logs in the database** is more beneficial for a real-world app. ### **1. Create an Audit Entity** ``` public class AuditLog { public int Id { get; set; } public string EntityName { get; set; } public string ChangeType { get; set; } public string ChangedBy { get; set; } public DateTime Timestamp { get; set; } = DateTime.UtcNow; } ``` ### **2. Log Changes to the Audit Table** Modify `LogChanges()` to **save logs to the database**: ``` private void LogChanges(DbContext context) { var auditLogs = new List(); foreach (var entry in context.ChangeTracker.Entries()) { if (entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) { auditLogs.Add(new AuditLog { EntityName = entry.Entity.GetType().Name, ChangeType = entry.State.ToString(), ChangedBy = "SystemUser" // Replace with actual user info }); } } if (auditLogs.Any()) { context.Set().AddRange(auditLogs); } } ``` Now, **every time an entity is added, updated, or deleted**, an audit log is stored in the database. --- ## **When to Use SaveChanges Interceptors for Auditing?** - **Security & Compliance** – Track sensitive data changes for **SOX, GDPR, or HIPAA** compliance. - **Debugging & Troubleshooting** – Know **who changed what** in case of unexpected issues. - **User Activity Logs** – Monitor app usage by tracking updates to key tables. - **Soft Deletes** – Instead of hard deleting, **intercept and flag records as inactive**. --- ## **Wrap-Up: Keep an Eye on Your Data** SaveChanges Interception is a **powerful, built-in way to track database changes** without modifying every query. Whether you’re logging updates for security, debugging, or compliance, **this technique makes auditing effortless**. So, next time someone asks, **“Who changed this record?”**—you’ll have the answer. **How are you handling auditing in your EF Core apps? Let’s discuss in the comments.** **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Many-to-Many Made Easy: Mastering Relationships in EF Core](https://www.woodruff.dev/many-to-many-made-easy-mastering-relationships-in-ef-core/) **Published:** February 5, 2025 **Author:** Chris Woodruff **Excerpt:** Remember when dealing with many-to-many relationships in Entity Framework felt like trying to assemble IKEA furniture without instructions? You needed an extra join entity and sometimes a sprinkle of luck to get it all working. EF Core has come to the rescue, making many-to-many relationships as easy as pie (or a fully assembled bookshelf) and giving you improved configurations that put you in the driver's seat. **Content:** Remember when dealing with many-to-many relationships in Entity Framework felt like trying to assemble IKEA furniture without instructions? You needed an extra join entity and sometimes a sprinkle of luck to get it all working. EF Core has come to the rescue, making many-to-many relationships as easy as pie (or a fully assembled bookshelf) and giving you improved configurations that put you in the driver’s seat. Let’s dive into how EF Core simplifies many-to-many mappings and makes your life as a developer so much easier. --- ## **What’s the Big Deal About Many-to-Many?** In a classic many-to-many relationship, two entities (like `Post` and `Tag`) are linked by a third table (often called a join table). Previously, EF Core made you define this join table as a separate entity, write mappings, and generally jump through hoops to get it all working. Now, EF Core lets you skip all that and directly define the relationship while still handling the join table behind the scenes. It’s like having a personal assistant for your database. --- ## **How It Works in EF Core** Let’s say you’re building a blogging platform. Each blog post can have multiple tags, and each tag can belong to multiple posts. Here’s how you can set it up: ### Define Your Entities ``` public class Post { public int Id { get; set; } public string Name { get; set; } public ICollection Tags { get; set; } } public class Tag { public int Id { get; set; } public string Text { get; set; } public ICollection Posts { get; set; } } ``` No join entity is needed. Just two collections that point to each other. Simple, right? --- ### Configure the Relationship Now, tell EF Core how these two entities are connected: ``` protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasMany(p => p.Tags) .WithMany(t => t.Posts) .UsingEntity(j => j.ToTable("PostTag")); } ``` Here’s what this does: - `HasMany` and `WithMany`: Define the many-to-many relationship. - `UsingEntity`: Tells EF Core to use a join table called `PostTag`. And that’s it! EF Core handles the join table automatically. --- ## **What Happens Behind the Scenes?** EF Core creates the `PostTag` table for you, with two foreign keys: - `PostId`: References the `Post` table. - `TagId`: References the `Tag` table. Here’s what the schema looks like: ``` CREATE TABLE PostTag ( PostId INT NOT NULL, TagId INT NOT NULL, PRIMARY KEY (PostId, TagId), FOREIGN KEY (PostId) REFERENCES Post(Id), FOREIGN KEY (TagId) REFERENCES Tag(Id) ); ``` It’s all done automatically, so you can focus on writing awesome code instead of fiddling with join entities. --- ## **Adding Data** Adding data to a many-to-many relationship is a breeze: ``` var post = new Post { Name = "EF Core Rocks" }; var tag = new Tag { Text = "EntityFramework" }; post.Tags = new List { tag }; context.Posts.Add(post); await context.SaveChangesAsync(); ``` EF Core inserts records into the `Post` and `Tag` tables, and links them in the `PostTag` table. Magic. ✨ --- ## **Querying Many-to-Many Relationships** Fetching related data is just as simple: #### Get Tags for a Post ``` var post = await context.Posts .Include(p => p.Tags) .FirstOrDefaultAsync(p => p.Id == postId); ``` #### Get Posts for a Tag ``` var tag = await context.Tags .Include(t => t.Posts) .FirstOrDefaultAsync(t => t.Id == tagId); ``` No complex SQL, no manual joins—just clean, readable LINQ. --- ## **When to Use Many-to-Many in EF Core** This simplified approach is perfect for: - **Tagging Systems:** Blogs, products, categories, etc. - **Memberships:** Users belonging to multiple groups or roles. - **Anything Else:** Any scenario where two entities need a flexible relationship. --- ## **Tips for Mastering Many-to-Many** 1. **Use `.UsingEntity` Wisely** You can customize the join table if needed (e.g., adding additional columns). 2. **Index Your Join Table** If your database grows, adding indexes to the join table can improve query performance. 3. **Profile Your Queries** Monitor SQL queries to ensure they’re efficient and behaving as expected. --- ## **Wrap-Up: Simplify Your Relationships** EF Core takes the pain out of many-to-many relationships, letting you focus on building features instead of wrestling with configurations. With just a few lines of code, you get clean, efficient mappings that work like a charm. So, what are you waiting for? Go forth and simplify your relationships (at least in your databases)! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Accelerating EF Core with Compiled Queries](https://www.woodruff.dev/accelerating-ef-core-with-compiled-queries/) **Published:** February 7, 2023 **Author:** Chris Woodruff **Excerpt:** One of my passions is data! I love all forms of data and how to work with it. I am a .NET fanatic also and have been a fan of Entity Framework since 2008. It has gotten so good since .NET Core 1.0, and I love to push EF Core to the limit, especially around Web APIs. In this blog post, I want to share my ideas about Compiled Queries in EF Core. **Content:** > We are surrounded by data, but starved for insights. > > Jay Baer One of my passions is data! I love all forms of data and how to work with it. I am a .NET fanatic also and have been a fan of Entity Framework since 2008. It has gotten so good since .NET Core 1.0, and I love to push EF Core to the limit, especially around Web APIs. In this blog post, I want to share my ideas about **Compiled Queries in EF Core**. ## Queries in EF Core Before we look at Compiled Queries in EF Core, let’s examine the “normal” query most developers utilize in their .NET projects. Here is an example of such a query. ``` public Album GetById(int id) { var dbAlbum = _context.Albums.Find(id); return dbAlbum; } ``` What happens in your application at runtime when using the query above? - The query has to be built using the value from the id parameter of the GetById() method. - The query is then compiled. This will be cached to save on JIT’ing the query everytime. - Entity Framework builds the corresponding SQL SELECT statement for the query and sends that command to the SQL database. - Entity Framework receives the results from the SQL database and hydrates the Album object based on the results. - If found, the resulting Album object is returned, or a null is returned. If this query is executed many times during the lifetime of the .NET program, it will become a performance issue to have the query compiled each time. This is where creating compiled queries using EF Core will be a benefit for your and your code. ## Compiled Queries in EF Core Entities Framework Core (EF Core) 2.0 was the version where Compiled Queries was introduced. The 2.0 release of EF Core was stimulating for developers around performance because of not only compiled queries DBContext pooling and FromSql. Blog post around both of these other EF Core features coming soon. A query is “compiled” when its SQL statement is generated and optimized at runtime. These queries can significantly boost performance, exceptionally when the same query gets executed multiple times. In EF Core, compiled queries are implemented using the **CompileQuery()** or **CompileQueryAsync()** method. In my demos and testing, I will create the compiled queries in the code close to the execution and my favorite location: in the DBContext class. I find that allowing the compiled queries to be in the DBContext and using the AddDbContextPool method when adding the DBContext to the Dependency Injection container is crucial to my use of compiled queries. You can find that code in my [**Chinook7WebAPI\_CmpldQry**](https://github.com/cwoodruff/Chinook7WebAPI_CmpldQry) repo, while my tests to show the performance increase is in my [**compiled-query**](https://github.com/cwoodruff/EFCoreDemos/tree/main/EFCoreDemos/compiled-query) project that is part of my [**EFCoreDemos**](https://github.com/cwoodruff/EFCoreDemos) repo. To use a compiled query, you first need to create a delegate that takes the input parameters for the query and returns the result. I also like to write a method on the DBContext class as a wrapper for the delegate. Here is an example of a compiled query that retrieves all customers from the database: ### Synchronous Compiled Query ``` private static readonly Func _queryGetAllAlbums = EF.CompileQuery((ChinookContext db) => db.Albums); public IEnumerable GetAllAlbums() => _queryGetAllAlbums(this); ``` ### Asynchronous Compiled Query ``` private static readonly Func _queryGetAllAlbumsAsync = EF.CompileAsyncQuery((ChinookContext db) => db.Albums); public IAsyncEnumerable GetAllAlbumsAsync() => _queryGetAllAlbumsAsync(this); ``` Once you have created the delegate and the method in your DBContext, you can use it throughout your solution. Here is an example of how you might use **ChinookContext.GetAllAlbums()** method: ``` using (var context = new ChinookContext()) { var customers = GetAllAlbums(context); foreach (var album in albums) { Console.WriteLine(album.Name); } } ``` It’s important to note that compiled queries are cached by default, so the SQL statement is only generated and optimized the first time the query is executed. This means that subsequent executions will be much faster. The other important note regarding adding your compiled queries to your DBContext is that the queries will be cached when the DBContext is created. If you use the AddDbContext() method, your context will be created for each call that gets a context from the DI container. That is why it is essential to use the AddDbContextPool() method, which will create several contexts in a pool and allow your application to use one with the queries compiled for each use of the context from the DI container. When you run the compiled-query EFCore demo, you will see the results of the different ways to use compiled queries. You can find the demo in my GitHub repo. ![](https://woodruff.dev/wp-content/uploads/2023/02/compiled-queries-demo-results.png)## Drawbacks using Compiled Queries Compiled queries can be helpful in situations where you need to execute the same query multiple times, and performance is a concern. However, it’s essential to remember that they come with some trade-offs. For example, compiled queries can make debugging and testing your code more difficult since the SQL statement is generated at runtime. Additionally, they can make your code more complex and harder to understand. ## Conclusion Overall, compiled queries are a powerful feature in EF Core that can help improve the performance of your data access code. However, as with any performance optimization, it’s essential to use them judiciously and to consider the trade-offs involved. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, EF Core, Entity Framework Core --- ### [Batching Like a Boss: Using IDbContextFactory for High-Performance EF Core Updates](https://www.woodruff.dev/batching-like-a-boss-using-idbcontextfactory-for-high-performance-ef-core-updates/) **Published:** February 3, 2025 **Author:** Chris Woodruff **Content:** I love receiving feedback on my blog posts! After sharing “[Batching Updates and Inserts: Making EF Core Work Smarter, Not Harder](https://woodruff.dev/batching-updates-and-inserts-making-ef-core-work-smarter-not-harder/),” I received some great comments, especially from [MaxiTB](https://mastodon.social/@maxitb) on Mastodon, which I wanted to share. Batch operations—whether inserting or updating multiple records—can quickly lead to performance issues, particularly when **DbContext** is misused. However, with the helpful support of **IDbContextFactory** and **DbContext Pooling**, you can optimize batch processing, ensuring your application remains **fast, scalable, and thread-safe**. Let’s break it down and see how **you can batch like a boss in EF Core!** --- ## **The Problem: DbContext and Batch Operations** By default, EF Core allows **batching of inserts and updates** when possible. But if you’re calling `SaveChanges()` in a loop, you’re in for a **bad time**. ### **The Wrong Way: Multiple DbContext Saves** ``` foreach (var order in orders) { order.Status = "Processed"; await context.SaveChangesAsync(); // Too many database calls! } ``` This sends **one update statement per record**, causing **tons of unnecessary database trips**. Not great for performance. ### **The Better Way: Batch and Save Once** ``` foreach (var order in orders) { order.Status = "Processed"; } await context.SaveChangesAsync(); // One batch update! ``` This **combines updates into a single database transaction**—MUCH faster! But wait—there’s more! **DbContext Pooling + `IDbContextFactory`** can take this a step further. --- ## **Why `IDbContextFactory`?** `IDbContextFactory` **creates new DbContext instances on demand** rather than relying on **scoped dependencies**. This is **super useful** in batch operations because: - **Prevents long-lived DbContext issues** (no memory leaks!) - **Thread-safe batch processing** (no concurrency nightmares) - **Works great with DbContext Pooling** (better resource management) ### **How to Register `IDbContextFactory` in EF Core** To use `IDbContextFactory`, update your **DI configuration** in `Program.cs`: ``` services.AddDbContextFactory(options => options.UseSqlServer("Your_Connection_String") ); ``` Boom! Now, we have **a factory to create fresh DbContext instances** whenever needed. --- ## **Using `IDbContextFactory` for Batch Updates** Instead of keeping a **single DbContext instance for all batch operations**, let’s **create a new one per batch** using `IDbContextFactory`. ### **The Right Way: Use IDbContextFactory for Batch Updates** ``` public class BatchUpdateService { private readonly IDbContextFactory _contextFactory; public BatchUpdateService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessBatchUpdatesAsync(List orderIds) { using var context = _contextFactory.CreateDbContext(); // Fresh context for this batch var orders = await context.Orders .Where(o => orderIds.Contains(o.Id)) .ToListAsync(); foreach (var order in orders) { order.Status = "Processed"; } await context.SaveChangesAsync(); // Batch update, single trip to DB! } } ``` ### **Why This is Awesome:** - **Each batch gets a fresh DbContext** (avoiding conflicts). - **Ensures DbContext is disposed properly** after the batch completes. - **Plays nice with DbContext Pooling**, improving efficiency. --- ## **How Different Databases Handle Batch Updates** Not all databases handle batch operations the same way. **SQL Server** does it well, but other databases? Not so much. **Database****Batch Inserts****Batch Updates****Notes****SQL Server**YesYesBest support for batching**PostgreSQL**Yes**Limited**Updates row-by-row unless optimized**MySQL****Limited****No**Bulk updates require custom logic**SQLite****No****No**One statement at a time### **Optimizing Batching for Non-SQL Server Databases** If you’re **not using SQL Server**, here are some ways to speed things up: **1. Use Raw SQL for Updates** ``` await context.Database.ExecuteSqlRawAsync( "UPDATE Orders SET Status = 'Processed' WHERE Id IN ({0})", orderIds); ``` **2. Process Large Batches in Chunks** ``` const int batchSize = 100; for (int i = 0; i < orders.Count; i += batchSize) { var batch = orders.Skip(i).Take(batchSize).ToList(); using var context = _contextFactory.CreateDbContext(); context.Orders.UpdateRange(batch); await context.SaveChangesAsync(); } ``` **This prevents loading too much data into memory at once!** **3. Use Bulk Extensions for Faster Writes** For databases like MySQL or SQLite, **EFCore.BulkExtensions** is a lifesaver: ``` await context.BulkUpdateAsync(orders); ``` --- ## **Final Takeaways: Batch Like a Pro!** Batch processing in EF Core **doesn’t have to be slow**. By combining: - **Efficient Batching** (saving once per batch) - **IDbContextFactory for Fresh DbContext Instances** - **DbContext Pooling for Performance** - **Optimized Queries for Non-SQL Server Databases** You can **level up your batch updates** and **keep your app blazing fast**! So, what’s your **go-to strategy** for batching updates? Have you tried **`IDbContextFactory`** in your EF Core projects? Let’s chat in the comments! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, dotnet, Entity Framework Core --- ### [Query Tags: Debugging EF Core Like a Detective](https://www.woodruff.dev/query-tags-debugging-ef-core-like-a-detective/) **Published:** February 3, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re a detective working on a big case. You’ve got a pile of clues (queries), but they’re all mixed together with no labels. Which one belongs to which part of the case? Frustrating, right? That’s what debugging EF Core queries feels like without Query Tags. But with Query Tags, it's like a breath of fresh air, relieving the frustration and making the process more manageable. Query Tags in EF Core are like sticky notes for your SQL. They allow you to label your queries, making it significantly easier to track down problems, optimize performance, and appear as a debugging wizard. **Content:** Imagine you’re a detective working on a big case. You’ve got a pile of clues (queries), but they’re all mixed together with no labels. Which one belongs to which part of the case? Frustrating, right? That’s what debugging EF Core queries feels like without **Query Tags**. But with Query Tags, it’s like a breath of fresh air, relieving the frustration and making the process more manageable. Query Tags in EF Core are like sticky notes for your SQL. They allow you to label your queries, making it significantly easier to track down problems, optimize performance, and appear as a debugging wizard. --- ## **What Are Query Tags?** Query Tags allow you to attach comments to your SQL queries directly from your EF Core code. These tags appear in the generated SQL, so when you’re profiling or analyzing queries, you can instantly tell which part of your app they’re coming from. Here’s what a query looks like **without** a tag: ``` SELECT * FROM Blogs WHERE IsActive = 1; ``` And here’s what it looks like **with** a Query Tag: ``` -- Fetching active blogs for the dashboard SELECT * FROM Blogs WHERE IsActive = 1; ``` See the difference? That comment at the top can save you hours of head-scratching. --- ## **Why Use Query Tags?** Here’s why Query Tags are a game-changer: 1. **Faster Debugging** When you’re analyzing SQL logs or database traces, tags make it easy to pinpoint where a query originated. 2. **Better Performance Tuning** Spot and optimize slow queries by knowing exactly which part of your app triggered them. 3. **Team Collaboration** Make life easier for your teammates (and your future self) by leaving meaningful breadcrumbs. --- ## **How to Add Query Tags** Adding Query Tags in EF Core is ridiculously simple. Here’s how you do it: ### Basic Example ``` var blogs = await context.Blogs .TagWith("Fetching active blogs for the dashboard") .Where(b => b.IsActive) .ToListAsync(); ``` The generated SQL will include this comment: ``` -- Fetching active blogs for the dashboard SELECT * FROM Blogs WHERE IsActive = 1; ``` ### Adding Dynamic Information You can even include dynamic data in your tags for more context: ``` var userId = 42; var blogs = await context.Blogs .TagWith($"Query by UserId: {userId}") .Where(b => b.CreatedBy == userId) .ToListAsync(); ``` Resulting SQL: ``` -- Query by UserId: 42 SELECT * FROM Blogs WHERE CreatedBy = 42; ``` --- ## **Use Cases for Query Tags** ### 1. **API Diagnostics** Tag queries with the name of the API endpoint triggering them: ``` .TagWith("API: /blogs/active"); ``` ### 2. **Background Jobs** If you’re running background jobs, tag their queries to distinguish them from user-triggered ones: ``` .TagWith("Background Job: Daily Cleanup"); ``` ### 3. **Performance Profiling** Add context to queries during performance testing: ``` .TagWith("Performance Test: High-traffic scenario"); ``` --- ## **Pro Tips for Using Query Tags** 1. **Keep Tags Meaningful** Avoid generic tags like “Fetching data.” Be specific: “Fetching top 10 active users for leaderboard.” 2. **Combine with Other EF Core Features** Pair Query Tags with `.AsNoTracking()` or `.AsSplitQuery()` to diagnose performance for specific scenarios. 3. **Avoid Overusing Tags** Not every query needs a tag. Focus on critical or complex queries that are harder to trace. 4. **Standardize Tags in Your Team** Use a consistent format for tags across your project. For example: - API queries: `"API: [Endpoint]"`. - Jobs: `"Job: [Job Name]"`. - Features: `"Feature: [Feature Name]"`. --- ## **When NOT to Use Query Tags** Query Tags are amazing for diagnostics but don’t go overboard. Here’s when to skip them: - **For Simple Queries** If the query is straightforward and easy to trace, a tag might be overkill. - **On Every Query** Don’t clutter your SQL logs with unnecessary tags. Use them selectively for debugging and performance tuning. --- ## **Wrap-Up: Tag It Like a Pro** Query Tags are the sticky notes of EF Core—simple, effective, and a lifesaver when debugging complex systems. Whether you’re profiling queries, diagnosing issues, or optimizing performance, these little comments can save you hours of frustration and make you look like a debugging superhero. So next time you’re knee-deep in SQL logs, remember: a well-placed Query Tag can turn your detective work into a breeze. Happy debugging! **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Batching Updates and Inserts: Making EF Core Work Smarter, Not Harder](https://www.woodruff.dev/batching-updates-and-inserts-making-ef-core-work-smarter-not-harder/) **Published:** February 1, 2025 **Author:** Chris Woodruff **Excerpt:** Let’s face it: making multiple database calls is like running back and forth between the kitchen and dining room to serve dinner one plate at a time. It’s inefficient, exhausting, and makes your app look like it’s stuck in the Stone Age. But with batching in EF Core, you can wave goodbye to this inefficiency. It's the magic trick that lets you update or insert multiple records in a single trip to the database, making you and your app more productive and efficient. Batching is all about doing more with less. It minimizes round trips to the database, saves resources, and makes your app faster. Understanding and implementing batching in EF Core is like gaining a superpower-it makes your app work smarter, not harder, and gives you a sense of control and confidence in your development process. **Content:** Let’s face it: making multiple database calls is like running back and forth between the kitchen and dining room to serve dinner one plate at a time. It’s inefficient, exhausting, and makes your app look like it’s stuck in the Stone Age. But with **batching** in EF Core, you can wave goodbye to this inefficiency. It’s the magic trick that lets you update or insert multiple records in a single trip to the database, making you and your app more productive and efficient. Batching is all about doing more with less. It minimizes round trips to the database, saves resources, and makes your app faster. Understanding and implementing batching in EF Core is like gaining a superpower- it makes your app work smarter, not harder, and gives you a sense of control and confidence in your development process. --- ## **What is Batching?** Batching combines multiple updates or inserts into a single database operation. Instead of executing a separate `INSERT` or `UPDATE` for every record, EF Core groups them into batches and sends them in one go. It’s like placing one big order at a restaurant instead of going back to the counter for every dish. Here’s a quick comparison: ### Without Batching ``` foreach (var user in users) { context.Users.Add(user); await context.SaveChangesAsync(); } ``` Each `SaveChangesAsync` call sends a separate `INSERT` to the database. Painful. ### With Batching ``` context.Users.AddRange(users); await context.SaveChangesAsync(); ``` One call to `SaveChangesAsync`, one batch of `INSERT`s. Much better! --- ## **Why Should You Care?** Batching in EF Core isn’t just cool—it’s necessary if you care about performance. Here’s why: 1. **Fewer Round Trips** Every database call adds latency. Batching reduces the number of trips, making your app faster. 2. **Lower Resource Usage** Each call to the database consumes resources on both the app and database server. Batching minimizes this overhead. 3. **Better Scalability** When your app grows, batching helps handle larger workloads without crushing your database. --- ## **How to Batch Like a Pro** #### 1. **Batching Inserts** Let’s say you’re adding multiple users to the database. Instead of adding them one by one: ``` var newUsers = new List { new User { Name = "Alice", Email = "alice@example.com" }, new User { Name = "Bob", Email = "bob@example.com" } }; context.Users.AddRange(newUsers); await context.SaveChangesAsync(); ``` EF Core batches these `INSERT`s into a single database call. --- #### 2. **Batching Updates** Updating multiple records? No problem. Use a `foreach` loop and call `SaveChangesAsync` once: ``` var users = await context.Users.Where(u => u.IsActive).ToListAsync(); foreach (var user in users) { user.LastActive = DateTime.UtcNow; } await context.SaveChangesAsync(); ``` EF Core combines all the `UPDATE`s into a single batch. --- #### 3. **Handling Large Batches** EF Core batches operations automatically by default, but there’s a limit to how many commands can fit in a batch. If you’re working with massive datasets, consider processing them in chunks: ``` const int batchSize = 100; for (int i = 0; i < users.Count; i += batchSize) { var batch = users.Skip(i).Take(batchSize).ToList(); context.Users.AddRange(batch); await context.SaveChangesAsync(); } ``` This ensures you don’t overwhelm the database with a single gigantic batch. --- ## **Tips for Efficient Batching** ### **Use AddRange and UpdateRange** Methods like `AddRange` and `UpdateRange` are your best friends for batching. Use them whenever you’re working with collections. ### **Leverage Transactions** Combine batching with transactions to ensure all your operations succeed or fail together: ``` using var transaction = await context.Database.BeginTransactionAsync(); context.Users.AddRange(users); await context.SaveChangesAsync(); await transaction.CommitAsync(); ``` ### **Profile Your Queries** Use tools like SQL Profiler or EF Core’s logging to see how batches are sent to the database. Optimize as needed. --- ## **When NOT to Batch** Batching is fantastic, but it’s not always the right choice. Here are a few cases where batching might not work well: - **When Operations Depend on Each Other** If one operation relies on the result of another, batching won’t work. - **Small Datasets** For a handful of records, batching might not offer noticeable benefits. - **Complex Relationships** If your entities have complex relationships, batching could lead to unexpected results. Always test! --- ## **Wrap-Up: Batch It Up!** Batching updates and inserts in EF Core is a simple yet powerful way to improve performance. By reducing database round trips and leveraging efficient operations, you can make your app faster, lighter, and ready to scale. So, next time you add or update multiple records, ask yourself: “Am I batching this?” If not, it’s time to level up your EF Core game. Your app—and your database—will thank you. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [No-Tracking Queries: Speed Up Your EF Core Like a Pro](https://www.woodruff.dev/no-tracking-queries-speed-up-your-ef-core-like-a-pro/) **Published:** January 31, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re at a library, flipping through books at lightning speed. You don’t need to write notes in the margins or keep tabs on which books you’ve touched—you’re just reading. That’s what no-tracking queries are in EF Core: a way to grab data without keeping track of changes. It’s fast, lightweight, and perfect for read-only operations. But wait, there’s more! Let’s throw identity resolution into the mix, a smart way to avoid duplicate entities when working with no-tracking queries. Together, they make a dynamic duo for blazing-fast data fetching, significantly reducing the time it takes to retrieve data from the database. **Content:** Imagine you’re at a library, flipping through books at lightning speed. You don’t need to write notes in the margins or keep tabs on which books you’ve touched—you’re just reading. That’s what **no-tracking queries** are in EF Core: a way to grab data without keeping track of changes. It’s fast, lightweight, and perfect for read-only operations. But wait, there’s more! Let’s throw **identity resolution** into the mix, a smart way to avoid duplicate entities when working with no-tracking queries. Together, they make a dynamic duo for blazing-fast data fetching, significantly reducing the time it takes to retrieve data from the database. --- ## **What’s a No-Tracking Query?** By default, EF Core keeps track of every entity it fetches. This is great if you plan to update those entities, but it’s overkill if you’re just reading data. No-tracking queries tell EF Core, “Hey, I’m just looking, no need to follow me around.” ### Without No-Tracking ``` var albums = await context.Albums.ToListAsync(); ``` EF Core tracks each `Album` it fetches, just in case you want to make changes later. ### With No-Tracking ``` var albums = await context.Albums .AsNoTracking() .ToListAsync(); ``` Now EF Core skips the tracking overhead. It fetches the data and calls it a day. --- ## **Why Use No-Tracking Queries?** Here’s why no-tracking queries are awesome: 1. **Speed Boost** No tracking = less work for EF Core. Your queries run faster, especially with large datasets. 2. **Lower Memory Usage** Tracking entities means keeping them in memory. No-tracking queries skip that step, making them lightweight. 3. **Read-Only Bliss** For read-only scenarios like APIs or reports, no-tracking queries are perfect. --- ## **Enter Identity Resolution** Now, imagine you’re querying related data, like albums and their tracks. Without identity resolution, EF Core might fetch the same entity multiple times and treat them as separate objects. That’s messy. Identity resolution swoops in to save the day, ensuring each entity is represented only once. ### No-Tracking Without Identity Resolution ``` var albums = await context.Albums .Include(a => a.Tracks) .AsNoTracking() .ToListAsync(); ``` In this case, EF Core won’t track entities *or* ensure a single instance per `Album`. Duplicate objects could sneak in. ### No-Tracking With Identity Resolution ``` var albums = await context.Albums .AsNoTrackingWithIdentityResolution() .ToListAsync(); ``` Now EF Core ensures every `Album` entity is unique, even in complex queries. --- ## **When to Use These Features** ### Use No-Tracking When: - You’re fetching data for display or reporting. - You’re dealing with large datasets where tracking would eat up memory. - You don’t need to modify the data. ### Use No-Tracking With Identity Resolution When: - You’re fetching related entities (e.g., parent-child relationships). - You need a clean, duplicate-free representation of your data. --- ## **Performance Example** Let’s see these in action with a quick performance snapshot: ### Without No-Tracking - Query Time: 300ms - Memory Usage: High (tracked entities for 10,000 rows) ### With No-Tracking - Query Time: 120ms - Memory Usage: Low (no tracking overhead) ### With No-Tracking + Identity Resolution - Query Time: 150ms - Memory Usage: Low (slightly higher due to identity resolution) --- ## **How to Set It as Default** Tired of adding `.AsNoTracking()` to every query? Make it the default for your context: ``` protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer("") .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } ``` This way, no-tracking becomes the default, and you can override it only when needed. --- ## **Gotchas to Watch Out For** Before you go no-tracking everything, keep these in mind: 1. **No Updates Allowed** You can’t modify entities fetched with no-tracking queries. They’re read-only. 2. **Identity Resolution Isn’t Free** While it prevents duplicates, it adds a bit of overhead. Use it only when necessary. 3. **Be Explicit About Relationships** If you fetch related data with no-tracking, ensure you include what you need. Lazy loading won’t work here. --- ## **Wrap-Up: Track Less, Do More** No-tracking queries and identity resolution are like a breath of fresh air for your EF Core app. They make your queries faster, your memory usage lower, and your life easier. Whether you’re building APIs, dashboards, or complex reports, these features can take your EF Core game to the next level. So, next time you’re fetching data, ask yourself: Do I really need to track this? If not, let no-tracking queries speed things up. Your app—and your database—will thank you. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Query Projection: Stop Hauling More Data Than You Need!](https://www.woodruff.dev/query-projection-stop-hauling-more-data-than-you-need/) **Published:** January 30, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re moving to a new house. You could pack only the essentials and enjoy a stress-free move, or you could drag everything you own—grandma’s old lamp, that broken treadmill, and a box labeled “random cables.” This analogy of moving to query projection makes the concept familiar and relevant. Moving with all that clutter? Nightmare. The same applies to your EF Core queries. Why fetch every column in a table when all you need is a handful of them? That’s where query projection comes in. It’s all about keeping your data lean, clean, and most importantly, efficient. Embracing query projection is like optimizing your coding practices for maximum efficiency. **Content:** Imagine you’re moving to a new house. You could pack only the essentials and enjoy a stress-free move, or you could drag everything you own—grandma’s old lamp, that broken treadmill, and a box labeled “random cables.” This analogy of moving to query projection makes the concept familiar and relevant. Moving with all that clutter? Nightmare. The same applies to your EF Core queries. Why fetch every column in a table when all you need is a handful of them? That’s where **query projection** comes in. It’s all about keeping your data lean, clean, and most importantly, efficient. Embracing query projection is like optimizing your coding practices for maximum efficiency. --- ## **What is Query Projection?** Query projection is the art of selecting only the data you actually need from a database. Instead of pulling in an entire entity (and all its columns), you pick and choose the specific properties you need for your application. Think of it like ordering a burrito—you can get one loaded with everything or just the fillings you actually want. Fewer ingredients, faster service. Here’s an example to compare: ### Without Projection ``` var employees = await context.Employees.ToListAsync(); ``` This pulls **every column** for every employee, including things you don’t care about (looking at you, “FaxNumber”). ### With Projection ``` var employees = await context.Employees .Select(e => new { e.Id, e.FirstName, e.LastName }) .ToListAsync(); ``` Now, you’re only grabbing the data you need. Clean, simple, and oh-so-efficient. --- ## **Why Should You Care About Query Projection?** Here’s why query projection should be your go-to strategy: 1. **Performance Boost** The less data you fetch, the faster your query runs. This is especially important when dealing with large datasets or high-traffic APIs. 2. **Reduced Memory Usage** Pulling fewer columns means less data is stored in memory. Your app will thank you for keeping it light. 3. **Simpler Data Transfer** If you send data over the wire (e.g., in an API response), projecting only what you need keeps payloads smaller and faster. 4. **Improved Clarity** With projections, your queries clearly show what data is being fetched, making your code easier to understand and maintain. --- ## **Best Practices for Query Projection** #### 1. **Only Select What You Need** Resist the temptation to fetch entire entities unless you really need all the data. Be specific: ``` var products = await context.Products .Select(p => new { p.Id, p.Name, p.Price }) .ToListAsync(); ``` Fetching only the essentials is faster, lighter, and cleaner. --- #### 2. **Use DTOs for Complex Projections** If you’re projecting a lot of fields or preparing data for an API, create a **Data Transfer Object (DTO)** to keep things organized: ``` public class EmployeeDto { public int Id { get; set; } public string FullName { get; set; } public string JobTitle { get; set; } } var employees = await context.Employees .Select(e => new EmployeeDto { Id = e.Id, FullName = e.FirstName + " " + e.LastName, JobTitle = e.Title }) .ToListAsync(); ``` This makes your code easier to read and reduces clutter in your controller or service. --- #### 3. **Combine Projection with Filtering** Why fetch all rows if you only need a subset? Combine projection with filters to further optimize your queries: ``` var activeEmployees = await context.Employees .Where(e => e.IsActive) .Select(e => new { e.Id, e.Name }) .ToListAsync(); ``` Filter first, then project. It’s like cutting your vegetables *before* cooking—so much more efficient. --- #### 4. **Leverage Anonymous Types for Quick Projections** For quick, throwaway results (e.g., in a temporary report or debug tool), anonymous types are your best friend: ``` var report = await context.Sales .Select(s => new { s.ProductName, s.QuantitySold }) .ToListAsync(); ``` Just remember: anonymous types are great for temporary use but not for long-term code, as they lack explicit structure. --- #### 5. **Avoid Over-Nesting** Deeply nested projections can be hard to maintain and slow to execute. Flatten your projections where possible: ``` var orders = await context.Orders .Select(o => new { o.Id, o.CustomerName, ProductNames = o.Products.Select(p => p.Name).ToList() }) .ToListAsync(); ``` This keeps your queries readable while still getting the data you need. --- ## **Common Mistakes to Avoid** 1. **Fetching Too Much Data** Don’t grab entire entities unless you absolutely need all their columns. 2. **Overusing Anonymous Types** Use DTOs for clarity and reuse, especially in APIs or large projects. 3. **Skipping Filters** Always filter your data as early as possible to avoid unnecessary processing. --- ## **Wrap-Up: Less is More** Query projection is all about keeping your data fetching smart and efficient. By selecting only what you need, you’ll boost your app’s performance, reduce memory usage, and make your code easier to maintain. So next time you write a query, think of it like packing for a trip: only take what you need, and leave the rest behind. Your app—and your database—will travel light and run fast. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Explicit Includes: The Art of Fetching Just Enough Data in EF Core](https://www.woodruff.dev/explicit-includes-the-art-of-fetching-just-enough-data-in-ef-core/) **Published:** January 29, 2025 **Author:** Chris Woodruff **Excerpt:** If you’ve ever gone grocery shopping while hungry, you know how easy it is to end up with more than you need. The same thing happens in EF Core when you’re too eager with your Include statements—you fetch all the data, even the stuff you don’t need, and suddenly, your app feels sluggish. That’s where Explicit Includes comes in, allowing you to fetch only what you need when needed, making your development process more efficient. Think of it as grocery shopping with a list, but for your code. **Content:** If you’ve ever gone grocery shopping while hungry, you know how easy it is to end up with more than you need. The same thing happens in EF Core when you’re too eager with your Include statements—you fetch all the data, even the stuff you don’t need, and suddenly, your app feels sluggish. That’s where **Explicit Includes** comes in, allowing you to fetch only what you need when needed, making your development process more efficient. Think of it as grocery shopping with a list but for your code. --- ## **What Are Explicit Includes?** When you fetch an entity in EF Core, its related data doesn’t magically appear unless you tell EF Core to include it. Explicit Includes give you precise control over which related data gets loaded, helping you: - Avoid over-fetching. - Keep your queries lean and mean. - Improve app performance by loading related data only when it’s actually needed. In other words, Explicit Includes are the Marie Kondo of data fetching—they spark joy by keeping your queries clean and intentional. --- ## **How to Use Explicit Includes** Here’s the scenario: You’re building a blogging app. Each blog has a list of posts, and each post has comments. Fetching everything in one go can quickly turn into a monster query. ### Basic Fetch Without Includes ``` var blogs = await context.Blogs.ToListAsync(); ``` This fetches blogs but leaves the related posts and comments behind. If you try to access them, you’ll get a sad, empty collection. ### Include to the Rescue ``` string titleFilter = "Entity Framework Core"; // Example filter condition var blogs = await context.Blogs .Include(b => b.Posts.Where(p => p.Title.Contains(titleFilter))) .ThenInclude(p => p.Comments) .ToListAsync(); ``` Now you’ve got blogs, their posts, and even the comments—all in one query. But wait… is this too much data for your use case? 🤔 --- ## **When to Use Explicit Includes** Here’s where Explicit Includes shine: when you need related data, but not all at once. Instead of loading everything up front, you can fetch related data as needed. ### Example: Fetching Related Data on Demand Let’s say you fetch your blogs without their posts initially: ``` var blogs = await context.Blogs.ToListAsync(); ``` Then, for a specific blog, you explicitly load its posts later: ``` foreach (var blog in blogs) { await context.Entry(blog) .Collection(b => b.Posts) .LoadAsync(); } ``` You’re fetching posts only when necessary, keeping your initial query lightweight. It’s like ordering dessert only after you’ve decided you’re not full from dinner. --- ## **Why Explicit Includes Matter** Here’s why you’ll love Explicit Includes: 1. **Avoid Over-fetching** Fetch only the needed data, reducing memory usage and speeding up queries. 2. **Fine-Grained Control** Decide precisely when and how related data gets loaded rather than fetching it all upfront. 3. **Better Performance for Large Datasets** Loading everything in one query can overwhelm your database server. Explicit Includes let you break things down into smaller, more manageable chunks. --- ## **A Real-World Scenario** Imagine a dashboard showing a list of blogs. Each blog displays the number of posts but not the posts themselves. Fetching posts in this case is unnecessary. ### The Wrong Way: Over-fetching ``` var blogs = await context.Blogs .Include(b => b.Posts) .ToListAsync(); ``` You’ve just fetched all the posts for every blog, even though you only needed a count. Oops. ### The Right Way: Explicit Loading ``` var blogs = await context.Blogs.ToListAsync(); foreach (var blog in blogs) { blog.PostCount = await context.Entry(blog) .Collection(b => b.Posts) .Query() .CountAsync(); } ``` This approach gives you the count without fetching all the posts, saving memory and database round trips. --- ## **When to Avoid Explicit Includes** While Explicit Includes are fantastic, they’re not always the right choice. Here’s when to stick with regular `Include` statements: - **Small Datasets:** If you’re working with a small amount of data, loading everything upfront might be simpler and faster. - **One-Off Queries:** If you only need the related data once, a single query with `Include` might be more efficient. --- ## **Tips for Mastering Explicit Includes** - **Combine with AsNoTracking:** If you’re not modifying the data, use `.AsNoTracking()` to avoid unnecessary tracking overhead. - **Profile Your Queries:** Use tools like SQL Server Profiler or EF Core’s logging to see exactly what SQL is being executed. - **Don’t Nest Too Deeply:** Fetching deeply nested relationships explicitly can get tricky. Consider restructuring your query or simplifying your data model. --- ## **Wrap-Up: Fetch Smart, Not Hard** Explicit Includes give you the power to fetch data intentionally, avoiding the pitfalls of over-fetching and bloated queries. By loading related data only when you actually need it, you’ll keep your EF Core app running smoothly and efficiently. So, next time you build a query, remember: You don’t need to fetch the whole grocery store when all you want is a loaf of bread. Fetch smart, and let your app (and your database) thank you. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Split Queries: Stop the Data Traffic Jam in EF Core](https://www.woodruff.dev/split-queries-stop-the-data-traffic-jam-in-ef-core/) **Published:** January 28, 2025 **Author:** Chris Woodruff **Excerpt:** Picture this: You’re hosting a dinner party, and instead of serving everyone a delicious buffet, you deliver each dish one at a time to every guest. When dessert arrives, everyone’s too tired (or annoyed) to enjoy it. My friends, this is the problem with single queries in EF Core when they fetch complex relationships. Enter Split Queries, the life of your EF Core dinner party. **Content:** Picture this: You’re hosting a dinner party, and instead of serving everyone a delicious buffet, you deliver each dish one at a time to every guest. When dessert arrives, everyone’s too tired (or annoyed) to enjoy it. My friends, this is the problem with **single queries** in EF Core when they fetch complex relationships. Enter **Split Queries**, the life of your EF Core dinner party. Split Queries break down extensive, complex database queries into smaller, more manageable bites, ensuring smoother performance and happier apps. Let’s dive into what they are, why they matter, and when to put them on your EF Core menu. --- ## **What Are Split Queries?** EF Core often generates a single query with multiple joins when it fetches related data. This works fine for smaller datasets but quickly becomes a problem when: - You have deeply nested relationships. - The query results in massive Cartesian products. - Your database server starts crying for help. With Split Queries, EF Core splits the related data retrieval into multiple more minor queries instead of a mega-query, reducing memory overhead and improving performance. --- ## **A Quick Example** Here’s a classic scenario: Fetching blogs with their related posts. #### Without Split Queries (Single Query) ``` var blogs = await context.Blogs .Include(b => b.Posts) .ToListAsync(); ``` EF Core will generate one giant query with a `JOIN`, which can lead to redundant data and bloated results. #### With Split Queries ``` var blogs = await context.Blogs .Include(b => b.Posts) .AsSplitQuery() .ToListAsync(); ``` Boom! Now, EF Core generates two separate queries—one for the blogs and one for their posts—making it faster and less memory-hungry. --- ## **Why Use Split Queries?** Here’s why Split Queries are the unsung heroes of EF Core: 1. **Reduce Redundant Data** Joins in single queries often duplicate rows when fetching related data. Split Queries eliminate this duplication. 2. **Avoid Memory Overload** For large datasets, single queries can result in massive in-memory objects. Split Queries keep things lean and efficient. 3. **Simplify Query Execution** By breaking the query into smaller chunks, you lighten the load on your database server, leading to faster execution. 4. **Prevent Cartesian Explosion** If you’ve ever seen a query return thousands of rows when you expected ten, you know the pain. Split Queries minimize this risk. --- ## **When to Use Split Queries** Split Queries are fantastic, but they’re not a silver bullet. Here’s when to consider using them: - **Complex Relationships:** If you’re working with deeply nested data (`Include` on `Include` on `Include`), Split Queries can save the day. - **Large Datasets:** Fetching related data for thousands of records? Split it up to avoid overwhelming your app. - **Performance Troubleshooting:** If a single query is causing bottlenecks, switching to Split Queries might resolve the issue. --- ## **How to Enable Split Queries** It’s as easy as flipping a switch. Just add `.AsSplitQuery()` to your LINQ statement: ``` var blogs = await context.Blogs .Include(b => b.Posts) .AsSplitQuery() .ToListAsync(); ``` Want to make it the default behavior for your app? Add this to your `DbContext` configuration: ``` protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer("") .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); } ``` That’s it! EF Core will now default to Split Queries unless you specify otherwise. --- ## **Gotchas to Watch Out For** Before you go splitting all your queries, keep these in mind: - **Multiple Database Calls:** Split Queries make multiple trips to the database. While this is usually faster than processing a giant query, it might not always be true for small datasets. - **Not for Lazy Loaders:** Split Queries won’t magically optimize that behavior if you rely heavily on lazy loading. - **Explicit Configuration Needed:** EF Core will still use single queries if you don’t enable `.AsSplitQuery()` or set it as the default. --- ## **Single vs Split Queries: A Performance Tale** Let’s look at a quick comparison: #### Single Query: - **Pros:** Fewer trips to the database. - **Cons:** Can create massive results with duplicated data, leading to high memory usage. #### Split Queries: - **Pros:** Smaller, more efficient database calls; avoids duplication. - **Cons:** Multiple trips to the database could add latency for small datasets. --- ## **Wrap-Up: Split It, Don’t Quit It** Split Queries are like a well-organized road trip: smaller, planned stops make the journey smoother and more enjoyable for everyone involved. By breaking down complex data retrieval into manageable chunks, you’ll avoid memory overload, reduce redundant data, and keep your app running at peak performance. So, next time your EF Core queries feel like they’re taking the scenic (and slow) route, remember: sometimes, splitting up is the best way to stay together. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [FromSql: Writing SQL Like a Boss in EF Core](https://www.woodruff.dev/fromsql-writing-sql-like-a-boss-in-ef-core/) **Published:** January 27, 2025 **Author:** Chris Woodruff **Excerpt:** So, you’ve embraced Entity Framework Core, and life’s been good. No more handcrafting SQL for every little query. But what happens when EF Core’s LINQ magic isn’t entirely cutting it? Maybe you need something specific, like a stored procedure or a complex SQL query that LINQ doesn’t handle elegantly. That’s where FromSql swoops in to save the day. **Content:** So, you’ve embraced Entity Framework Core, and life’s been good. No more handcrafting SQL for every little query. But what happens when EF Core’s LINQ magic isn’t entirely cutting it? Maybe you need something specific, like a stored procedure or a complex SQL query that LINQ doesn’t handle elegantly. That’s where **FromSql** swoops in to save the day. FromSql lets you drop raw SQL into your EF Core queries and still reap the benefits of a strong ORM. It’s like having your cake (SQL) and eating it too (EF Core). Let’s dive into how this works and why it’s incredible. --- ## **What is FromSql?** In a nutshell, `FromSql` allows you to run raw SQL queries directly within your EF Core context. It’s perfect for those moments when: - You need a highly optimized query that EF Core can’t generate efficiently. - You’re working with legacy databases where specific SQL is a must. - You miss writing good ol’ SQL because it makes you feel like a database wizard. Here’s a quick example: ``` var city = "Redmond"; var customers = context.Customers .FromSql($"SELECT * FROM Customers WHERE City = {city}") .ToList(); ``` Boom. Straight SQL, fully supported in EF Core. --- ## **Why Use FromSql?** EF Core’s LINQ-to-SQL translation sometimes feels like taking the scenic route when you only want a shortcut. With `FromSql`, you control the exact SQL that’s executed, meaning you can: - Optimize for performance by crafting precise SQL. - Leverage SQL features that EF Core doesn’t fully support. - Impress your team with your epic SQL skills. --- ### **The Basics of FromSql** Using `FromSql` is as easy as pie. Here’s how you can get started: ### **Basic Query** Run a raw SQL query directly: ``` var customers = context.Customers .FromSql("SELECT * FROM Customers") .ToList(); ``` ### **Parameterized Queries** Always use parameters to avoid SQL injection. EF Core makes this simple: ``` var city = "Seattle"; var customers = context.Customers .FromSql($"SELECT * FROM Customers WHERE City = {city}") .ToList(); ``` EF Core will automatically handle parameterization, so you can write safe and secure queries without breaking a sweat. --- ## **Tips for Writing Optimized SQL** When using `FromSql`, you’re back in the driver’s seat for SQL optimization. Here are some tips to get the most out of it: ### **Select Only What You Need** Don’t select `*`. Be specific about the columns you need. This reduces data transfer and improves performance: ``` SELECT Id, Name, City FROM Customers WHERE City = @city ``` Reminder – You may break your app if you do not match the shape of your EF Core Entity Models when you query data using query projection. **Index Your Queries** Ensure your database has indexes that align with the fields you’re querying. This can drastically improve performance. **Batch Your Queries** When retrieving related data, try batching or joining in SQL rather than fetching row by row. --- ## **Advanced Scenarios with FromSql** Here’s where `FromSql` really shines: ### **Stored Procedures** Have some stored procedures lying around? No problem. You can call them with `FromSql`: ``` var artist = context.Artists .FromSql($"EXEC dbo.GetArtistDetails {artistId}") .FirstOrDefault(); ``` ### **Complex Joins and Filters** When LINQ starts looking like a maze of joins, switch to SQL for clarity and control: ``` var results = context.Orders .FromSql("SELECT o.Id, o.Total, c.Name FROM Orders o JOIN Customers c ON o.CustomerId = c.Id") .ToList(); ``` --- ## **Gotchas to Watch Out For** While `FromSql` is powerful, it does have a few quirks: - **Tracking Behavior** By default, results are tracked. If you’re using a read-only query, append `.AsNoTracking()` to save resources. - **Mapped Entities Only** `FromSql` works with entities that are already mapped to your DbContext. If you’re querying custom projections, you must map them or use raw ADO.NET. - **SQL Injection** Always use parameters (like `$` interpolation) to avoid SQL injection vulnerabilities. --- ## **Wrap-Up: Unleash the SQL Beast** `FromSql` bridges the gap between SQL’s raw power and EF Core’s convenience. It’s the perfect tool for those times when you need the precision of handcrafted SQL without ditching the ORM entirely. Whether you’re calling stored procedures, crafting efficient joins, or just reliving your SQL glory days, `FromSql` has got your back. So go ahead—start writing SQL like a boss and take your EF Core game to the next level. Your app (and your DBA) will love you for it. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Compiled Models: The Fast Lane for EF Core Performance](https://www.woodruff.dev/compiled-models-the-fast-lane-for-ef-core-performance/) **Published:** January 26, 2025 **Author:** Chris Woodruff **Excerpt:** Let’s talk about startup time. Not the Silicon Valley kind, but the time it takes for your EF Core app to boot up, stretch its legs, and actually start handling requests. If your app is dragging its feet like a teenager on a Monday morning, you need a secret weapon: Compiled Models. **Content:** Let’s talk about *startup time*. Not the Silicon Valley kind, but the time it takes for your EF Core app to boot up, stretch its legs, and actually start handling requests. If your app is dragging its feet like a teenager on a Monday morning, you need a secret weapon: **Compiled Models**. Think of compiled models as pre-built blueprints for your database schema that EF Core can load instantly, skipping all the tedious setup. It’s like meal-prepping your database models so your app can hit the ground running. --- ### **What Are Compiled Models, Anyway?** Typically, EF Core builds your model on the fly every time your app starts up. It figures out your entities, relationships, and configurations—basically, everything you’ve already told it in code. While it works, it’s not exactly speedy, especially for large or complex models. Compiled Models take that runtime work and shift it to **build time**. The result? EF Core already knows everything it needs, so it can skip the warm-up routine and start doing what it does best: handling your data like a champ. --- ### **How Much Faster Are We Talking?** Here’s a quick comparison: - **Without Compiled Models:** Your app spends precious milliseconds (or even seconds) building the model from scratch. - **With Compiled Models:** It’s like skipping the line at the amusement park. Your app starts faster, especially in scenarios with complex schemas or high-scale deployments. 🚀 And when your app is deployed across multiple instances (hello, microservices!), those time savings really add up. --- ### **Setting Up Compiled Models** Ready to give compiled models a spin? Here’s how to set it up in your EF Core project: ### **Install the Necessary Tools** You’ll need the EF Core CLI tools. If you don’t have them yet, install them with: ``` dotnet tool install --global dotnet-ef ``` ### **Generate the Compiled Model** Run the following command in your project directory: ``` dotnet ef dbcontext optimize --output-dir Models --namespace MyApp.Models ``` This creates a compiled model in the `Models` folder with all the database schema goodness baked in. ### **Use the Compiled Model in Your DbContext** Update your `DbContext` to load the compiled model: ``` protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseModel(MyApp.Models.MyCompiledModel.Instance) .UseSqlServer(""); } ``` That’s it! Your app is now cruising in the fast lane. --- ### **The Fine Print** Compiled models are amazing, but they come with a few “house rules”: - **Code Changes Require Regeneration** You’ll need to regenerate the compiled model if you update your entity configurations or add new ones. It’s a small price to pay for all that speed. - **No Lazy-Loading or Change-Tracking Proxies** Compiled models don’t support lazy-loading or change-tracking proxies, so make sure your app can live without them. - **Global Query Filters Are Out** You’ll need to revisit your strategy if you rely on global query filters because compiled models don’t play nice with them. --- ### **Why Use Compiled Models?** Here’s why compiled models are a game-changer: - **Blazing-Fast Startup:** Great for apps with complex schemas or high traffic. - **Consistency Across Instances:** Perfect for cloud deployments where every millisecond counts. - **Easier Performance Tuning:** Your model is pre-built, so you know precisely what EF Core works with. --- ### **Wrap-Up: Build It Once, Use It Always** Compiled Models are like meal-prepping for your EF Core app—put in a little effort up front, and you’ll save time every time your app starts up. Whether optimizing for the cloud or just looking to shave some seconds off your app’s startup, this tool is worth adding to your kit. So, grab your EF Core CLI and start compiling. Your app (and your users) will feel the difference. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, dotnet, Entity Framework Core, programming --- ### [Building with .NET and Rust: A Tale of Two Ecosystems](https://www.woodruff.dev/building-with-net-and-rust-a-tale-of-two-ecosystems/) **Published:** January 21, 2025 **Author:** Chris Woodruff **Excerpt:** Programming ecosystems are like bustling cities. .NET is the metropolis with skyscrapers and a thriving business district, while Rust is the up-and-coming tech hub—small but mighty, with startups everywhere. Both offer incredible opportunities, but their vibes are very different. So, let’s grab a coffee and tour these two dynamic ecosystems! **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rus](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: [Memory Wars: Garbage Collection in C# vs. Ownership in Rust](https://woodruff.dev/memory-wars-garbage-collection-in-c-vs-ownership-in-rust/) - Part 5: [Threads, Tasks, and Ownership: C# and Rust Concurrency Explored](https://woodruff.dev/threads-tasks-and-ownership-c-and-rust-concurrency-explored/) - Part 6: ***Building with .NET and Rust: A Tale of Two Ecosystems*** - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts Programming ecosystems are like bustling cities. .NET is the metropolis with skyscrapers and a thriving business district, while Rust is the up-and-coming tech hub—small but mighty, with startups everywhere. Both offer incredible opportunities, but their vibes are very different. So, let’s grab a coffee and tour these two dynamic ecosystems! ### .NET: The Established Powerhouse .NET is the seasoned pro of the programming world. Built by Microsoft, it’s a massive, well-maintained ecosystem with everything you need to build enterprise-grade software. **Why Developers Love .NET** - **Comprehensive Framework:** From desktop apps to web APIs, cloud services, and mobile development with Xamarin/Maui, .NET has tools for every job. - **.NET/C# IDEs:** There are many IDEs to choose from, such as Visual Studio, Rider, and VSCode, to name a few. Whether you’re debugging, refactoring, or deploying, these IDEs make life easier. - **NuGet:** With over 300,000 packages, NuGet ensures you’ll always find the libraries you need, whether it’s for logging, testing, or creating charts. - **Azure Integration:** Unsurprisingly, .NET plays beautifully with Azure, Microsoft’s cloud platform. Scaling apps, adding AI, or leveraging serverless functions? No problem. **Highlights** - Strong enterprise support and a mature ecosystem. - Fantastic tooling with Visual Studio and Rider. - Frameworks like ASP.NET Core for web development and Entity Framework for ORM. **Sample Workflow** ``` // Building a web API with ASP.NET Core var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello, .NET World!"); app.Run(); ``` ### Rust: The New Kid on the Block Rust may be younger, but it’s no slouch. Built for performance and safety, Rust is a favorite among developers tackling low-level programming, WebAssembly, and more. **Why Developers Love Rust** - **Crates.io:** Think NuGet but Rust-flavored. Crates.io is packed with libraries (or “crates”) for everything from web development to cryptography. - **Tooling:** Cargo, Rust’s package manager and build tool, is beloved for its simplicity. It handles dependencies, compiles your code, and even runs your tests. - **Safety and Speed:** Rust’s memory safety guarantees and zero-cost abstractions make it perfect for high-performance, secure applications. - **WebAssembly Leadership:** Rust is a top choice for writing WebAssembly modules, opening up exciting possibilities in the browser. - **RustRover:** A great IDE built for Rust developers. [Try RustRover](https://www.jetbrains.com/rust/) **Highlights** - A growing but passionate community. - Excellent support for modern paradigms like async programming. - Frameworks like Rocket, Actix, and Warp for web development. **Sample Workflow** ``` // Building a web server with Rocket #[macro_use] extern crate rocket; #[get("/")] fn index() -> &'static str { "Hello, Rust World!" } #[launch] fn rocket() -> _ { rocket::build().mount("/", routes![index]) } ``` ### Ecosystem Head-to-Head Feature.NETRust**Best For**Enterprise apps, cloud servicesSystem-level programming, WebAssembly**Package Manager**NuGetCargo**Tooling**Visual Studio, Rider, CLI toolsCargo, rust-analyzer**Web Frameworks**ASP.NET Core, BlazorRocket, Actix, Warp**Community**Large, corporate-backedSmall but fiercely passionate**Memory Safety**Managed by garbage collectorEnforced at compile time### Picking Your City If you’re building enterprise-level applications or need seamless integration with cloud services, .NET’s ecosystem is hard to beat. It’s mature, reliable, and has all the bells and whistles you want. On the other hand, Rust’s ecosystem is ideal for performance-critical tasks and cutting-edge technologies like WebAssembly. It’s compact but rapidly growing, and the focus on safety means fewer late-night debugging sessions. ### Final Thoughts .NET and Rust ecosystems each have their charm. .NET feels like a dependable, well-paved city with a solution for every problem. Rust is a scrappy, innovative startup town buzzing with energy and new ideas. Why not explore both? The more ecosystems you know, the more tools you have in your development arsenal. So, are you sticking with .NET’s polished skyscrapers, or are you ready to explore Rust’s exciting frontiers? Let us know! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Level Up Your Skills: Learning Rust as a C# Dev](https://www.woodruff.dev/level-up-your-skills-learning-rust-as-a-c-dev/) **Published:** January 23, 2025 **Author:** Chris Woodruff **Excerpt:** Switching from C# to Rust can feel like stepping into a new world. Suddenly, you’re dealing with ownership, borrowing, and a compiler that’s as strict as your high school math teacher. But don’t worry—you’ve got this. With the right mindset, some practical tips, and a few helpful resources, you’ll go from a Rust rookie to a Rustacean in no time. Let’s dive in! **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rus](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: [Memory Wars: Garbage Collection in C# vs. Ownership in Rust](https://woodruff.dev/memory-wars-garbage-collection-in-c-vs-ownership-in-rust/) - Part 5: [Threads, Tasks, and Ownership: C# and Rust Concurrency Explored](https://woodruff.dev/threads-tasks-and-ownership-c-and-rust-concurrency-explored/) - Part 6: [Building with .NET and Rust: A Tale of Two Ecosystems](https://woodruff.dev/building-with-net-and-rust-a-tale-of-two-ecosystems/) - Part 7: ***Level Up Your Skills: Learning Rust as a C# Dev*** - Part 8: Rust’s Superpower: Speed Meets Smarts Switching from C# to Rust can feel like stepping into a new world. Suddenly, you’re dealing with ownership, borrowing, and a compiler that’s as strict as your high school math teacher. But don’t worry—you’ve got this. With the right mindset, some practical tips, and a few helpful resources, you’ll go from a Rust rookie to a Rustacean in no time. Let’s dive in! ### Mindset Matters: Embrace the Challenge Before we jump into the tips, let’s talk about the mindset. Rust isn’t just a language; it’s a different way of thinking about programming. You’ll encounter concepts like ownership and lifetimes that might initially feel like hurdles. Instead of fighting them, embrace them as part of Rust’s charm. Every compile-time error is a learning opportunity—and trust me, you’ll see a lot of them at first. ### Practical Tips for Getting Started Here are some battle-tested tips to help you on your Rust journey: - **Start Small:** Begin with simple projects. Try implementing familiar algorithms or small command-line tools. This helps you get comfortable with Rust’s syntax and concepts without feeling overwhelmed. - **Understand Ownership:** Rust’s ownership model is its crown jewel, but it can be tricky at first. Think of it like passing the keys to a car—whoever has the keys controls the car. Once you’ve got the hang of it, everything else falls into place. - **Borrowing and Lifetimes:** Borrowing lets you access data without taking ownership. Lifetimes ensure references are valid. Start simple, and don’t hesitate to revisit concepts as you progress. - **Leverage** `Cargo`**:** Cargo is Rust’s build system and package manager. It’s your best friend for creating projects, managing dependencies, and running tests: ``` cargo new my_project cd my_project cargo run ``` - **Experiment with** `std`**:** Rust’s standard library (`std`) is packed with utilities. Explore collections, error handling, and threading modules to see what’s available. - **Don’t Fear the Borrow Checker:** Rust’s borrow checker can feel intimidating. Instead of fighting it, try to understand what it’s trying to tell you. The compiler’s error messages are usually detailed and helpful. - **Write Tests:** Rust makes it easy to write tests. Start with simple `#[test]` functions and expand from there: ``` #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } ``` ### Resources to Kickstart Your Rust Journey Here are some must-visit resources for learning Rust: 1. **The Rust Book:** Officially titled *The Rust Programming Language*, this is the ultimate guide for beginners. It’s free and available online: [Rust Book](https://doc.rust-lang.org/book/). 2. **Rustlings:** Rustlings is a series of small exercises that help you learn Rust concepts interactively. It’s like a coding dojo for Rust. [Get Rustlings](https://github.com/rust-lang/rustlings) 3. **Crates.io:** Dive into Rust’s package registry to explore libraries and dependencies. [Visit Crates.io](https://crates.io/) 4. **Rust Playground:** Play around with Rust code in your browser. It’s perfect for quick experiments. [Try it here](https://play.rust-lang.org/) 5. **Community:** Join the Rust community on forums, Discord, and Reddit. Rustaceans are known for being welcoming and helpful. 6. **Videos and Tutorials:** Look for YouTube channels and tutorials tailored to C# developers transitioning to Rust. Practical examples make the concepts stick. 7. **RustRover:** A great IDE made especially for Rust development. [Try RustRover](https://www.jetbrains.com/rust/) ### Final Thoughts Learning Rust as a C# developer is an adventure. It’s not just about picking up another syntax—it’s about rethinking how you write software. While the road might feel steep at first, the payoff is huge. Rust’s focus on safety, performance, and modern programming paradigms makes it an invaluable addition to your toolkit. So, grab your editor, fire up `cargo`, and start your Rust journey today. And remember, every Rustacean started where you are now—you’re in good company. Happy coding! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [DbContext Pooling: The Secret Sauce to Faster EF Core Apps](https://www.woodruff.dev/dbcontext-pooling-the-secret-sauce-to-faster-ef-core-apps/) **Published:** January 25, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re running a restaurant. Every time a customer orders, you buy a brand-new frying pan, use it once, and toss it out. Sounds absurd, right? But that’s essentially what happens in EF Core when you create a new DbContext for every request. It’s wasteful, slow, and totally unnecessary. Enter DbContext Pooling: the genius way to reuse those frying pans—err, contexts—and turbocharge your app’s performance. **Content:** Imagine you’re running a restaurant. Every time a customer orders, you buy a brand-new frying pan, use it once, and toss it out. Sounds absurd, right? But that’s essentially what happens in EF Core when you create a new DbContext for every request. It’s wasteful, slow, and totally unnecessary. Enter **DbContext Pooling**: the genius way to reuse those frying pans—err, contexts—and turbocharge your app’s performance. ## **What Is DbContext Pooling?** Think of DbContext Pooling as a VIP lounge for your DbContext instances. Instead of creating and disposing of a new DbContext for every request, EF Core prepares a pool of pre-configured DbContext objects. When a request comes in, it grabs one from the pool, uses it, and then returns it back like a good citizen. This prevents your app from wasting precious resources by repeatedly creating and destroying objects. The result? You get significantly faster performance and fewer headaches. ## **How Does It Work?** Here’s the magic in action: 1. **Without Pooling** - Every request spins up a shiny new DbContext. If your app gets thousands of requests, thousands of DbContext objects will be created and destroyed. Yikes! - ***Performance snapshot:*** - Total contexts created: 64,637 - Requests per second: 6,395 2. **With Pooling** - By pooling, EF Core reuses the same DbContext instances whenever possible. Fewer object creations mean less garbage collection and better throughput. - ***Performance snapshot:*** - Total contexts created: 32 - Requests per second: 15,714 That’s more than **double the throughput**! ## **How to Add DbContext Pooling to Your App** Ready to join the pooling party? Here’s how you can set it up in just a few lines of code: ``` public void ConfigureServices(IServiceCollection services) {     services.AddDbContextPool(options =>         options.UseSqlServer("YourConnectionString")); } ``` That’s it! You’ve just unlocked a faster, more efficient app with minimal effort. ## **Things to Watch Out For** Before you dive headfirst into pooling, here are a few things to keep in mind: ### **No Scoped Dependencies** Your DbContext should be stateless, meaning it shouldn’t hold any references to services or objects with scoped lifetimes. Otherwise, you’ll run into bizarre bugs. ### **Reset Your State** EF Core automatically resets the state of a pooled DbContext, but if you’re doing something fancy (like tracking lots of entities), make sure you don’t leave any messes behind. ### **Know Your Limits** While pooling works wonders for most apps, it’s not a one-size-fits-all solution. Test your app’s performance and see if pooling delivers your needed boost. ## **Why You’ll Love DbContext Pooling** DbContext Pooling is like upgrading your old bike to a sports car. It’s simple, efficient, and gives you more speed with less effort. Whether building blazing-fast APIs or high-throughput web apps, pooling ensures your EF Core app runs like a well-oiled machine. So, what are you waiting for? Add DbContext Pooling to your app today and feel the performance boost. Trust me—your app (and your users) will thank you. **Categories:** Entity Framework Core **Tags:** .NET, C#, Data, databases, Entity Framework Core, programming --- ### [Announcing My New Book: htmx Essentials for ASP.NET Core Developers](https://www.woodruff.dev/announcing-my-new-book-htmx-essentials-for-asp-net-core-developers/) **Published:** January 24, 2025 **Author:** Chris Woodruff **Excerpt:** As a developer, you know the web development landscape constantly evolves, and staying ahead means embracing tools and practices that simplify and enhance how we build applications. That's why I'm thrilled to announce my latest project: "htmx Essentials for ASP.NET Core Developers." **Content:** As a developer, you know the web development landscape constantly evolves, and staying ahead means embracing tools and practices that simplify and enhance how we build applications. That’s why I’m thrilled to announce my latest project: **“htmx Essentials for ASP.NET Core Developers.”** This book is your comprehensive guide to mastering dynamic, server-side interactions with Razor Pages using the powerful **htmx** library. With htmx, you can simplify complex client-side logic, create seamless user experiences, and maximize the potential of ASP.NET Core Razor Pages—without needing heavy JavaScript frameworks. ### What Is htmx? If you’re new to htmx, it’s a lightweight library that lets you extend HTML with powerful capabilities like: - Making AJAX requests directly in your markup using attributes like hx-get and hx-post. - Dynamically swapping content without reloading the entire page. - Triggering actions based on user interactions using hx-trigger. - Simplifying navigation and state management with history support. All this is done while keeping your application’s focus on server-side development, enabling better performance and maintainability. ### What’s Inside the Book? **“htmx Essentials for ASP.NET Core Developers”** is structured to take you from the fundamentals to advanced techniques, with plenty of practical examples along the way. Here’s a glimpse of what the book covers: - **Setting Up Your Environment**: Get started with ASP.NET Core 9 Razor Pages and htmx. - **Core Concepts**: Learn how to use hx-get, hx-post, and other commands to fetch and update content dynamically. - **Building Blocks**: Explore forms, modal dialogs, tables, tabs, and more, all powered by htmx. - **Advanced Interactivity**: Dive into triggers, custom events, and Hyperscript to take your apps to the next level. - **User Experience**: Enhance usability with loading indicators, out-of-band updates, and real-time validation. - **Performance Optimization**: Learn techniques to optimize server-side performance and reduce client-side load. - **Real-World Scenarios**: Build dashboards, e-commerce apps, and even chat applications using htmx and Razor Pages. - **Deployment and Maintenance**: Debug, test, and deploy htmx applications using best practices for Azure hosting. Each chapter is designed to help you solve real-world problems and provide actionable insights that you can immediately apply to your projects. ### A Unique Way to Learn: Chapters Released Online as I Write One of the most exciting aspects of this book is that I’m **releasing it chapter by chapter online** at as I write it. This approach gives you early access to the content, allows you to follow along in real time, and provides a platform for you to provide feedback, ask questions, and shape the book’s direction as it evolves. This interactive process ensures that the book is tailored to your needs and provides a unique learning experience. When the book is fully written, it will be available as an **ebook**, providing you with a complete resource for htmx and ASP.NET Core development. ### Why This Book Matters The shift toward server-side interactivity is more than a trend—it’s a practical response to the growing complexity of modern web development. htmx empowers developers to: - Simplify development by keeping logic server-side. - Reduce the overhead of maintaining heavy front-end frameworks. - Create performant, accessible, and maintainable web applications. This book focuses on all these benefits, showing you how to harness the power of htmx while leveraging the robust features of ASP.NET Core Razor Pages. ### Join Me on This Journey Whether you’re a seasoned developer or just starting with ASP.NET Core, this book will have something for you. Following along with the chapters as they’re released will give you early insights, practical examples, and a deeper understanding of how to use htmx effectively in your projects. Visit today to explore the chapter outline. Let’s build better, faster, and more intuitive web applications together—one chapter at a time! If you have questions or feedback or just want to share your excitement, drop a comment below or contact me. I can’t wait to hear what you think. **Categories:** Blog **Tags:** .NET, asp.net, Book, C#, htmx, programming, web development --- ### [Rust's Superpower: Speed Meets Smarts](https://www.woodruff.dev/rusts-superpower-speed-meets-smarts/) **Published:** January 24, 2025 **Author:** Chris Woodruff **Excerpt:** Regarding speed and efficiency, Rust doesn’t just run the race—it leaves other languages eating its dust. Built for blazing performance and low-level control, Rust’s unique features make it a standout choice for projects where every millisecond and megabyte count. Let’s dive into why Rust is the Usain Bolt of programming languages and how it stacks up in the performance department. **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rus](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: [Memory Wars: Garbage Collection in C# vs. Ownership in Rust](https://woodruff.dev/memory-wars-garbage-collection-in-c-vs-ownership-in-rust/) - Part 5: [Threads, Tasks, and Ownership: C# and Rust Concurrency Explored](https://woodruff.dev/threads-tasks-and-ownership-c-and-rust-concurrency-explored/) - Part 6: [Building with .NET and Rust: A Tale of Two Ecosystems](https://woodruff.dev/building-with-net-and-rust-a-tale-of-two-ecosystems/) - Part 7: [Level Up Your Skills: Learning Rust as a C# Dev](https://woodruff.dev/level-up-your-skills-learning-rust-as-a-c-dev/) - Part 8: ***Rust’s Superpower: Speed Meets Smarts*** Regarding speed and efficiency, Rust doesn’t just run the race—it leaves other languages eating its dust. Built for blazing performance and low-level control, Rust’s unique features make it a standout choice for projects where every millisecond and megabyte count. Let’s dive into why Rust is the Usain Bolt of programming languages and how it stacks up in the performance department. ### Why Rust is Fast Rust’s performance comes down to its core principles. Here are the big hitters: 1. **Zero-Cost Abstractions:** Rust’s abstractions don’t come with a runtime cost. When you use high-level features like iterators or pattern matching, they compile down to efficient machine code. You get modern programming convenience without sacrificing speed. 2. **Manual Memory Management, Automatically Safe:** Unlike C++, where you manually manage memory or C#, where the garbage collector (GC) handles it for you, Rust ensures memory safety without runtime overhead. Its ownership system makes sure resources are freed as soon as they’re no longer needed—no GC pauses, no dangling pointers. 3. **Concurrency Without Fear:** Rust’s compiler enforces thread safety at compile time. This lets you write concurrent code that’s efficient and safe without worrying about data races. ### Benchmarks Don’t Lie Rust consistently scores high in performance benchmarks, especially in areas like: - **File Parsing:** Parsing files in Rust can be lightning-fast due to its memory management model and zero-cost abstractions. - **Networking:** Thanks to frameworks like Tokio and hyper, Rust’s async programming model rivals the performance of C. - **Cryptography:** Rust’s low-level control makes it a popular choice for cryptographic libraries and secure systems. #### Example Benchmark: Fibonacci Let’s look at a simple benchmark—calculating Fibonacci numbers recursively: **C#:** ``` using System; class Program { static void Main() { Console.WriteLine(Fib(30)); } static int Fib(int n) { if (n u32 { if n **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [From C# to Rust: A Journey Through Code and Concepts](https://www.woodruff.dev/from-c-to-rust-a-journey-through-code-and-concepts/) **Published:** January 16, 2025 **Author:** Chris Woodruff **Excerpt:** Welcome to my epic blog series, where we pit C# against Rust in a friendly, code-slinging showdown! If you’re a C# developer curious about Rust’s quirks and power or a Rustacean interested in how your language compares to the polished .NET ecosystem, you’ve come to the right place. **Content:** Welcome to my epic blog series, where we pit C# against Rust in a friendly, code-slinging showdown! If you’re a C# developer curious about Rust’s quirks and power or a Rustacean interested in how your language compares to the polished .NET ecosystem, you’ve come to the right place. This series is like a guided tour of two amazing programming worlds, filled with insights, practical tips, and just the right amount of nerdy humor. Ready to dive in? Here’s what we’ve got lined up for you: ### 1. **Why Every C# Developer Should Explore Rust** We kick things off with a look at why Rust might be worth your time. From performance perks to fearless concurrency, we highlight what makes Rust a standout choice for your next coding adventure. ### 2. **Exploring Programming Paradigms: C# and Rust Side by Side** C# and Rust each bring their own unique flair to object-oriented and functional programming. We’ll compare their paradigms and show you how each language approaches common programming problems. ### 3. **Syntax Smackdown: Comparing Constructs in C# and Rust** Syntax matters. In this post, we’ll walk through variables, functions, loops, and conditionals, showcasing the similarities and differences in how you write code in these two languages. ### 4. **Memory Wars: Garbage Collection in C# vs. Ownership in Rust** Memory management is where Rust shines brightest. We’ll explore how C# relies on garbage collection and how Rust’s ownership model ensures safety without runtime overhead. ### 5. **Threads, Tasks, and Ownership: C# and Rust Concurrency Explored** Threads, tasks, and data races—oh my! We’ll compare how these two languages handle concurrency and why Rust’s fearless approach is a game-changer for safe, efficient multi-threading. ### 6. **Building with .NET and Rust: A Tale of Two Ecosystems** .NET is the established powerhouse, while Rust is the scrappy upstart. We’ll explore the tools, libraries, and communities that make these ecosystems tick. ### 7. **Level Up Your Skills: Learning Rust as a C# Dev** Making the jump from C# to Rust? This post is packed with tips, tricks, and resources to make the transition smoother. From conquering the borrow checker to mastering Cargo, we’ve got you covered. ### 8. **Rust’s Superpower: Speed Meets Smarts** Rust is fast, and we mean *fast*. We’ll look at benchmarks and real-world examples that showcase Rust’s unmatched performance and efficiency compared to traditional garbage-collected languages. ### Why This Series? Programming languages are like tools in a toolbox—the more you know, the more problems you can solve. By exploring the strengths and differences between C# and Rust, you’ll expand your skillset and gain a deeper appreciation for what makes each language unique. Whether you’re curious about Rust’s growing popularity, looking to level up your skills, or just here for the nerdy comparisons, this series has something for everyone. So buckle up, and let’s get started on this coding adventure. **Categories:** Rust **Tags:** .NET, C#, programming, rust --- ### [Why Every C# Developer Should Explore Rust](https://www.woodruff.dev/why-every-c-developer-should-explore-rust/) **Published:** January 17, 2025 **Author:** Chris Woodruff **Excerpt:** Hey there, C# developers! If you’re reading this, you’re probably curious about Rust, that trendy programming language everyone’s been talking about. Maybe you’ve heard whispers of “memory safety” or “no garbage collector” and wondered, “What’s the big deal? Can’t I stick with my trusty .NET stack?” Well, let’s dive in and explore why you, a C# maestro, might want to take a detour into Rust-land. **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: ***Why Every C# Developer Should Explore Rust*** - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rust](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: Memory Wars: Garbage Collection in C# vs. Ownership in Rust - Part 5: Threads, Tasks, and Ownership: C# and Rust Concurrency Explored - Part 6: Building with .NET and Rust: A Tale of Two Ecosystems - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts Hey there, C# developers! If you’re reading this, you’re probably curious about Rust, that trendy programming language everyone’s been talking about. Maybe you’ve heard whispers of “memory safety” or “no garbage collector” and wondered, “What’s the big deal? Can’t I stick with my trusty .NET stack?” Well, let’s dive in and explore why you, a C# maestro, might want to take a detour into Rust-land. ### Why Rust? Rust is like that indie band you didn’t know you needed in your playlist. It’s powerful, efficient, and has some fresh takes on how programming should be done. While C# has been your reliable rock band—solid, polished, and dependable—Rust comes in with a new vibe, ready to shake things up. Here’s why you should pay attention: - **Performance Boost:** Rust is designed for speed. Unlike C#, which relies on a garbage collector to manage memory, Rust uses an ownership model. That means your code can run faster and use less memory—great for systems programming, game development, or handling massive data crunching. - **Memory Safety Without Runtime Overhead:** Rust prevents common bugs like null pointer dereferencing and buffer overflows at compile time. Think of it as having a super strict code review buddy who never lets anything unsafe through. - **Growing Ecosystem:** Rust is making waves in areas like WebAssembly, embedded systems, and cloud computing. The community’s passionate, and the tooling keeps getting better. Crates.io (Rust’s package registry) is overflowing with goodies to make your life easier. ### But I Love C#! I get it—C# is fantastic. Its integration with the .NET ecosystem, the power of LINQ, and the elegance of async/await make it a fantastic choice for web apps, Windows apps, and more. But no language is perfect for every use case. Here are some areas where Rust can complement your C# expertise: 1. **Systems-Level Programming:** Need to write code that interacts directly with hardware or the OS? Rust’s low-level capabilities make it a go-to choice for building operating systems, embedded systems, or high-performance networking tools. 2. **WebAssembly:** Rust is a superstar in the WebAssembly space, letting you build blazing-fast web apps. Imagine combining your ASP.NET Core backend with a Rust-powered WebAssembly frontend. That’s a power couple! 3. **Concurrency Done Right:** Rust’s ownership model ensures that your concurrent code is safe and free of data races. While C# offers tools like locks and tasks, Rust’s compile-time guarantees take it to the next level. ### The Learning Curve Let’s be honest: Rust isn’t the most straightforward language to pick up. Its strict compiler can feel like a harsh teacher at first. But think back to when you first tackled async/await in C#. It was a bit of a brain teaser, right? Now, it’s second nature. Rust’s ownership model and borrowing rules are similar—challenging at first but immensely rewarding once you get the hang of them. ### Where to Start Ready to dip your toes in? Here are a few ways to begin your Rust adventure: - **Start with the Basics:** Head to The Rust Book, the ultimate beginner’s guide. - **Try Rust for Web Dev:** Check out frameworks like Actix or Rocket to see how Rust handles web apps. - **Port a Small Project:** Take a simple C# project and try rebuilding it in Rust. You’ll learn a ton! - **Join the Community:** The Rustaceans (yep, that’s what Rust devs call themselves) are super welcoming. Hop into forums, Discords, or even on social media to connect with others. ### Final Thoughts Rust isn’t here to replace C#. It’s here to complement it. As a developer, expanding your toolbox with languages like Rust can open up new opportunities and make you a more versatile problem solver. So, why not give it a shot? Who knows—you might just fall in love with its quirky charm and no-nonsense approach to programming. Now, go forth and Rust-ify your world, one line of code at a time. Happy coding! **Categories:** Rust **Tags:** .NET, C#, programming, rust --- ### [Exploring Programming Paradigms: C# and Rust Side by Side](https://www.woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) **Published:** January 18, 2025 **Author:** Chris Woodruff **Excerpt:** When it comes to programming languages, each has its quirks, strengths, and unique ways of looking at the world. Think of them as different superheroes with distinct powers. C# is your reliable, well-rounded champion of object-oriented programming (OOP), while Rust is the fearless defender of memory safety and performance. So, how do these two stack up when it comes to their paradigms? Let’s find out! **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: ***Exploring Programming Paradigms: C# and Rust Side by Side*** - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rust](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: Memory Wars: Garbage Collection in C# vs. Ownership in Rust - Part 5: Threads, Tasks, and Ownership: C# and Rust Concurrency Explored - Part 6: Building with .NET and Rust: A Tale of Two Ecosystems - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts When it comes to programming languages, each has its quirks, strengths, and unique ways of looking at the world. Think of them as different superheroes with distinct powers. C# is your reliable, well-rounded champion of object-oriented programming (OOP), while Rust is the fearless defender of memory safety and performance. So, how do these two stack up when it comes to their paradigms? Let’s find out! ### Object-Oriented Programming (OOP) **C#: The Classic Hero of OOP** C# thrives in the OOP world. With its rich support for classes, inheritance, and interfaces, it’s designed to encapsulate data and behavior neatly. Need a blueprint for an object? C# has you covered. Here’s a quick example: ``` public class Car { public string Make { get; set; } public string Model { get; set; } public Car(string make, string model) { Make = make; Model = model; } public void Drive() { Console.WriteLine($"Driving a {Make} {Model}!"); } } ``` With inheritance, polymorphism, and interfaces, C# makes OOP feel natural and intuitive. It’s like driving a car with all the modern bells and whistles—smooth and predictable. **Rust: OOP, But With a Twist** Rust supports OOP principles, but it’s not your typical OOP language. Forget traditional classes and inheritance—Rust does things its own way using structs and traits. Here’s a taste of how it works: ``` struct Car { make: String, model: String, } impl Car { fn new(make: &str, model: &str) -> Self { Car { make: make.to_string(), model: model.to_string(), } } fn drive(&self) { println!("Driving a {} {}!", self.make, self.model); } } ``` Rust emphasizes composition over inheritance. Traits define shared behaviors that structs can implement, giving you a flexible yet powerful way to design your code. It’s like driving a manual transmission—you have more control but need to pay closer attention. ### Functional Programming (FP) Both C# and Rust dabble in functional programming but take different approaches. **C#: LINQ-ing It Up** C# shines with tools like LINQ, lambda expressions, and higher-order functions. These make it easy to manipulate collections and embrace FP concepts: ``` var numbers = new[] { 1, 2, 3, 4, 5 }; var squares = numbers.Select(n => n * n); foreach (var square in squares) { Console.WriteLine(square); } ``` With LINQ, functional programming feels right at home in C#. **Rust: Immutability and Pattern Matching** Rust leans heavily into immutability and pattern matching. It’s FP with a focus on safety and clarity: ``` let numbers = vec![1, 2, 3, 4, 5]; let squares: Vec = numbers.iter().map(|n| n * n).collect(); for square in squares { println!("{}", square); } ``` Rust’s approach to FP feels deliberate and robust. Pattern matching, in particular, is a joy to use and makes the code expressive and concise. ### Concurrency: The Final Frontier **C#: Threads, Tasks, and Async/Await** Concurrency in C# is all about tasks and async/await. It’s straightforward and powerful: ``` async Task FetchDataAsync() { Console.WriteLine("Fetching data..."); await Task.Delay(1000); Console.WriteLine("Data fetched!"); } ``` C# makes asynchronous programming approachable, but you must watch out for pitfalls like deadlocks or race conditions. **Rust: Fearless Concurrency** Rust’s concurrency model is built on its ownership system. It ensures data races are caught at compile time. Here’s a simple example: ``` use std::thread; let handle = thread::spawn(|| { println!("Hello from a thread!"); }); handle.join().unwrap(); ``` With traits like `Send` and `Sync`, Rust makes sure your code is thread-safe by design. It’s like having a seatbelt that won’t let you start the car until everything is secure. ### The Verdict C# and Rust approach paradigms differently, but both are incredible tools for the right job. C# excels in enterprise applications, where OOP and productivity shine. On the other hand, Rust thrives in performance-critical, system-level programming with its unique take on safety and control. So, why not have both in your toolbox? Each language can teach you new ways to think about programming—and that’s always a win. What do you think? Are you ready to embrace Rust’s quirks and join the fearless programming revolution? Let us know in the comments below! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Syntax Showdown: A Look at Common Constructs in C# and Rust](https://www.woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) **Published:** January 19, 2025 **Author:** Chris Woodruff **Excerpt:** Programming languages are like different cuisines. Some stick to comfort and tradition, like C# with its familiar object-oriented flavors, while others, like Rust, spice things up with bold new ingredients like ownership and borrowing. In this post, we’ll compare the “syntax recipes” for common constructs in C# and Rust, highlighting the delicious differences and similarities along the way. **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: ***Syntax Smackdown: Comparing Constructs in C# and Rus*** - Part 4: Memory Wars: Garbage Collection in C# vs. Ownership in Rust - Part 5: Threads, Tasks, and Ownership: C# and Rust Concurrency Explored - Part 6: Building with .NET and Rust: A Tale of Two Ecosystems - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts Programming languages are like different cuisines. Some stick to comfort and tradition, like C# with its familiar object-oriented flavors, while others, like Rust, spice things up with bold new ingredients like ownership and borrowing. In this post, we’ll compare the “syntax recipes” for common constructs in C# and Rust, highlighting the delicious differences and similarities along the way. ### Variable Declarations: Setting the Stage **C#: Explicit, with a Touch of Magic** C# likes to keep things straightforward—most of the time. You declare variables with clear types, or if you’re feeling fancy, use `var` for type inference: ``` int number = 10; string message = "Hello, World!"; bool isActive = true; var inferred = 42; // Compiler figures out it's an int ``` **Rust: Explicit by Default** Rust is all about clarity and safety. You’ll explicitly declare types when needed, though the compiler’s type inference is pretty smart: ``` let number: i32 = 10; let message: &str = "Hello, World!"; let is_active: bool = true; let inferred = 42; // Compiler infers it's an i32 ``` Bonus points: Rust’s variables are immutable by default. To make them mutable, you add a little `mut`: ``` let mut counter = 0; counter += 1; // Now it's allowed ``` ### Functions: Getting Things Done **C#: Polished and Practical** C# functions look sleek and professional. You declare a return type, name your function, and jump right into the action: ``` int Add(int a, int b) { return a + b; } string Greet(string name) { return $"Hello, {name}!"; } ``` **Rust: Concise and Composable** Rust’s functions are compact and often omit the return keyword for simplicity: ``` fn add(a: i32, b: i32) -> i32 { a + b // Implicit return (no semicolon means "return") } fn greet(name: &str) -> String { format!("Hello, {}!", name) } ``` ### Conditionals: Making Decisions **C#: Classic Control Flow** C# sticks to the tried-and-true `if-else` format: ``` int number = 10; if (number > 5) { Console.WriteLine("Number is greater than 5"); } else { Console.WriteLine("Number is 5 or less"); } ``` **Rust: Lean and Expressive** Rust keeps conditionals clean and concise, with a slight twist—`if` is an expression, so it can return values: ``` let number = 10; if number > 5 { println!("Number is greater than 5"); } else { println!("Number is 5 or less"); } ``` Or, returning a value from `if`: ``` let message = if number > 5 { "Greater than 5" } else { "5 or less" }; println!("{}", message); ``` ### Loops: Repeating the Fun **C#: Iteration Made Easy** C# has all the classic looping constructs: ``` for (int i = 0; i < 5; i++) { Console.WriteLine($"Iteration {i}"); } int count = 0; while (count < 5) { Console.WriteLine($"Count {count}"); count++; } ``` **Rust: Loops with a Twist** Rust’s looping syntax is simple, with `for` loops often taking center stage: ``` for i in 0..5 { println!("Iteration {}", i); } let mut count = 0; while count < 5 { println!("Count {}", count); count += 1; } ``` And, for infinite loops, Rust has a dedicated keyword: ``` loop { println!("This runs forever unless you break it"); break; // Escape hatch } ``` ### Error Handling: Keeping It Safe **C#: Try and Catch** C# uses exceptions to handle errors. It’s straightforward and familiar: ``` try { int result = 10 / 0; } catch (DivideByZeroException e) { Console.WriteLine("Cannot divide by zero"); } ``` **Rust: Results That Mean Business** Rust avoids exceptions in favor of `Result` and `Option`, forcing you to handle errors explicitly: ``` fn divide(a: i32, b: i32) -> Result **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Memory Wars: Garbage Collection in C# vs. Ownership in Rust](https://www.woodruff.dev/memory-wars-garbage-collection-in-c-vs-ownership-in-rust/) **Published:** January 20, 2025 **Author:** Chris Woodruff **Excerpt:** Regarding memory management, programming languages take different approaches to ensure your applications don’t crash and burn. Think of it as cleaning up after a party: C# hires a janitor (garbage collector) to tidy up for you while Rust hands you a checklist and says, “You’ve got this.” Both methods work, but they have their quirks. Let’s dive into how memory is handled in these two languages and what makes each approach unique. **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rus](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: ***Memory Wars: Garbage Collection in C# vs. Ownership in Rust*** - Part 5: Threads, Tasks, and Ownership: C# and Rust Concurrency Explored - Part 6: Building with .NET and Rust: A Tale of Two Ecosystems - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts Regarding memory management, programming languages take different approaches to ensure your applications don’t crash and burn. Think of it as cleaning up after a party: C# hires a janitor (garbage collector) to tidy up for you while Rust hands you a checklist and says, “You’ve got this.” Both methods work, but they have their quirks. Let’s dive into how memory is handled in these two languages and what makes each approach unique. ### C#: The Garbage Collector to the Rescue C# uses a garbage collector (GC), which is like having a diligent robot that cleans up the memory you’re no longer using. It’s automatic, so you don’t have to consider it too much. Here’s how it works: 1. **Allocation Made Simple:** When you create an object, the .NET runtime allocates memory for it on the heap. 2. **Garbage Collection Runs the Show:** The GC periodically checks for objects that are no longer in use and frees up their memory. 3. **No Dangling Pointers:** Once an object is collected, it’s gone—no risk of accessing invalid memory. Example: ``` class Program { static void Main() { var myObject = new MyClass(); Console.WriteLine("Doing something with myObject"); myObject = null; // Eligible for garbage collection } } ``` The GC handles cleanup, so you don’t need to worry about freeing memory manually. But there’s a catch: Garbage collection can’t predict the future, so it runs when it decides it’s necessary, which might cause performance hiccups. ### Rust: Ownership and Borrowing Rust takes a different path—one where you’re in charge but with guardrails to keep things safe. Rust’s memory model is based on **ownership**, which ensures that memory is properly managed without a GC. Here’s the rundown: 1. **Ownership Rules:** Every piece of data has a single owner. When the owner goes out of scope, the memory is automatically freed. 2. **Borrowing:** You can temporarily “borrow” data (immutably or mutably) without taking ownership. 3. **No Runtime Overhead:** There’s no runtime garbage collection since everything is checked at compile time. Example: ``` fn main() { let s1 = String::from("Hello"); let s2 = s1; // Ownership is transferred to s2 // println!("{}", s1); // Error: s1 is no longer valid let s3 = String::from("World"); let len = calculate_length(&s3); // Borrow s3 println!("The length of '{}' is {}.", s3, len); } fn calculate_length(s: &String) -> usize { s.len() } ``` Rust’s strict ownership and borrowing rules ensure that memory issues like dangling pointers and double frees are caught at compile time. It’s a bit like having a rigorous but effective life coach who ensures you always clean up after yourself. ### Comparing the Two FeatureC#Rust**Ease of Use**Automatic with GCManual, but compiler-checked**Performance**GC can cause pausesNo runtime overhead**Memory Safety**Mostly safe, with some risksGuaranteed by compiler**Flexibility**More forgivingRequires planning### Which Approach is Better? It depends on your needs! C# is excellent for applications where ease of use and developer productivity are key. The garbage collector does the heavy lifting, freeing you to focus on writing code. On the other hand, Rust’s ownership model is perfect for performance-critical systems, embedded programming, or scenarios where memory safety is non-negotiable. ### Final Thoughts Memory management in programming is like choosing between automatic and manual transmission cars. C# offers the ease of automatic, letting you cruise without worrying about shifting gears. Rust is manual, giving you more control and precision but requiring a bit more effort to master. Understanding these approaches will make you a better programmer, whatever you choose. So, are you ready to take the wheel and try Rust’s ownership model? Or will you stick to the comfort of C#’s garbage collector? Let us know in the comments! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Threads, Tasks, and Ownership: C# and Rust Concurrency Explored](https://www.woodruff.dev/threads-tasks-and-ownership-c-and-rust-concurrency-explored/) **Published:** January 21, 2025 **Author:** Chris Woodruff **Excerpt:** Concurrency in programming can be like cooking dinner while answering emails—you’re juggling multiple tasks at once, hoping nothing burns. C# and Rust both tackle concurrency, but their approaches couldn’t be more different. C# offers a traditional multitasking toolkit with threads and async/await, while Rust rewrites the rulebook with a compiler-enforced, fearless concurrency model. Let’s see how they compare! **Content:** ## Posts in this Series on Rust for C# Developers - Part 1: [Why Every C# Developer Should Explore Rust](https://woodruff.dev/why-every-c-developer-should-explore-rust/) - Part 2: [Exploring Programming Paradigms: C# and Rust Side by Side](https://woodruff.dev/exploring-programming-paradigms-c-and-rust-side-by-side/) - Part 3: [Syntax Smackdown: Comparing Constructs in C# and Rus](https://woodruff.dev/syntax-showdown-a-look-at-common-constructs-in-c-and-rust/) - Part 4: [Memory Wars: Garbage Collection in C# vs. Ownership in Rust](https://woodruff.dev/memory-wars-garbage-collection-in-c-vs-ownership-in-rust/) - Part 5: ***Threads, Tasks, and Ownership: C# and Rust Concurrency Explored*** - Part 6: Building with .NET and Rust: A Tale of Two Ecosystems - Part 7: Level Up Your Skills: Learning Rust as a C# Dev - Part 8: Rust’s Superpower: Speed Meets Smarts Concurrency in programming can be like cooking dinner while answering emails—you’re juggling multiple tasks at once, hoping nothing burns. C# and Rust both tackle concurrency, but their approaches couldn’t be more different. C# offers a traditional multitasking toolkit with threads and `async/await`, while Rust rewrites the rulebook with a compiler-enforced, fearless concurrency model. Let’s see how they compare! ### C#: The Multitasking Maestro C# makes concurrent programming accessible with features like threads, tasks, and the much-loved `async/await`. It’s like hiring a team of sous-chefs to help you cook—easy delegation, but you need to watch out for kitchen collisions. **Threads and Tasks** Threads are the OGs of concurrency in C#. Tasks, introduced later, offer a higher-level abstraction: ``` using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { Console.WriteLine("Starting tasks..."); var task1 = Task.Run(() => DoWork("Task 1")); var task2 = Task.Run(() => DoWork("Task 2")); await Task.WhenAll(task1, task2); Console.WriteLine("All tasks completed."); } static void DoWork(string taskName) { Console.WriteLine($"{taskName} is working..."); Thread.Sleep(1000); // Simulating work Console.WriteLine($"{taskName} is done."); } } ``` Tasks simplify threading and allow you to use `async/await` for non-blocking operations. However, you’re still responsible for avoiding race conditions, deadlocks, and thread safety issues—your kitchen can still turn into chaos. **Locks and Semaphores** To manage shared resources safely, you’ll need synchronization tools like locks and semaphores: ``` private static readonly object _lock = new object(); lock (_lock) { // Access shared resource safely } ``` It works, but it’s easy to forget these guards, leading to subtle bugs. ### Rust: Fearless and Safe Rust’s approach to concurrency is all about safety without sacrificing performance. Thanks to its ownership and borrowing rules, Rust ensures that you don’t shoot yourself in the foot when working with multiple threads. It’s like having a kitchen where every chef is assigned their own workstation—no bumping into each other. **Ownership and Borrowing** Rust’s ownership model ensures exclusive access to resources. You can only mutate data if you have a unique reference to it. This eliminates the possibility of data races at compile time: ``` use std::thread; fn main() { let data = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Data: {:?}", data); }); handle.join().unwrap(); } ``` Notice the `move` keyword? It transfers ownership of `data` to the thread, ensuring no other thread can access it. **Send and Sync Traits** Rust uses the `Send` and `Sync` traits to enforce thread safety. A type can be transferred between threads if it implements `Send`, and it can be shared across threads if it implements `Sync`. These traits are automatically applied to types deemed safe by the compiler. **Concurrency Primitives** Rust provides threads, channels, and locks, but with a safety-first twist: ``` use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } ``` Here, `Arc` (atomic reference counting) and `Mutex` ensure safe sharing and mutation of the `counter` across threads. ### Comparing the Models FeatureC#Rust**Ease of Use**High, with `async/await`Moderate, with steep learning curve**Performance**Good, with some overheadExcellent, no runtime GC**Safety**Developer-dependentCompiler-enforced**Concurrency Tools**Threads, tasks, locksThreads, channels, `Arc`/`Mutex`### Final Thoughts C# offers a developer-friendly approach to concurrency, with tools that make multitasking approachable. However, it leaves safety largely in your hands. Rust, on the other hand, enforces safety at every turn, ensuring that concurrency issues like data races are impossible by design. So, if you’re building enterprise apps with complex async workflows, C# might be your best bet. But if you’re diving into performance-critical systems or just want to experience “fearless concurrency,” give Rust a try. Either way, you’ll come out with a stronger grasp of concurrency concepts—and that’s a win. Which concurrency model do you prefer? Let us know in the comments below! **Categories:** Rust **Tags:** .NET, C#, dotnet, programming, rust --- ### [Think Beyond Synchronous: The Ultimate Guide to Tasks in C#](https://www.woodruff.dev/think-beyond-synchronous-the-ultimate-guide-to-tasks-in-c/) **Published:** January 2, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine you’re cooking dinner. You handle both tasks simultaneously instead of waiting for the water to boil before chopping vegetables. That’s the power of asynchronous programming in C#: enabling your programs to perform multiple operations concurrently, improving performance and responsiveness. The Task class, a cornerstone of modern .NET development, is at the heart of this capability. **Content:** Imagine you’re cooking dinner. You handle both tasks simultaneously instead of waiting for the water to boil before chopping vegetables. That’s the power of asynchronous programming in C#: enabling your programs to perform multiple operations concurrently, improving performance and responsiveness. The **Task** class, a cornerstone of modern .NET development, is at the heart of this capability. In this blog post, we’ll explore how tasks work in C#, how to use them effectively, and why they’re essential for building high-performance applications. --- ### What Are Tasks in C#? A **Task** represents an asynchronous operation that runs independently of the main thread. This means your program doesn’t need to wait for one task to finish before starting the next, leading to faster and more responsive applications. For example, if your program needs to fetch data from a database, process files, and respond to user inputs, tasks allow these operations to happen simultaneously. This improves the overall performance and ensures that the application remains responsive to the user. --- ### Creating and Running Tasks To work with tasks in C#, you use the **`System.Threading.Tasks`** namespace. The **`Task`** class provides methods for defining and executing asynchronous code. The simplest way to create a task is by using **`Task.Run`**, which accepts a delegate (a function that can be passed as an argument) specifying the code to run asynchronously. Here’s a basic example: ``` Task myTask = Task.Run(() => { // Simulate some work Console.WriteLine("Task is running asynchronously."); }); ``` If you want the task to return a value, you can use **`Task`**, where `TResult` is the type of the returned value: ``` Task myTask = Task.Run(() => { // Simulate work that produces a result return 42; // Returning a value }); ``` --- ### Best Practices: Embracing `async` and `await` While using Task.Run is practical, modern C# encourages using **async** and **await** for non-blocking asynchronous programming. These keywords allow you to write asynchronous code that looks and behaves like synchronous code, making it easier to read and maintain. Here’s how to use `async` and `await` with a task: ``` async Task FetchDataAsync() { return await Task.Run(() => { // Simulate a time-consuming operation return 42; }); } int result = await FetchDataAsync(); Console.WriteLine($"Result: {result}"); ``` This approach avoids blocking the main thread and keeps your application responsive. --- ### Managing Multiple Tasks You’ll often need to work with multiple tasks in real-world applications simultaneously. The **`Task.WhenAll`** and **`Task.WhenAny`** methods make it easy to handle multiple asynchronous operations: - **`Task.WhenAll`** waits for all tasks to complete:csharpCopy code`var task1 = Task.Run(() => Console.WriteLine("Task 1")); var task2 = Task.Run(() => Console.WriteLine("Task 2")); await Task.WhenAll(task1, task2);` - **`Task.WhenAny`** completes as soon as any one of the tasks finishes:csharpCopy code`await Task.WhenAny(task1, task2);` --- ### Cancelling a Task Sometimes, you may need to cancel a task before it finishes. To achieve this, use the **`CancellationTokenSource`** and **`CancellationToken`** classes. These allow you to pass a cancellation token to the task and check periodically whether cancellation has been requested. Here’s an example: ``` csharpCopy codeCancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; Task myTask = Task.Run(() => { while (!token.IsCancellationRequested) { // Simulate ongoing work Console.WriteLine("Task is running..."); } }, token); // Cancel the task cts.Cancel(); ``` By using cancellation tokens, your tasks can respond gracefully to cancellation requests. --- ### Summary: The Power of Tasks Tasks are a powerful feature in C# that significantly boosts the efficiency of your applications. They enable you to write scalable and responsive applications, execute code asynchronously, handle multiple operations concurrently, and even cancel tasks when needed. By mastering tasks and combining them with async and await, you can take your programming skills to the next level. Whether you’re building a simple calculator app or a complex distributed system like a cloud-based file storage service, tasks are an indispensable tool in modern C#. Ready to get started? Experiment with tasks in your next project and see the difference they can make! **Categories:** Blog, Random C# **Tags:** .NET, C#, dotnet, programming --- ### [Managing Client Sessions: Tracking and Personalizing Connections](https://www.woodruff.dev/managing-client-sessions-tracking-and-personalizing-connections/) **Published:** January 15, 2025 **Author:** Chris Woodruff **Excerpt:** In the world of socket programming, managing client sessions is where the magic happens. It’s what transforms a basic connection into a personalized, memorable experience. Whether you’re building a chat application, a multiplayer game, or a real-time dashboard, tracking and managing client sessions is the secret sauce that keeps users engaged and coming back for more. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - [Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets***](https://woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/ "Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets") - [Part 5: ***Building Bridges: Client-Side Socket Programming in Action***](https://woodruff.dev/building-bridges-client-side-socket-programming-in-action/ "Part 5: Building Bridges: Client-Side Socket Programming in Action") - [Part 6: ***Handling Complexity: Server-Side Socket Programming Explained***](https://woodruff.dev/handling-complexity-server-side-socket-programming-explained/ "Part 6: Handling Complexity: Server-Side Socket Programming Explained") - [Part 7: ***Real-Time Communication: Effective Data Exchange with Sockets***](https://woodruff.dev/real-time-communication-effective-data-exchange-with-sockets/ "Part 7: Real-Time Communication: Effective Data Exchange with Sockets") - [Part 8: ***Error Handling and Graceful Shutdowns in Socket Programming***](https://woodruff.dev/error-handling-and-graceful-shutdowns-in-socket-programming/ "Part 8: Error Handling and Graceful Shutdowns in Socket Programming") - Part 9: ***Managing Client Sessions: Tracking and Personalizing Connections*** In the world of socket programming, managing client sessions is where the magic happens. It’s what transforms a basic connection into a personalized, memorable experience. Whether you’re building a chat application, a multiplayer game, or a real-time dashboard, tracking and managing client sessions is the secret sauce that keeps users engaged and coming back for more. Let’s break down how to manage client sessions effectively and take your networked applications to the next level. --- ## What Are Client Sessions? A client session is more than just a connection—it’s a living, breathing interaction between your application and its users. It allows you to: - Identify Clients: Know who’s connecting to your application. - Personalize Experiences: Tailor content or functionality based on individual users. - Maintain State: Keep track of ongoing activities or preferences across interactions. Imagine logging into your favorite app and finding all your settings, preferences, and progress exactly as you left them. That’s the power of managing client sessions. --- ## Step 1: Identifying Clients The first step in managing sessions is assigning a unique identifier to each client. This could be a username, an account ID, or a randomly generated session ID. Here’s a simple way to generate a unique ID: ``` string sessionId = Guid.NewGuid().ToString(); Console.WriteLine($"New session created: {sessionId}"); ``` This identifier becomes the key to everything you’ll track about the client during their session. --- ## Step 2: Storing Session Data Once you’ve identified a client, you need a way to store their session data. In a socket-based application, a dictionary is a lightweight and efficient choice: ``` using System.Collections.Generic; Dictionary clientSessions = new Dictionary(); clientSessions[sessionId] = new { UserName = "Alice", ConnectedAt = DateTime.Now }; ``` This approach lets you store custom information for each session, such as: - User Details: Name, preferences, or role. - Connection Metadata: IP address, connection time, or activity log. - Temporary State: Current game level, active chat room, or pending transactions. --- ## Step 3: Tracking Active Clients As clients connect and disconnect, your server needs to keep tabs on who’s active. A simple way to do this is by maintaining a list or dictionary of active sessions: ``` HashSet activeClients = new HashSet(); activeClients.Add(sessionId); // When a client disconnects activeClients.Remove(sessionId); ``` This helps you monitor the overall health of your server and ensure resources are being used efficiently. --- ## Step 4: Personalizing Client Experiences One of the biggest advantages of managing sessions is the ability to personalize interactions. For example: - **Dynamic Responses**: Tailor server responses based on the client’s preferences or past behavior. - **Custom Notifications**: Send targeted messages or updates to specific clients. - **Persistent State**: Let users pick up where they left off, whether it’s resuming a video, rejoining a chat, or continuing a task. Here’s an example of sending a personalized message: ``` string welcomeMessage = $"Welcome back, {clientSessions[sessionId].UserName}!"; byte[] messageBytes = Encoding.UTF8.GetBytes(welcomeMessage); clientSocket.Send(messageBytes); ``` --- ## Step 5: Cleaning Up Sessions When a client disconnects, it’s essential to clean up their session data. This prevents memory leaks and ensures your server remains performant: ``` if (clientSessions.ContainsKey(sessionId)) { clientSessions.Remove(sessionId); Console.WriteLine($"Session {sessionId} ended and cleaned up."); } ``` Automating session cleanup for inactive clients can further optimize your application. For instance, you can implement a timeout mechanism to close idle sessions. ## Good Guidance for Managing Client Sessions 1. **Secure Your Sessions**: Protect session data with encryption and secure transmission protocols. 2. **Optimize Storage**: Use in-memory caches like Redis for scalable session storage in high-traffic applications. 3. **Balance Performance**: Avoid storing excessive data in the session to maintain responsiveness. 4. **Log Key Events**: Track session start, activity, and termination for debugging and analytics. --- ## Why Session Management Matters Managing client sessions is about more than just keeping track of who’s connected. It’s about creating seamless, meaningful interactions that make users feel valued. When you track sessions effectively, you’re not just running a server—you’re building relationships. Whether it’s a game server that remembers your last move or a chat app that reconnects you to an ongoing conversation, session management transforms the way users experience your application. And as you grow your skills, it opens the door to advanced capabilities like scaling across multiple servers or providing personalized real-time updates. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Building Bridges: Client-Side Socket Programming in Action](https://www.woodruff.dev/building-bridges-client-side-socket-programming-in-action/) **Published:** January 11, 2025 **Author:** Chris Woodruff **Excerpt:** Imagine your app is like a curious explorer, eager to reach out into the digital wilderness to gather information, send requests, or simply have a conversation with a server. That’s where client-side socket programming steps in—it’s the bridge that connects your app to the world. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - [Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets***](https://woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/ "Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets") - Part 5: ***Building Bridges: Client-Side Socket Programming in Action*** - Part 6: Handling Complexity: Server-Side Socket Programming Explained - Part 7: Real-Time Communication: Effective Data Exchange with Sockets - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections Imagine your app is like a curious explorer, eager to reach out into the digital wilderness to gather information, send requests, or simply have a conversation with a server. That’s where client-side socket programming steps in—it’s the bridge that connects your app to the world. In this post, we’ll walk you through the essentials of client-side socket programming in C#. Whether you’re new to networking or brushing up on your skills, this guide will help you confidently send and receive data like a pro. --- ## What’s the Role of a Client in Socket Programming? Think of a client as the initiator. It’s the one that starts the conversation by sending a request to a server. This could be anything from fetching a webpage to uploading a file. The client then waits (sometimes patiently, sometimes not) for the server to respond. The entire exchange happens through a magical entity called a **socket**. Let’s get hands-on and build that bridge! --- ## Step 1: Creating a Socket First things first—you need a socket. In C#, creating one is straightforward: ``` using System.Net.Sockets; // Create a TCP socket Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); ``` ### What’s Going On Here? - `AddressFamily.InterNetwork`: This sets the socket to use IPv4. - `SocketType.Stream`: A stream socket uses a reliable, connection-oriented protocol (hello, TCP!). - `ProtocolType.Tcp`: We’re using the Transmission Control Protocol, perfect for tasks where reliability matters. With just one line, you’ve crafted a digital portal for communication. Pretty cool, right? --- ## Step 2: Connecting to a Server Now that you have your socket, it’s time to connect it to a server. Think of this like dialing a phone number—you need the server’s address and port to initiate the connection. ``` using System.Net; // Define the server endpoint IPAddress serverAddress = IPAddress.Parse("127.0.0.1"); int serverPort = 11000; IPEndPoint serverEndpoint = new IPEndPoint(serverAddress, serverPort); // Connect the socket clientSocket.Connect(serverEndpoint); Console.WriteLine("Connected to the server!"); ``` > ### Pro Tip > > Always use a try-catch block when connecting. Networks can be unpredictable, and it’s best to gracefully handle any hiccups: ``` try { clientSocket.Connect(serverEndpoint); Console.WriteLine("Connection successful!"); } catch (SocketException ex) { Console.WriteLine($"Connection failed: {ex.Message}"); } ``` --- ## Step 3: Sending Data to the Server Once connected, it’s time to send data. Think of this like composing a message and handing it to your socket for delivery. ``` using System.Text; // Convert the message to bytes string message = "Hello, Server!"; byte[] messageBytes = Encoding.UTF8.GetBytes(message); // Send the message clientSocket.Send(messageBytes); Console.WriteLine("Message sent to the server."); ``` Simple, right? Just be sure your data is in byte format, as that’s the language sockets speak. --- ## Step 4: Receiving a Response What’s a conversation without a response? Use the Receive method to listen for data from the server. ``` byte[] buffer = new byte[1024]; // Allocate a buffer int bytesReceived = clientSocket.Receive(buffer); string response = Encoding.UTF8.GetString(buffer, 0, bytesReceived); Console.WriteLine($"Server says: {response}"); ``` ### Handling Variable-Length Messages If you’re dealing with larger messages or streaming data, consider implementing a loop to handle chunks of incoming data. However, for most basic use cases, the above method works just fine. --- ## Step 5: Closing the Connection Every good conversation must come to an end, and the same applies to socket connections. Always close your socket when you’re done to free up resources: ``` clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); Console.WriteLine("Connection closed."); ``` --- ## Why Client-Side Socket Programming Matters Understanding how to implement client-side sockets isn’t just a technical skill—it’s a superpower. It allows you to create apps that communicate with servers anywhere in the world, whether you’re building a real-time chat app, a file-sharing platform, or the next big multiplayer game. Stay tuned for our next post, where we’ll explore the server-side counterpart to this conversation. Together, these skills will empower you to create networked applications that truly connect. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Real-Time Communication: Effective Data Exchange with Sockets](https://www.woodruff.dev/real-time-communication-effective-data-exchange-with-sockets/) **Published:** January 13, 2025 **Author:** Chris Woodruff **Excerpt:** Real-time communication is the heartbeat of modern applications, powering everything from video calls to live sports updates. At the core of these dynamic interactions lies the effective exchange of data using sockets. Sockets are the unseen maestros of this symphony, ensuring that data flows seamlessly between clients and servers, even when speed and accuracy are paramount. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - [Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets***](https://woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/ "Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets") - [Part 5: ***Building Bridges: Client-Side Socket Programming in Action***](https://woodruff.dev/building-bridges-client-side-socket-programming-in-action/ "Part 5: Building Bridges: Client-Side Socket Programming in Action") - [Part 6: ***Handling Complexity: Server-Side Socket Programming Explained***](https://woodruff.dev/handling-complexity-server-side-socket-programming-explained/ "Part 6: Handling Complexity: Server-Side Socket Programming Explained") - Part 7: ***Real-Time Communication: Effective Data Exchange with Sockets*** - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections > Real-time communication is the heartbeat of modern applications, powering everything from video calls to live sports updates. At the core of these dynamic interactions lies the effective exchange of data using sockets. Sockets are the unseen maestros of this symphony, ensuring that data flows seamlessly between clients and servers, even when speed and accuracy are paramount. Let’s explore how to achieve efficient and reliable data exchange with sockets, making your applications not just functional, but extraordinary. --- ## Understanding Real-Time Data Exchange In socket communication, data exchange is a two-way street. Clients send requests, and servers respond, often in rapid succession. Whether you’re streaming a video, participating in a multiplayer game, or chatting in real-time, the quality of the user experience hinges on how efficiently data travels through this pipeline. Sockets, being versatile and powerful, enable this exchange by providing a direct, low-level pathway for data to move back and forth. But making this work smoothly requires understanding the nuances of sending and receiving data. --- ## Sending Data: Speaking the Socket’s Language Sockets speak in bytes, so any message you send must first be converted into a byte array. For instance, sending a simple text message looks like this in C#: ``` using System.Text; // Convert message to bytes string message = "Hello, Server!"; byte[] messageBytes = Encoding.UTF8.GetBytes(message); // Send the message clientSocket.Send(messageBytes); ``` Here’s what’s happening: The message is encoded into UTF-8, ensuring it’s compact and universally understood. The Send method delivers it to the server through the socket. But what if your data is more complex, like a file or a structured object? In such cases, serialization (e.g., converting an object into a JSON string) ensures the data is ready for transport. --- ## Receiving Data: Listening Like a Pro Receiving data is equally straightforward, but it requires a bit of finesse to handle varying amounts of data gracefully. Here’s a basic example: ``` byte[] buffer = new byte[1024]; // Allocate buffer int bytesReceived = clientSocket.Receive(buffer); string response = Encoding.UTF8.GetString(buffer, 0, bytesReceived); Console.WriteLine($"Server says: {response}"); ``` This snippet: 1. Creates a buffer to temporarily hold incoming data. 2. Reads data from the socket into the buffer. 3. Converts the received bytes back into a human-readable string. --- ## Managing Larger or Continuous Data Streams When dealing with larger files or continuous streams of data, you can’t always rely on a single read operation to capture everything. In these cases, breaking the data into chunks ensures nothing is lost. Here’s an example for handling larger data: ``` byte[] buffer = new byte[1024]; StringBuilder messageBuilder = new StringBuilder(); while (true) { int bytesRead = clientSocket.Receive(buffer); if (bytesRead == 0) break; // No more data messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); } string fullMessage = messageBuilder.ToString(); Console.WriteLine($"Complete message received: {fullMessage}"); ``` This approach: - Reads the data in chunks, appending each piece to a StringBuilder. - Stops when the connection indicates there’s no more data. --- ## Ensuring Reliability and Speed While sockets are designed for efficiency, you can take additional steps to ensure your data exchange is both fast and reliable: 1. Timeouts: Avoid endless waits by setting timeouts for sending and receiving data. ``` clientSocket.ReceiveTimeout = 5000; // 5 seconds clientSocket.SendTimeout = 5000; ``` 2. Acknowledgments: Have the server send an acknowledgment after receiving data to confirm successful delivery. 3. Compression: For large datasets, compress the data before sending to reduce transmission time. --- ## Why Effective Data Exchange Matters Real-time communication is all about creating experiences that feel instant and intuitive. Whether it’s a quick chat response, a seamless video stream, or a fast-paced online game, users expect their applications to respond immediately and accurately. By mastering the art of sending and receiving data with sockets, you’re not just building applications—you’re crafting experiences. You’re ensuring that every piece of data reaches its destination quickly, efficiently, and without compromise. In future discussions, we’ll explore advanced topics like securing socket communication and optimizing performance, taking your real-time applications to the next level. For now, go ahead and experiment with these techniques—you’re well on your way to mastering the art of real-time communication. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Handling Complexity: Server-Side Socket Programming Explained](https://www.woodruff.dev/handling-complexity-server-side-socket-programming-explained/) **Published:** January 12, 2025 **Author:** Chris Woodruff **Excerpt:** Handling server-side socket programming is like orchestrating a digital symphony. While the client starts the conversation, the server is the conductor, managing multiple requests, coordinating responses, and ensuring everything stays in harmony. Server-side programming is a mix of art and science—it’s about balancing responsiveness, scalability, and reliability. Let’s break it down and make sense of the complexity. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - [Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets***](https://woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/ "Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets") - [Part 5: ***Building Bridges: Client-Side Socket Programming in Action***](https://woodruff.dev/building-bridges-client-side-socket-programming-in-action/ "Part 5: Building Bridges: Client-Side Socket Programming in Action") - Part 6: ***Handling Complexity: Server-Side Socket Programming Explained*** - Part 7: Real-Time Communication: Effective Data Exchange with Sockets - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections Handling server-side socket programming is like orchestrating a digital symphony. While the client starts the conversation, the server is the conductor, managing multiple requests, coordinating responses, and ensuring everything stays in harmony. Server-side programming is a mix of art and science—it’s about balancing responsiveness, scalability, and reliability. Let’s break it down and make sense of the complexity. --- ## What Does a Server Do in Socket Programming? At its core, a server is a listener. It’s always on, patiently waiting for clients to knock on its door. Once a client connects, the server takes on the role of a host, responding to requests and maintaining communication. It’s not just about one client, though—a good server juggles multiple connections effortlessly, ensuring every client feels like the only one. --- ## Step 1: Creating the Server Socket Before a server can listen, it needs a socket. Creating one in C# is straightforward: ``` using System.Net; using System.Net.Sockets; Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); ``` This sets up a TCP-based stream socket using IPv4. It’s your server’s main entry point for communication. --- ## Step 2: Binding to an Endpoint The server needs to associate itself with a specific IP address and port, like assigning an address where clients can find it. ``` IPAddress ipAddress = IPAddress.Any; // Accept connections on all network interfaces int port = 11000; serverSocket.Bind(new IPEndPoint(ipAddress, port)); ``` Using IPAddress.Any ensures your server listens on all available network interfaces, making it accessible no matter where the client connects. --- ## Step 3: Listening for Connections Once the server is bound, it’s time to start listening for client requests: ``` serverSocket.Listen(10); // Queue up to 10 pending connections Console.WriteLine("Server is listening..."); ``` The Listen method tells the server to prepare for incoming connections. The number 10 represents how many connections can wait in line before the server starts rejecting them. --- ## Step 4: Accepting a Client When a client attempts to connect, the server uses the Accept method to establish the connection. This creates a new socket dedicated to that client. ``` Socket clientSocket = serverSocket.Accept(); Console.WriteLine($"Client connected: {clientSocket.RemoteEndPoint}"); ``` Each client gets its own socket, which allows the server to manage multiple connections simultaneously. --- ## Step 5: Handling Multiple Clients In real-world scenarios, a server often deals with many clients at once. One way to handle this is by creating a new thread or task for each client: ``` Thread clientThread = new Thread(() => HandleClient(clientSocket)); clientThread.Start(); ``` The HandleClient method contains the logic for interacting with that specific client, leaving the main thread free to continue accepting new connections. --- ## Step 6: Communicating with Clients Servers send and receive data using the client’s dedicated socket. For example, to receive data: ``` byte[] buffer = new byte[1024]; int bytesReceived = clientSocket.Receive(buffer); string clientMessage = Encoding.UTF8.GetString(buffer, 0, bytesReceived); Console.WriteLine($"Client says: {clientMessage}"); ``` And to send a response: ``` byte[] message = Encoding.UTF8.GetBytes("Hello, Client!"); clientSocket.Send(message); ``` These operations form the heart of server-client communication. --- ## Step 7: Closing Connections Gracefully When the interaction is complete, it’s essential to close the client’s socket properly: ``` clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); Console.WriteLine("Client disconnected."); ``` This ensures no resources are left hanging, keeping the server efficient and responsive. --- ## Why Server-Side Programming Matters Server-side socket programming isn’t just about managing connections—it’s about creating robust, scalable systems that can handle the unexpected. Whether you’re building a chat server, a multiplayer game, or a streaming platform, understanding these principles ensures your application can support users seamlessly and reliably. In our next post, we’ll dive into managing advanced server-side challenges like scaling, error handling, and securing your sockets. Because when it comes to building bridges in the digital world, every connection matters. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [C# Socket Programming Essentials: Creating and Configuring Sockets](https://www.woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/) **Published:** January 10, 2025 **Author:** Chris Woodruff **Excerpt:** If you’ve ever wondered how applications like chat messengers or multiplayer games keep us connected, sockets are the unsung heroes behind the scenes. The concept of socket programming dates back to the early days of the internet, when developers needed a way to establish communication between different devices. In this post, we’ll dive into the essentials of socket programming in C#, focusing on creating and configuring sockets. Whether you’re a seasoned developer or just getting your feet wet in network programming, these fundamentals will help you get started. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets*** - Part 5: Building Bridges: Client-Side Socket Programming in Action - Part 6: Handling Complexity: Server-Side Socket Programming Explained - Part 7: Real-Time Communication: Effective Data Exchange with Sockets - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections If you’ve ever wondered how applications like chat messengers or multiplayer games keep us connected, sockets are the unsung heroes behind the scenes. The concept of socket programming dates back to the early days of the internet, when developers needed a way to establish communication between different devices. In this post, we’ll dive into the essentials of socket programming in C#, focusing on creating and configuring sockets. Whether you’re a seasoned developer or just getting your feet wet in network programming, these fundamentals will help you get started. --- ## What Is a Socket? Think of a socket as a digital plug that connects devices over a network. It’s your application’s gateway for sending and receiving data. In C#, sockets are part of the `System.Net.Sockets` namespace, giving you the tools to create powerful networked applications. --- ## Creating Your First Socket Creating a socket in C# is straightforward. You specify three key elements: the address family, the socket type, and the protocol. Here’s an example: ``` using System.Net.Sockets; // Create a TCP/IP socket Socket mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); ``` What’s Happening Here? - `AddressFamily.InterNetwork`: This indicates that we’re using IPv4. - `SocketType.Stream`: This tells the socket to use a reliable, connection-oriented data stream (perfect for TCP). - `ProtocolType.Tcp`: Specifies that the Transmission Control Protocol (TCP) will manage communication. Pretty neat, right? With just one line of code, you’ve created a socket that can handle real-world network communication. --- ## Configuring the Socket Now that we’ve created a socket, it’s time to tweak it to meet your application’s needs. Sockets come with several configurable options, such as timeouts and buffer sizes. ### Setting Timeouts No one likes an app that hangs indefinitely. Timeouts ensure your socket operations don’t leave users waiting forever. ``` mySocket.ReceiveTimeout = 5000; // Waits for 5 seconds before giving up mySocket.SendTimeout = 5000; // Same for sending data ``` ### Buffer Sizes Buffer size determines how much data the socket can handle at once. This is worth tuning if you’re dealing with large files or real-time data. ``` mySocket.ReceiveBufferSize = 8192; // 8 KB for receiving data mySocket.SendBufferSize = 8192; // 8 KB for sending data ``` --- ## Binding the Socket Before your socket can start communicating, it needs an address to call home. This is where binding comes in. ``` using System.Net; // Bind the socket to an endpoint IPEndPoint localEndpoint = new IPEndPoint(IPAddress.Any, 11000); mySocket.Bind(localEndpoint); ``` Here: - IPAddress.Any: This means the socket will listen on all available network interfaces. - 11000: This is the port number the socket will use. You can choose any free port. --- ## Wrapping It All Up Once you’ve created, configured, and bound your socket, it’s ready to listen for incoming connections (if it’s a server) or connect to another endpoint (if it’s a client). These steps form the foundation of socket programming in C#. With a few lines of code, you’ve set the stage for building robust and scalable networked applications. In upcoming posts, we’ll explore client-side and server-side programming to bring these concepts to life. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Error Handling and Graceful Shutdowns in Socket Programming](https://www.woodruff.dev/error-handling-and-graceful-shutdowns-in-socket-programming/) **Published:** January 14, 2025 **Author:** Chris Woodruff **Excerpt:** In the world of socket programming, things don’t always go as planned. Networks are unpredictable, connections drop, and unexpected errors can throw a wrench in the smooth operation of your application. But here’s the good news: with robust error handling and graceful shutdowns, you can keep your application resilient and your users happy, even when things go sideways. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - [Part 3: ***Socket Types: Choosing the Right Tool for the Job***](https://woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/ "Part 3: Socket Types: Choosing the Right Tool for the Job") - [Part 4: ***C# Socket Programming Essentials: Creating and Configuring Sockets***](https://woodruff.dev/c-socket-programming-essentials-creating-and-configuring-sockets/ "Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets") - [Part 5: ***Building Bridges: Client-Side Socket Programming in Action***](https://woodruff.dev/building-bridges-client-side-socket-programming-in-action/ "Part 5: Building Bridges: Client-Side Socket Programming in Action") - [Part 6: ***Handling Complexity: Server-Side Socket Programming Explained***](https://woodruff.dev/handling-complexity-server-side-socket-programming-explained/ "Part 6: Handling Complexity: Server-Side Socket Programming Explained") - [Part 7: ***Real-Time Communication: Effective Data Exchange with Sockets***](https://woodruff.dev/real-time-communication-effective-data-exchange-with-sockets/ "Part 7: Real-Time Communication: Effective Data Exchange with Sockets") - Part 8: ***Error Handling and Graceful Shutdowns in Socket Programming*** - Part 9: Managing Client Sessions: Tracking and Personalizing Connections In the world of socket programming, things don’t always go as planned. Networks are unpredictable, connections drop, and unexpected errors can throw a wrench in the smooth operation of your application. But here’s the good news: with robust error handling and graceful shutdowns, you can keep your application resilient and your users happy, even when things go sideways. Let’s explore how to handle errors effectively and ensure your sockets say their goodbyes politely when it’s time to close. --- ## Why Error Handling Matters Imagine you’re on a phone call, and suddenly the line goes dead. You’d probably be annoyed, right? Now imagine if your app crashes because a socket operation failed—it’s just as frustrating for your users. Good error handling ensures your application doesn’t crash when something goes wrong; instead, it recovers gracefully, keeping the experience smooth and professional. In socket programming, errors can happen for many reasons: - The server is unavailable. - The network connection is unstable. - Data transmission takes too long (timeouts). - A client disconnects unexpectedly. Addressing these errors properly means your application remains functional and reliable. --- ## Catching and Handling Errors In C#, you can wrap your socket operations in try-catch blocks to catch exceptions and take appropriate action. For example: ``` try { clientSocket.Connect(serverEndpoint); Console.WriteLine("Connected to the server."); } catch (SocketException ex) { Console.WriteLine($"Socket error: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Unexpected error: {ex.Message}"); } ``` This approach allows you to: 1. Handle known issues, like network errors (SocketException). 2. Capture unexpected problems (Exception) and log them for further investigation. **Pro tip**: Always log errors with enough detail to debug the issue later, but avoid exposing sensitive information. --- ## Timeouts: Avoiding the Endless Wait Sometimes, a socket operation might hang indefinitely, waiting for data or a connection. Setting timeouts prevents your application from being stuck: ``` clientSocket.ReceiveTimeout = 5000; // 5 seconds for receiving data clientSocket.SendTimeout = 5000; // 5 seconds for sending data ``` If an operation exceeds the timeout, a SocketException is thrown, allowing you to handle the delay gracefully. --- ## Graceful Shutdowns: Parting on Good Terms When you’re done using a socket, it’s essential to close it properly. Abruptly terminating a connection can leave resources dangling and may even cause problems for the server or client. Here’s how to shut down a socket the right way: ``` try { clientSocket.Shutdown(SocketShutdown.Both); // Stop both sending and receiving clientSocket.Close(); // Release resources Console.WriteLine("Connection closed gracefully."); } catch (SocketException ex) { Console.WriteLine($"Error during shutdown: {ex.Message}"); } ``` Why is this important? Proper shutdowns: - Inform the other side that the connection is ending. - Free up system resources tied to the socket. - Prevent potential issues if the socket is reused or still active. --- ## Anticipating Common Scenarios A well-designed application doesn’t just handle errors when they occur—it anticipates them. Here are a few common scenarios and how to prepare for them: 1. Server Unavailability If the server is down or unreachable, handle the connection failure without crashing: ``` try { clientSocket.Connect(serverEndpoint); } catch (SocketException) { Console.WriteLine("Unable to connect to the server. Please try again later."); } ``` 2. Client Disconnects Mid-Session Be ready for a client to disconnect unexpectedly. Monitor the Receive method’s return value: ``` int bytesRead = clientSocket.Receive(buffer); if (bytesRead == 0) { Console.WriteLine("Client disconnected."); clientSocket.Close(); } ``` 3. Data Transmission Errors If sending data fails, log the issue and notify the user without disrupting the entire application: ``` try { clientSocket.Send(data); } catch (SocketException ex) { Console.WriteLine($"Failed to send data: {ex.Message}"); } ``` --- ## Building Resilient Applications Error handling and graceful shutdowns are not just technical niceties—they’re essential for creating applications that users trust and enjoy. By anticipating potential issues and responding to them effectively, you show users that you’ve thought of everything, even when things go wrong. Remember, it’s not about avoiding every error (that’s impossible); it’s about handling them with professionalism and care. Whether it’s a dropped connection or a temporary server outage, your app can shine by staying stable, responsive, and user-friendly. With these practices, you’re well on your way to mastering the art of socket programming. In the next topic, we’ll dive into managing client sessions and maintaining scalability in a multi-client environment. Stay tuned—your journey into networking excellence is just getting started. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Socket Types: Choosing the Right Tool for the Job](https://www.woodruff.dev/socket-types-choosing-the-right-tool-for-the-job/) **Published:** January 9, 2025 **Author:** Chris Woodruff **Excerpt:** Sockets, the unsung architects of digital communication, are each crafted with precision to meet the intricate demands of networking. The right socket choice is not just a technical decision—it’s a transformative step that can elevate your application’s performance from ordinary to extraordinary. With the wrong choice, efficiency crumbles; with the right one, your application thrives, and you, as a professional, are empowered. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") - [Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model***](https://woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/ "Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model") - Part 3: ***Socket Types: Choosing the Right Tool for the Job*** - Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets - Part 5: Building Bridges: Client-Side Socket Programming in Action - Part 6: Handling Complexity: Server-Side Socket Programming Explained - Part 7: Real-Time Communication: Effective Data Exchange with Sockets - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections Sockets, the unsung architects of digital communication, are each crafted with precision to meet the intricate demands of networking. The right socket choice is not just a technical decision—it’s a transformative step that can elevate your application’s performance from ordinary to extraordinary. With the wrong choice, efficiency crumbles; with the right one, your application thrives, and you, as a professional, are empowered. Now, let’s set out on a journey to unravel the unique characteristics and influence of each socket type. We’ll discover how they shape the connections between devices and applications in our interconnected world. --- ## Key Socket Types: The Architects of Connection 1. **Stream Sockets (TCP)** The stalwart workhorse of networking, stream sockets are all about **reliability** and **order**. They establish a steadfast connection, ensuring every byte of data reaches its destination intact and in the correct sequence. If your application demands unwavering accuracy—think file transfers, web browsing, or email services—stream sockets are your go-to heroes. *Why choose stream sockets?* Imagine sending an important document. Would you trust an unreliable messenger, or one who guarantees delivery to the right hands, in perfect condition? Stream sockets are that dependable courier, making them the backbone of critical data exchanges. 2. **Datagram Sockets (UDP)** The thrill-seeker of the socket world, datagram sockets prioritize **speed** over formality. Unlike their stream counterparts, they operate without the overhead of a connection, blazing a trail for fast, lightweight communication. Perfect for video streaming, online gaming, and real-time broadcasts, they embrace the chaos of the digital realm without compromising on performance. *Why choose datagram sockets?* Picture an adrenaline-fueled race where speed is everything. In scenarios where a dropped packet or two won’t ruin the experience, datagram sockets deliver unparalleled velocity, keeping your users engaged and exhilarated. 3. **Raw Sockets** For the tech virtuosos, raw sockets are the tools of mastery. They strip away abstractions, granting direct access to the network’s soul. Whether you’re building custom protocols, performing network monitoring, or crafting security solutions, raw sockets offer **unprecedented control**. *Why choose raw sockets?* Imagine being handed the keys to a Formula 1 car. Raw sockets demand expertise but reward you with the freedom to create, innovate, and push boundaries that other sockets can’t reach. 4. **Sequential Packet Sockets** The best of both worlds, sequential packet sockets combine **reliability** with **boundary preservation**. They deliver data in distinct records, maintaining order without sacrificing structure. These are ideal for transporting record-based information, like in database systems or structured messaging services. *Why choose sequential packet sockets?* Think of them as the meticulous archivists of the socket family, ensuring that every piece of data retains its individuality while being delivered reliably. --- ## Why Understanding Socket Types Matters Choosing the correct socket type is more than a technical decision—it’s an act of empowerment. It’s about aligning your application’s unique needs with the socket’s capabilities, ensuring your users enjoy seamless experiences. Your work directly impacts their enjoyment, whether they’re streaming the latest blockbuster or engaging in a heart-racing online battle. In the next post, we’ll unravel how C# provides a robust toolkit to implement these sockets, allowing you to unleash your creativity and craft applications that are as efficient as they are extraordinary. Ready to unlock the true potential of networking? Let’s dive deeper into the world of C# sockets! **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [The Backbone of Digital Communication: Understanding the Client-Server Model](https://www.woodruff.dev/the-backbone-of-digital-communication-understanding-the-client-server-model/) **Published:** January 8, 2025 **Author:** Chris Woodruff **Excerpt:** The client-server model, a ubiquitous presence in our digital universe, is the invisible framework that powers nearly every online interaction you experience. From the seamless streaming of your favorite shows to the lightning-fast loading of websites, this model is the architectural masterpiece orchestrating how devices exchange information in a networked world. It’s the quiet genius behind the scenes, ensuring your digital life feels effortless. **Content:** > > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > > > ## Blog Posts in this Series > > - [Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications***](https://woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/ "Part 1: Demystifying Socket Programming: A Gateway to Networked Applications") > - Part 2: ***The Backbone of Digital Communication: Understanding the Client-Server Model*** > - Part 3: Socket Types: Choosing the Right Tool for the Job > - Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets > - Part 5: Building Bridges: Client-Side Socket Programming in Action > - Part 6: Handling Complexity: Server-Side Socket Programming Explained > - Part 7: Real-Time Communication: Effective Data Exchange with Sockets > - Part 8: Error Handling and Graceful Shutdowns in Socket Programming > - Part 9: Managing Client Sessions: Tracking and Personalizing Connections The client-server model, a ubiquitous presence in our digital universe, is the invisible framework that powers nearly every online interaction you experience. From the seamless streaming of your favorite shows to the lightning-fast loading of websites, this model is the architectural masterpiece orchestrating how devices exchange information in a networked world. It’s the quiet genius behind the scenes, ensuring your digital life feels effortless. --- ## What Is the Client-Server Model? At its essence, the client-server model is a story of collaboration, a tale of two roles working in harmony: - **Clients**: The bold seekers of information, always on the lookout for services or resources. They are the explorers, sending out requests for what they need. - **Servers**: The tireless providers, standing ready to respond. They are the guardians of information, delivering what the clients require with precision and speed. This dynamic duo forms the backbone of online interactions, making complex processes feel as smooth and intuitive as flipping a switch. --- ## How Do Sockets Make It All Work? Here’s where the real magic happens. Sockets are the secret sauce, the unsung heroes enabling clients and servers to converse fluently and effectively. Think of them as digital conduits, carrying requests and responses like high-speed messengers in the digital age. 1. **Client Initiation**: Picture a client as a curious adventurer, reaching out with a request—perhaps to load a webpage, stream a song, or fetch the latest news. The socket acts as their trusted courier, delivering the message to the server with unerring accuracy. 2. **Server Response**: Enter the server, the steadfast responder. It processes the client’s request and crafts a response, sending it back down the socket’s digital pipeline. Whether it’s a webpage, a video stream, or a critical piece of data, the server ensures the client gets exactly what it needs. --- ## Why Should You Care? The client-server model isn’t just about technology—it’s about possibilities. It’s the reason we can enjoy instant communication through messaging apps, endless entertainment through streaming services, and real-time access to information from anywhere on the planet through web browsers. It’s the unsung hero of scalability and reliability, making it possible to serve millions of users seamlessly in these applications. And here’s the best part: understanding this model puts you in control as a developer. With tools like C#, you can harness the power of sockets and the client-server paradigm to create applications that are not only functional but also elegant, efficient, and awe-inspiring. It’s a testament to your skills and dedication. In the posts to come, we’ll delve into how C# simplifies the implementation of this model, unlocking your potential to craft digital experiences that not only meet but exceed user expectations. The adventure is just beginning—stay tuned! **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Demystifying Socket Programming: A Gateway to Networked Applications](https://www.woodruff.dev/demystifying-socket-programming-a-gateway-to-networked-applications/) **Published:** January 7, 2025 **Author:** Chris Woodruff **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). > > ## Blog Posts in this Series - Part 1: ***Demystifying Socket Programming: A Gateway to Networked Applications*** - Part 2: The Backbone of Digital Communication: Understanding the Client-Server Model - Part 3: Socket Types: Choosing the Right Tool for the Job - Part 4: C# Socket Programming Essentials: Creating and Configuring Sockets - Part 5: Building Bridges: Client-Side Socket Programming in Action - Part 6: Handling Complexity: Server-Side Socket Programming Explained - Part 7: Real-Time Communication: Effective Data Exchange with Sockets - Part 8: Error Handling and Graceful Shutdowns in Socket Programming - Part 9: Managing Client Sessions: Tracking and Personalizing Connections In today’s breathtakingly interconnected world, where technology orchestrates every facet of our lives, socket programming emerges as the unsung hero, the invisible thread weaving together the fabric of seamless communication between devices and applications. It’s not just a technical concept—it’s the heart of digital connection, the engine driving our hyper-connected reality. From the moment you check your email on your smartphone to the instant you stream your favorite show on your smart TV, socket programming is there, making it all possible. But let’s pause briefly: What is socket programming, and why does it matter so deeply? Picture this: a socket is like a digital handshake, a virtual plug that bridges applications, enabling them to whisper secrets or shout messages across the room or the globe. Whether it’s a smartphone chatting with a server to fetch your morning weather update or a gaming console streaming a pulse-pounding multiplayer experience, sockets are the unseen magic at play, making these digital experiences possible. --- ## Why is Socket Programming the Lifeblood of Modern Tech? Socket programming is not just important—it’s indispensable. It’s the foundation beneath the apps we adore and the systems we rely on, tirelessly enabling interactions we often take for granted. Let’s explore some electrifying examples: ### **Web Services** Every time you browse the web, sockets are quietly laboring behind the scenes. They fetch your favorite memes, deliver heartwarming articles, and serve up streaming videos with laser-like precision. ### **Real-Time Communication** Imagine FaceTiming a loved one thousands of miles away. Those crystal-clear video streams and near-instant audio are made possible by the relentless efficiency of sockets, ensuring your connection feels like magic. ### **IoT Devices** From smart refrigerators reminding you to restock milk to doorbell cameras keeping your home secure, the Internet of Things (IoT) relies on sockets to transmit data seamlessly between devices, creating the “smart” in your smart home. --- ## Why Should Developers Care? Here’s the truth: Understanding socket programming doesn’t just make you a better developer and a solution architect capable of building bridges between systems, crafting solutions that scale effortlessly, and solving problems that demand ingenuity and grit. It’s your ticket to creating applications that aren’t just functional but exceptional, and it’s a sure way to boost your confidence in your skills. This is more than coding. This is empowerment. This is mastery. So, get ready for an exciting journey! In the posts to come, we’ll unravel the intricate art and unyielding science of socket programming, equipping you with the skills to create applications that transcend expectations and redefine connectivity. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Routing and Topologies – Navigating the Digital Highways](https://www.woodruff.dev/routing-and-topologies-navigating-the-digital-highways/) **Published:** January 6, 2025 **Author:** Chris Woodruff **Excerpt:** When you send an email, stream a video, or check a social media feed, it's easy to take how that data travels to you for granted. Behind the scenes, an intricate system of routing and network topologies ensures that everything arrives where it's supposed to—fast and error-free. Let's pull back the curtain and take a closer look at how data navigates the digital highways and the role that topologies play in shaping those pathways. **Content:** > ### NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). When you send an email, stream a video, or check a social media feed, it’s easy to take how that data travels to you for granted. Behind the scenes, an intricate system of routing and network topologies ensures that everything arrives where it’s supposed to—fast and error-free. Let’s pull back the curtain and take a closer look at how data navigates the digital highways and the role that topologies play in shaping those pathways. ## What Is Routing? Imagine you’re driving to a new destination in a bustling city. You rely on a GPS to guide you through traffic, avoid closed roads, and get you there efficiently. Routing works much the same way, except the travelers are data packets, and the “GPS” is a combination of algorithms and network devices. Routing is the process of directing data packets from a source (say, your computer) to a destination (a server or another computer). Routers act as traffic controllers, analyzing each data packet’s destination and deciding the best path to take. Here’s why routing is so important: - **Efficiency**: It ensures that data takes the shortest, least congested route. - **Reliability**: Routers can find alternative routes if one path is blocked (due to a device failure, for example). - **Scalability**: It allows networks of all sizes—from small home networks to the vast internet—to function seamlessly. Static vs. Dynamic Routing Routing decisions don’t happen by magic. They’re based on two main strategies: static and dynamic routing. - **Static Routing**: Think of this as having a fixed map. Network administrators manually configure routes on each router. This method works well for small, stable networks but can become a nightmare for larger or frequently changing ones. If a route breaks, someone has to update it manually. - **Dynamic Routing**: This is the GPS of routing. Routers share information about network changes and calculate the best routes in real-time. Dynamic routing protocols, like OSPF (Open Shortest Path First) or BGP (Border Gateway Protocol), make networks adaptive and resilient. If a link fails, these protocols quickly find an alternative path. Dynamic routing is the go-to choice for larger, more complex networks because of its flexibility and efficiency. ## Understanding Network Topologies Now that we’ve covered how data moves, let’s talk about the roads themselves—network topologies. These are the layouts or designs of how devices connect in a network. Each topology has its own strengths and weaknesses, much like different city layouts. Here’s a quick tour of the most common network topologies: 1. **Bus Topology**: Picture a single main road with houses connected along it. In a bus topology, all devices are connected to one central cable. - **Pros**: Simple and cost-effective for small networks. - **Cons**: If the main cable fails, the entire network goes down. 2. **Star Topology**: Think of this as a hub-and-spoke model. A central hub connects directly to all devices. - **Pros**: Easy to manage, and if one device fails, it doesn’t affect the rest of the network. - **Cons**: The hub is a single point of failure—if it goes down, so does the network. 3. **Ring Topology**: Devices form a closed loop, like cars driving on a circular road. Data travels in one direction around the ring. - **Pros**: Simple to troubleshoot, and data flows predictably. - **Cons**: A single break in the loop can disrupt the entire network. 4. **Mesh Topology**: This is the gold standard for redundancy. Every device is connected to every other device, creating multiple paths for data. - **Pros**: Highly fault-tolerant and reliable. - **Cons**: Expensive and complex to set up. 5. **Hybrid Topology**: A combination of two or more topologies. For example, a mix of star and bus designs. - **Pros**: Flexible and adaptable to different needs. - **Cons**: Can be complex to manage and design. ![](https://woodruff.dev/wp-content/uploads/2025/01/Chapter02-03.jpg)The choice of topology depends on the network’s size, budget, and fault-tolerance needs. ## How Routing and Topologies Work Together Think of routing and topologies as two sides of the same coin. The topology defines the physical and logical layout of the network, while routing determines how data navigates within that layout. For example: - In a **star topology**, the central hub might act as a router, directing data between devices. - In a **mesh topology**, routers ensure that data takes the most efficient path out of the many available. They shape how networks function together, ensuring data moves smoothly from point A to point B. ## Why It All Matters Understanding routing and topologies isn’t just for network engineers. If you’ve ever wondered why your video call lags or why some networks are faster than others, these concepts hold the answer. Routing ensures data gets where it needs to go, while topologies determine how devices connect and interact. Together, they create the backbone of our connected world. Whether you’re designing a new network, troubleshooting issues, or just curious how it all works, mastering these concepts is a step toward navigating the digital highways like a pro. So, the next time you stream a movie or send a file, take a moment to appreciate the intricate dance of routers and topologies working behind the scenes to make it all happen. It’s a modern marvel, and now you’re one step closer to understanding how it all works. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Decoding IP Addressing and Subnetting – The Backbone of Networking](https://www.woodruff.dev/decoding-ip-addressing-and-subnetting-the-backbone-of-networking/) **Published:** January 5, 2025 **Author:** Chris Woodruff **Excerpt:** In today's hyper-connected world, where everything from your smartphone to your coffee machine can be online, understanding networking is no longer just for IT pros. At the heart of all this connectivity lies IP addressing and subnetting—the unsung heroes of the digital age. These concepts are the foundation of how devices communicate and share data seamlessly across networks, ensuring everything works. But how does it all happen? Let's break it down. **Content:** NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). In today’s hyper-connected world, where everything from your smartphone to your coffee machine can be online, understanding networking is no longer just for IT pros. At the heart of all this connectivity lies IP addressing and subnetting—the unsung heroes of the digital age. These concepts are the foundation of how devices communicate and share data seamlessly across networks, ensuring everything works. But how does it all happen? Let’s break it down. Think of an IP address as the digital equivalent of a street address. Just like the post office uses your address to deliver mail to the right home, devices on a network use IP addresses to send and receive data. --- ## What Is IP Addressing? There are two main versions of IP addresses in use today: - **IPv4**: The older, more familiar format that looks like this: 192.168.1.1. It uses a 32-bit structure, which means there are around 4.3 billion possible addresses. Back when the internet was still new, this seemed like plenty. Spoiler alert—it wasn’t. - **IPv6**: Enter the modern solution to the address shortage. IPv6 uses a 128-bit structure, offering trillions upon trillions of unique addresses. Think of IPv6 as the unlimited buffet of IP addressing. Each IP address is divided into two parts: 1. The **network portion** identifies the network the device belongs to. 2. The **host portion** identifies the specific device on that network. This split allows devices to talk to each other within a network and across the broader internet. --- ## Subnetting: Breaking Big Networks Into Manageable Pieces Imagine a massive city where every house is on one endless street. Chaos, right? Subnetting is like dividing that city into neighborhoods. It organizes networks into smaller, more manageable pieces, improving efficiency and making network management easier. Here’s why subnetting matters: - **Efficient Use of IPs**: Many IP addresses would go unused without subnetting, especially in smaller networks. - **Improved Security**: Subnetting creates boundaries that limit the spread of potential threats within a network. - **Reduced Congestion**: By breaking networks into smaller subnets, we reduce broadcast traffic and keep things running smoothly. ![](https://woodruff.dev/wp-content/uploads/2025/01/Chapter02-02.jpg)Subnetting relies on a tool called the **subnet mask**. This mask acts as a guide, separating an IP address’s network and host portions. For example, if you have the IP address 192.168.1.25 and a subnet mask 255.255.255.0, the first three sections (192.168.1) identify the network, and the last part (.25) points to the specific device. --- ## CIDR Notation: Making IPs Easier to Manage Have you ever seen an IP address with something like “/24” at the end? That’s CIDR notation, short for Classless Inter-Domain Routing. It’s a simplified way to show the subnet mask. The “/24” means the first 24 bits of the address are for the network portion, while the rest are for hosts. Why does CIDR matter? - **It’s efficient**: CIDR allows for better IP address allocation by tailoring subnet sizes to actual needs. - **It reduces complexity**: Instead of using long, clunky subnet masks, CIDR keeps things short and sweet. For example, you might manage a network with multiple subnets. One subnet might need room for 50 devices, while another only needs 10. CIDR lets you create the perfect size for each, optimizing the use of IP addresses. --- ## Why Should You Care? Whether designing a network, troubleshooting connectivity issues, or just trying to figure out why your smart fridge won’t connect to Wi-Fi, understanding IP addressing and subnetting gives you the tools to solve problems and make systems run more efficiently. At their core, IP addressing and subnetting aren’t just technical jargon—they’re the backbone of our connected world. Mastering these concepts empowers you to design scalable, secure, and future-proof networks and inspires you to create efficient and effective solutions in the digital realm. So, the next time you see an IP address or hear the term “subnet mask,” remember this: you’re looking at the invisible threads that hold our digital universe together. And now, you’re one step closer to mastering them. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Demystifying Network Programming: The Backbone of Modern Applications](https://www.woodruff.dev/demystifying-network-programming-the-backbone-of-modern-applications/) **Published:** January 3, 2025 **Author:** Chris Woodruff **Excerpt:** Network programming might sound intimidating, but at its core, it’s the art of making applications talk to each other. Whether it’s your favorite messaging app sending texts in real-time or your smartwatch syncing health stats to your phone, network programming is the magic behind it all. It’s not just a technical skill—it’s the backbone of our interconnected digital world. **Content:** > NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). Network programming might sound intimidating, but at its core, it’s the art of making applications talk to each other. Whether it’s your favorite messaging app sending texts in real-time or your smartwatch syncing health stats to your phone, network programming is the magic behind it all. It’s not just a technical skill—it’s the backbone of our interconnected digital world. ## Where Do We Use Network Programming? The real beauty of network programming is its versatility. It powers a wide array of technologies and applications we interact with daily. Here are a few examples: - **Client-Server Applications**: From mobile banking apps to e-commerce websites, most modern applications follow the client-server model, where clients (like your browser) send requests to servers, and servers respond with the requested data. - **Web Services**: APIs and web services use network programming to enable seamless integration between different software systems. For instance, your weather app fetching forecasts relies on APIs to pull data from a remote server. - **Real-Time Communication**: Instant messaging, video calls, and live streaming heavily depend on network programming to ensure swift, reliable data exchange. - **Internet of Things (IoT)**: Smart devices like thermostats, cameras, and voice assistants communicate over networks, gathering data and performing tasks intelligently. - **Cloud Computing**: Services like Google Drive and Microsoft Azure use network programming to provide scalable, on-demand resources to users worldwide. --- ## The Foundations: What You Need to Know To truly appreciate network programming, it helps to understand a few foundational concepts: - **Sockets**: Think of sockets as endpoints where data flows in and out. They’re the virtual bridges connecting devices across a network. - **IP Addressing and Port Numbers**: These act as the GPS coordinates and doorways for devices on a network. They ensure data gets to the right place. - **Data Serialization**: When devices communicate, they need a common language. Serialization ensures data can be packaged and understood across platforms and languages. - **Packet Transmission**: Instead of sending one huge data block, networks break it into smaller chunks called packets. These packets are sent, reassembled, and delivered to the destination. --- ## Why Should You Care? If you’re a developer or someone venturing into tech, understanding network programming opens up a world of opportunities. It’s not just about writing code—it’s about building bridges between devices, services, and users. Network programming skills are in high demand, from creating APIs to developing scalable cloud applications. Understanding how the digital systems around you work can be empowering. It’s like peeling back the curtain and seeing how the magic of modern technology unfolds. This knowledge can give you a sense of control and confidence in the digital world. --- ## A Journey Worth Taking Network programming may seem complex at first, but it’s a journey worth taking. It equips you with the tools to solve real-world problems and create applications that connect people, services, and data. It’s not just a technical skill—it’s a superpower in today’s digital-first world. The journey of learning network programming is exciting and full of potential. Whether you’re looking to build your first API, create a real-time chat app, or dive into IoT, network programming lays the foundation for it all. So, roll up your sleeves, and let’s start building the digital bridges that make our world smaller and more connected. **Categories:** Network Book Sample **Tags:** Book, network, programming --- ### [Cracking the Code: A Beginner's Guide to Network Protocols](https://www.woodruff.dev/cracking-the-code-a-beginners-guide-to-network-protocols/) **Published:** January 4, 2025 **Author:** Chris Woodruff **Excerpt:** When you send a text message, watch a video online, or even check your email, countless interactions happen behind the scenes to make it all work seamlessly. These interactions rely on network protocols—a set of rules that ensures devices can talk to each other, even if they're from completely different manufacturers or built for entirely different purposes. **Content:** NOTE – This post is an example from the book **“Beyond Boundaries: Networking Programming with C# 12 and .NET 8”**. For a deeper dive into socket programming and more networking concepts, visit or get your copy of the book on [Leanpub](https://leanpub.com/csharp-networking/). When you send a text message, watch a video online, or even check your email, countless interactions happen behind the scenes to make it all work seamlessly. These interactions rely on **network protocols**—a set of rules that ensures devices can talk to each other, even if they’re from completely different manufacturers or built for entirely different purposes. Let’s explore why network protocols are essential, how they work, and the magic they bring to the digital world. ## How Do Protocols Work? Imagine you’re sending a letter to a friend. You’d follow a process: write the letter, address the envelope, and drop it in the mail. Similarly, when data travels across a network, it follows several steps, each guided by a specific protocol: 1. **Addressing**: Before data can go anywhere, it needs an address. Protocols like **IP** (**Internet Protocol**) assign every device a unique identifier, ensuring the data knows precisely where to go. 2. **Packaging and Transport**: Data doesn’t travel as one big blob. It’s broken into smaller chunks called packets. Protocols like **TCP** (**Transmission Control Protocol**) ensure these packets are delivered in the correct order and without errors. Alternatively, protocols like **UDP** (**User Datagram Protocol**) prioritize speed over reliability, making them ideal for real-time applications like video streaming or online gaming. 3. **Application-Specific Rules**: Finally, the data interacts with the application using its specific protocol. For example: - **HTTP/HTTPS** ensures web browsers and servers can communicate effectively. - **SMTP** helps send emails. - **DNS** resolves user-friendly web addresses like [www.example.com](http://www.example.com/) into numerical IP addresses. --- ## Why So Many Protocols? Not all data is created equal, and neither are its communication needs. For instance, watching a live video stream prioritizes speed over precision, so losing a tiny bit of data here and there is acceptable. On the other hand, sending an email or transferring a file demands absolute accuracy. This is why different protocols exist for different tasks: - **TCP** is like sending a package with a receipt. It guarantees the package arrives, and you’ll know if it didn’t. - **UDP** is like shouting a quick message to someone across the room. It’s fast, but it doesn’t guarantee they’ll hear every word. - **FTP** (**File Transfer Protocol**) is the go-to for transferring files. - **SMTP** (**Simple Mail Transfer Protocol**) handles sending emails, while **IMAP** and **POP3** manage receiving them. - **DNS** acts as the Internet’s phonebook, translating domain names into IP addresses. Each protocol has a specific job, ensuring the Internet runs smoothly and efficiently. --- ## Why Should You Care About Protocols? Understanding network protocols can be a game-changer, even if you’re not a network engineer. Here’s why: - **Build Better Applications**: As a developer, understanding how protocols like HTTP, TCP, and UDP work can help you design faster, more efficient apps. - **Troubleshoot Like a Pro**: Understanding the data flow helps you pinpoint issues, whether a slow-loading website or a misbehaving API. - **Appreciate the Complexity**: The next time you stream a video or make a video call, you’ll have a newfound respect for the layers of technology, making it all possible. --- ## The Layered Cake of Network Protocols: TCP/IP The most common suite of network protocols, TCP/IP, is built like a layered cake. Each layer has a specific role, and they work together to ensure smooth communication: 1. **Application Layer**: Handles user-facing tasks. Protocols like HTTP, FTP, and DNS live here. 2. **Transport Layer**: Ensures data is delivered reliably (TCP) or quickly (UDP). 3. **Internet Layer**: Handles addressing and routing data with IP. 4. **Link Laye**r: Manages the physical connection between devices, like Ethernet or Wi-Fi. 5. **Physical Layer**: Manages communication between two devices by defining both the transmission medium and how data is transmitted. ![](https://woodruff.dev/wp-content/uploads/2025/01/Chapter01-02-1024x603.png)This structure allows flexibility and scalability, making the Internet what it is today. --- ## Wrapping It Up Network protocols are the unsung heroes of our digital age. They ensure that devices across the globe can communicate effectively, enabling everything from casual chats to critical data transfers. Understanding these protocols gives you insight into the mechanics of the Internet and equips you with the knowledge to build or troubleshoot networked systems. So, the next time you load a webpage or stream a movie, take a moment to appreciate the invisible rules and conventions that make it all possible. Network protocols may not get the spotlight, but they are true connectivity champions. **Categories:** Network Book Sample **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [12 Months, 12 Books: My Yearlong Journey to Learn, Grow, and Level Up](https://www.woodruff.dev/12-months-12-books-my-yearlong-journey-to-learn-grow-and-level-up/) **Published:** December 31, 2024 **Author:** Chris Woodruff **Excerpt:** As we enter a new year, I’m focusing on personal and professional growth as a developer. The tech industry evolves at breakneck speed, and staying ahead requires more than mastering the latest tools and frameworks. To truly excel, developers must dive deep into their craft, exploring timeless programming principles, leadership, collaboration, and personal development. With their ability to distill the wisdom of experienced professionals, books remain one of the best ways to gain these insights. That’s why I’ve set a personal goal for 2025: to read one transformative book each month. **Content:** As we enter a new year, I’m focusing on personal and professional growth as a developer. The tech industry evolves at breakneck speed, and staying ahead requires more than mastering the latest tools and frameworks. To truly excel, developers must dive deep into their craft, exploring timeless programming principles, leadership, collaboration, and personal development. With their ability to distill the wisdom of experienced professionals, books remain one of the best ways to gain these insights. That’s why I’ve set a personal goal for 2025: to read one transformative book each month. This isn’t just a list of technical books—it’s a comprehensive journey to becoming a well-rounded developer. From understanding the art of writing clean code to enhancing my communication skills and reflecting on philosophical ideas that shape how we approach technology, each book represents a unique facet of my growth. By this time next year, I hope to be a better coder and a stronger leader, collaborator, and thinker. Here’s my diverse reading plan for 2025, designed to challenge and inspire me throughout the year. I asked ChatGPT about each book and why I should read it. I asked to say it in my voice to make it personal. ## **Technical Mastery** ### Programming Rust If there’s one book that will help me wrap my head around Rust, it’s Programming Rust. This language is all about safe, fast, and fearless coding, and this book dives straight into the “how” with practicality, clarity and depth. 2025 is the year I conquered Rust, and this is the guide I’ll trust to get me there. ### Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems Designing Data-Intensive Applications is an absolute must-read because it’s a masterclass on handling data at scale—my favorite thing. It breaks down the big ideas behind building reliable, scalable, and just plain smart systems. If you love data as much as I do, this book is like candy for your brain. ## **Communication & Writing** ### The Sense of Style: The Thinking Person’s Guide to Writing in the 21st Century The Sense of Style is my go-to for leveling up my writing game—it’s bright, modern, and actually fun to read. Pinker explains how to write clearly and make your ideas shine, which is a game-changer for documents, blogs, or anything else I need to communicate. This book is a no-brainer if you care about being understood (and who doesn’t?). ### On Writing Well: An Informal Guide to Writing Nonfiction On Writing Well is a timeless classic for anyone who wants to write clearly and with impact, whether for work or just life in general. It’s packed with practical tips to cut the fluff and make your words resonate. If writing feels like a chore, this book will inspire and motivate you, changing your approach and transforming your writing. ## **Philosophy of Success & Critical Thinking** ### A Philosophy of Software Design, 2nd Edition A Philosophy of Software Design is a book that makes you stop and think about how you approach building software. It’s all about reducing complexity and, more importantly, promoting joy in coding. The book’s principles can transform your codebase from a tangled mess into a work of art, making your work more enjoyable and inspiring. ### Think Smarter Think Smarter is like a workout for your brain—it’s all about boosting critical thinking skills so you can tackle problems more effectively. It’s packed with practical tips that actually make decision-making and problem-solving easier (and less stressful). If you want to level up how you approach challenges, this book is a total game-changer. ## **Leadership & Team Dynamics** ### Agile Conversations Agile Conversations is all about fixing the one thing that trips up most teams: communication. It dives into having the right kinds of conversations to build trust, tackle tough topics, and actually work better together. If you’ve ever felt like your team could do more if they just talked things through better, this book’s for you. ## **Future Looking** ### The Singularity Is Nearer: When We Merge with AI The Singularity Is Nearer is a wild ride into the future where AI and humans might actually merge—it’s like sci-fi, but with legit science. It’s packed with mind-blowing ideas about what’s coming next in tech and how it could change everything. If you’ve ever wondered where we’re heading with AI, this book will have you both excited and a little freaked out (in the best way). ### Dancing with Qubits: From qubits to algorithms, embark on the quantum computing journey shaping our future Dancing with Qubits is the perfect way to dip your toes into the mind-bending world of quantum computing without feeling totally lost. It breaks down the crazy concepts behind qubits and quantum algorithms in a way that actually makes sense. If you’re curious about the tech that’s going to shape the future, this book is your backstage pass. ## **Personal Improvement** ### Building a Second Brain Building a Second Brain is all about organizing your thoughts and ideas so you can actually use them when you need them. It’s like creating your personal system for never forgetting great ideas, tasks, or random inspiration. If your brain ever feels like it’s on overload, this book is the ultimate life hack. ### The Improv Mindset The Improv Mindset is all about rolling with the punches and thinking on your feet—skills every developer (and human) could use more of. It shows how the principles of improv can help you collaborate better, handle surprises, and just have more fun. If you’ve ever wanted to say “yes, and” to life, this book is a total game-changer. ### Deep Work Deep work is all about cutting through the noise and actually focusing on what matters, which is something I know I need more of. It’s packed with strategies to get into that flow state where the best ideas and work happen. If you’re tired of feeling distracted all the time, this book is the reset button you didn’t know you needed. # **Initial Monthly Plan (I may change this based on future needs)** - January — Building a Second Brain - February — A Philosophy of Software Design - March —On Writing Well: An Informal Guide to Writing Nonfiction - April — Think Smarter - May — Deep Work - June — Programming Rust - July — The Improv Mindset - August — Agile Conversations - September — The Sense of Style: The Thinking Person’s Guide to Writing in the 21st Century - October — Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems - November — Dancing with Qubits – Second Edition: From qubits to algorithms, embark on the quantum computing journey shaping our future - December — The Singularity Is Nearer: When We Merge with AI By tackling one book a month, I’ll embark on a profoundly personal journey of exploration, from technical mastery to leadership strategies and personal growth. I’m excited to share this journey and reflect on what I’ve learned as the year unfolds. Here’s to a 2025 filled with transformative learning and development! **Categories:** Blog **Tags:** books, developers, learning, new year, reading --- ### [I am Self-Publishing the Network Programming Book!](https://www.woodruff.dev/i-am-self-publishing-the-network-programming-book/) **Published:** March 12, 2024 **Author:** Chris Woodruff **Content:** Last September, I wrote about [starting to write a book](https://woodruff.dev/practical-network-programming-csharp/) covering C# 12 and .NET 8 for network programming. I had big hopes with a well-known publisher. It just did not work out because of issues with both sides. It could not happen. So, I took some time off, and after a few months, I am picking back up where I left off. I will write the book online, publish each chapter when I finish it, and get it reviewed by my friends. I will look for feedback and mistakes. I will ask for areas I missed that would be good additions. I do not know when each chapter will be released, but I hope to finish the 19 chapters by June if I can write that fast. Thanks to everyone who has helped and encouraged, and please enjoy “[Beyond Boundaries – Networking Programming with C# 12 and .NET 8](https://cwoodruff.github.io/book-network-programming-csharp/).” **Categories:** Blog **Tags:** .NET, Book, C#, dotnet, network, programming --- ### [Compiling Success: My Aspirations for a Transformative Year Ahead](https://www.woodruff.dev/compiling-success-my-aspirations-for-a-transformative-year-ahead/) **Published:** January 1, 2024 **Author:** Chris Woodruff **Content:** ## Introduction As we step into 2024, I find myself reflecting on the journey I’ve embarked upon in the ever-evolving field of software development and architecture. This year, I am setting 10 ambitious goals to grow and expand my life and expertise. I believe that setting clear, achievable objectives is crucial in one’s professional and personal development, and I’m excited to share these with you. ## My 10 Goals for 2024 1\. **Self-publish a book** One of my primary goals for 2024 is to self-publish a book. This project represents a significant personal and professional milestone, allowing me to share my knowledge and experiences in software development and architecture with a broader audience. I plan to cover key concepts, industry insights, and personal anecdotes that encapsulate my journey in the tech world. The process of writing, editing, and publishing will not only refine my communication skills but also deepen my understanding of the field. This endeavor is not just about putting words on a page; it’s about contributing to the community and leaving a lasting impact. By year’s end, I aim to have a published book that serves as a testament to my expertise and passion for software development. 2\. **Become a better Software Architect** In 2024, a key goal of mine is to enhance my skills and capabilities as a software architect. This involves delving deeper into advanced architectural design principles, staying abreast of emerging technologies, and continuously refining my problem-solving strategies. I aim to take on more complex projects that challenge my current understanding and push me out of my comfort zone. To achieve this, I plan to continuously learn through courses and workshops and actively participate in professional communities. Additionally, I seek mentorship from seasoned architects and collaborate more closely with diverse teams to gain different perspectives. This journey is not just about technical growth; it’s about evolving into a visionary leader who can effectively guide projects and teams toward innovative solutions and success. 3\. **Start an Open-Source project that impacts the community** One of my aspirations for 2024 is to initiate an open-source project that significantly benefits the community. This goal aims to create a platform where innovation and collaboration converge to solve real-world problems. I am committed to fostering a community that upholds the principles of open-source culture: transparency, collaboration, and meritocracy. Through this endeavor, I hope to give back to the community that has been instrumental in my growth as a developer, providing a tool that others can use, improve upon, and learn from. The success of this project will be measured not by its popularity but by its impact and the value it delivers to users and contributors alike. 4\. **Learn a new software programming language – Rust** For 2024, I have set the goal to learn Rust, a language that’s gaining traction for its performance and safety, particularly in system-level programming. My objective is to become proficient in Rust’s unique features, such as ownership, zero-cost abstractions, and strong concurrency capabilities. By mastering Rust, I aim to enhance my skill set in building reliable and efficient software, potentially contributing to areas where high performance and safety are paramount. This learning endeavor will involve dedicating time to studying the language’s syntax and semantics, actively participating in the Rust programming community, and developing small-scale projects to apply my knowledge practically. Embracing Rust will broaden my programming repertoire and prepare me for future challenges in software development, where Rust’s robustness can be a game-changer. 5\. **Improve my speaking skills** A personal objective for the year 2024 is to refine my public speaking skills, specifically tailored to conferences and podcasting. The art of articulating ideas clearly and engagingly is crucial for thought leadership in the software development sphere. I aim to develop a confident, persuasive speaking style that resonates with live audiences and podcast listeners. To achieve this, I plan to seek opportunities for public speaking, enroll in workshops, and work with a speaking coach. I also intend to engage with seasoned speakers to learn effective communication and audience retention techniques. By enhancing my speaking abilities, I aim to share my insights more impactfully and inspire and educate others in the field, leaving a lasting impression that transcends the boundaries of the stage or the airwaves. 6\. **Learn to be more patience** This year, I am dedicated to cultivating patience, recognizing that it is as much a professional asset as a personal virtue. In the fast-paced realm of technology, where quick results are often prized, learning to embrace patience can lead to more thoughtful decision-making and heightened quality of work. My strategy involves mindfulness exercises, time management practices, and setting realistic expectations for project timelines. I also plan to reflect on past experiences where patience resulted in better outcomes. By deliberately practicing patience, I aim to foster a work environment that values thoroughness and deliberation, ultimately enhancing my interactions with colleagues and the quality of the software I develop. The goal is to transform patience from a mere concept into a tangible skill that permeates all aspects of my life and work. 7\. **Help my old and new friends in the community** In 2024, I am committed to extending a helping hand to both old and new friends within my community. Recognizing the immense value of camaraderie and support in the tech industry, my goal is to be there for others, whether it’s through mentoring, collaboration, or simply offering a listening ear. I plan to volunteer my time to local coding boot camps, participate in peer programming sessions, and contribute to community-driven projects. By sharing my experiences and knowledge, I hope to empower those around me, helping them overcome obstacles and achieve their own professional goals. The aim is to cultivate a mutual support network, fostering a sense of belonging and growth that benefits all community members. 8\. **Make a difference at my job** As I embark on a new professional chapter in 2024, I aim to make a substantial impact at my new job. I aspire to go beyond just fulfilling my role; I aim to become a catalyst for positive change and innovation within the organization. My approach will be to first thoroughly understand the company’s culture, processes, and goals. Then, by leveraging my skills and experience, I plan to identify opportunities for improvement, drive efficiency, and contribute to strategic projects that align with the company’s vision. Through active collaboration, thought leadership, and a commitment to excellence, I am determined to deliver tangible results and inspire my colleagues, helping elevate our collective work to new heights. The goal is clear: to leave an indelible mark that propels the company forward and reflects a deep dedication to my new professional home. 9\. **Travel more and have more life experiences** This year, I set my sights on broadening my horizons by traveling more for the community and with my family. The goal is to enrich my life experiences, stepping outside the familiar confines of daily routines to explore new places and cultures. These journeys are not just about leisure; they’re a venture into learning and personal growth. I intend to participate in technology conferences or collaborative projects, which can deepen professional bonds. Simultaneously, I aim to create lasting memories with my family, understanding that shared experiences are the bedrock of our connection. This commitment to travel is a commitment to embracing diversity, fostering relationships, and gaining insights that will influence my worldview and professional ethos well into the future. 10\. **The most important goal is to become a better father and husband** In pursuing personal development this year, my most heartfelt goal is to become the best father and husband I can be. I understand that the keystones of this endeavor are patience, presence, and empathy. My plan is to carve out quality time dedicated to my family, ensuring that our moments together are meaningful and enriching. I will strive to listen more intently, offer support steadfastly, and nurture our family’s dreams with unwavering commitment. ## Reflecting on the Journey These goals represent a stepping stone toward becoming a more skilled and knowledgeable software developer, architect, and, more importantly, a person. They encompass a range of areas, from technical skills enhancement to project management, leadership, and personal growth. My aim is not just to achieve these goals but to learn and grow through the process of pursuing them. ## A Note of Thanks As I publish this post, I’d like to extend my heartfelt thanks to you, my friends who read my blog. Your support, feedback, and insights have been invaluable in shaping my career path. The community we’ve built together has been a source of motivation and inspiration. I look forward to sharing my journey through 2024 with you and hope that my experiences can, in some way, contribute to your own professional and personal growth. Thank you for being a part of my journey. Here’s to a successful and fulfilling 2024! **Categories:** Blog **Tags:** personal journey --- ### [The Ultimate Guide to Network Programming in C# 12 & .NET 8](https://www.woodruff.dev/practical-network-programming-csharp/) **Published:** September 6, 2023 **Author:** Chris Woodruff **Excerpt:** I am thrilled to announce my new adventure into the world of book writing with my upcoming title, "Practical Network Programming Using C#." A few months ago, I received a request about deep diving into network programming, particularly with C#. I am very passionate about this area of software development, so I agreed, and Packt will publish the book. **Content:** I am thrilled to announce my new adventure into the world of book writing with my upcoming title, “Practical Network Programming Using C#.” A few months ago, I received a request about deep diving into network programming, particularly with C#. I am very passionate about this area of software development, so I agreed, and Packt will publish the book. This book will be fantastic because it’s tailored to work with C# 12 and .NET 8, the latest and most potent versions at the time of writing. The goal is to harness the capabilities of this advanced software platform and language to provide a seamless and efficient approach to network programming. ### Here’s a sneak peek into the sections: 1. **Introduction to Network Programming** - **Beginner’s mindset:** For those who are new to network programming, this section will introduce fundamental concepts like sockets, IP addresses, ports, and the client-server model. - **C# for Networking:** Here, we’ll explore how C# plays a pivotal role in this domain, discussing the built-in libraries and tools that are at our disposal. 2. **Advanced Network Programming Techniques** - **Asynchronous Programming:** This chapter will delve into async and await, which are crucial for preventing application blockages during networking tasks. - **Advanced Socket Programming:** We’ll explore advanced topics like raw sockets, multicasting, and setting socket options. - **Building Performant and Robust Network Applications:** Crafting network applications that deliver high-speed performance and stand resilient against unexpected challenges. 3. **Network Communication and Protocols** - **Understanding Protocols:** This section will cover the basics of protocols like TCP, UDP, and HTTP, their differences, and when to use them. 4. **Network Security, Testing, and Deployment** - **Securing your Network Applications:** A deep dive into encryption, secure sockets layer (SSL), transport layer security (TLS), and more. - **Testing Network Applications:** Discuss unit testing, integration testing, and ensuring the resilience and robustness of our applications. - **Deployment and Scalability Considerations:** A look at the strategies and best practices to ensure that software applications can be smoothly launched, maintained, and expanded to handle growing user demands and workloads. ### Why C# and .NET 8? With every new version, C# and the .NET framework introduce enhancements that simplify developers’ lives. C# 12, in tandem with .NET 8, offers improved features and tools that streamline the network programming process. Using them can produce more secure, efficient, and scalable network applications. ### Who is this book for? Whether you are a beginner wanting to step into network programming or an experienced developer seeking to hone your skills with C# 12 and .NET 8, this book will be a comprehensive guide. Stay tuned for the book’s launch date and early bird offers. Let’s embark on this exciting journey of mastering network programming with C# together! **Categories:** Blog **Tags:** .NET, Book, C#, development, dotnet, network, programming --- ### [The Journey of Self-Discovery: Exploring Your True Self at 50 as a Late Bloomer](https://www.woodruff.dev/the-journey-of-self-discovery-exploring-your-true-self-at-50-as-a-late-bloomer/) **Published:** February 8, 2023 **Author:** Chris Woodruff **Excerpt:** Late blooming can refer to a person who has experienced a significant change or transformation in their life later on in their years. This change can come from a newfound passion, career, or even a new sense of self. For many late bloomers, this change can bring about a new sense of purpose and fulfillment and be a time of tremendous personal growth. **Content:** > It is never too late to be what you might have been. > > George Eliot ![starting over](https://woodruff.dev/wp-content/uploads/2023/02/new-start-fresh-start.jpg)Late blooming can refer to a person who has experienced a significant change or transformation in their life later on in their years. This change can come from a newfound passion, career, or even a new sense of self. For many late bloomers, this change can bring about a new sense of purpose and fulfillment and be a time of tremendous personal growth. For 50-year-old men who may have felt like they have not accomplished as much as they had hoped in their earlier years, becoming a late bloomer can be a time of reflection and reassessment. It’s a time to take stock of where you are in your life and what you want to achieve moving forward. This can be a time to re-evaluate your priorities and make changes that better align with your goals and desires. One of the keys to embracing a late blooming phase in life is to be open to change. This means being willing to take risks, try new things, and challenge yourself in ways you never thought possible. It can be a time to pursue hobbies or interests that you may have been too busy or too focused on other things to pursue in your earlier years. Another key to embracing a late blooming phase is embracing growth and learning. Whether through formal education or simply by exploring new topics and ideas, continuous learning can be a great way to keep your mind active and engaged. Finally, staying positive and surrounding yourself with supportive people is essential. Surrounding yourself with positive and supportive friends and family can help keep you motivated and on track and provide the encouragement you need to keep pushing forward. In conclusion, being a 50-year-old late bloomer can be a time of significant personal growth and fulfillment. By embracing change, embracing growth and learning, and staying positive, you can make the most of this new chapter in your life. So if you feel like you’ve been a late bloomer, don’t worry. Embrace it and make the most of the opportunities that come your way. **Categories:** Blog **Tags:** better me, personal --- ### [Looking at Writing Book Blurbs and How that can Help Conference Speakers](https://www.woodruff.dev/looking-at-writing-book-blurbs-and-how-that-can-help-conference-speakers/) **Published:** January 6, 2023 **Author:** Chris Woodruff **Content:** “If you are inspiring others to do better, you’re successful.” – Mod Sun I read a book recently (well, last night, really) that helped me to think about creating conference talks and the abstracts speakers have to craft when they submit their talks to CFPs. The book is [“5 Steps to Better Blurbs: Crafting Dynamic Descriptions that Sell”](https://www.amazon.com/Steps-Better-Blurbs-Crafting-Descriptions-ebook/dp/B071J9XQN4/) by [Julie C. Gilbert](http://www.juliecgilbert.com/). Julie is an author of science fiction and Christian books and freelances with other authors to help them write the blurbs on the back of the printed books and the descriptions for ebooks. She wrote the book to help other authors write better blurbs, reach more audiences, and sell more books. Julie has a lot of good ideas and an excellent process for writing book blurbs. I thought, why not take those ideas and the process to help speakers write better abstracts for their talks first to get selected to speak at conferences and finally to get more attendees to put their butts in their seats to watch and enjoy the speaker’s talks. Let’s look at some of the ideas and the process. # Breaking down the Talk Abstract like a Book Blurb Julie breaks down a book blurb into parts sections. These sections summarize what is essential to selling the book. These sections can also help us understand what is vital for the abstracts we submit to a conference and are shown for attendees to pick from among all the other talks at the conference. ## Tagline/Talk Title We all know that the title of our talk is essential since every book blurb needs an excellent tagline to attract the reader and get them to buy the book. We as speakers need to work hard to have a talk title that will also catch the conference person or team’s attention that goes through the many submissions attention and attendees going through the many talks at the conference they are paying good money to attend. How to create these titles is beyond this blog post. ## Introduce the Main Character/Talk Topic Every book has one or more main characters, and the blurb must share the main characters’ essence to make the reader connect with them. Your abstract should also give the meaning of the topic of your talk. It can go into a bit of detail on the topic, just enough to entice the viewer to stay for the rest of your abstract. Remember, less is more when writing an abstract. ## Throw the Monkey Wrench at the Main Character/Goal of the Talk What is wrong is key to any book and also your talk. As the speaker and expert on the talk topic, you must communicate why your essential idea has more to know. No one wants a lecture without knowing why it is crucial for their work or career. ## Wrap-up Question or Statement/What the Attendee will get from the talk The book blurb, in the end, has to make the reader buy the book with a why and how the main characters will be safe or accomplish their goal in the book. You also have to share what the attendees will get at the end of your talk. This final statement may be the part that pulls someone into your session that may have passed it for another. The 10 Principles to Crafting a Great Blurb or Abstract I love the overview Julie discusses for writing an excellent book blurb: ***Keep your eye on the target****: the attendee wants to attend your talk. It would be best to strike that delicate balance between enticing and overwhelming them.* OK, on to the ten principles! ### Principle #1: Generate a Great Talk Title Enough said!! Just create a fabulous title that captures the attendee’s attention and wants them to listen to your talk no matter what. ### Principle #2: Know Your Selling Points What 3-5 things the attendee needs to know about your talk? ### Principle #3: Know Your Secrets These are things that you are going to hold back from the attendee. You don’t want to give away everything, but these spoilers set the tone for your abstract. ### Principle #4: Know Your Topic Keep to the topic of your talk. Keep the excellent focus in the abstract! Don’t go off on tangents. ### Principle #5: Know Your Audience and Genre You should know this from when you created your talk. Are you targeting developers, architects, managers or maybe everyone? Know what binds your audience to the topic of the talk. ### Principle #6: Set the Correct Tone You should set a tone that matches your talk or go for a neutral, engaging vibe in your abstract. Once again, word choice will be affected by your tone. ### Principle #7: Tell a Miniature Story We use the talk title as the hook. It would be best if you also answered some questions. What’s involved in the session? What problems face the attendees that the topic can solve? Why should the attendees care about your ideas? ### Principle #8: Establish the Stakes It would help if you had some conflict in your abstract. It may sound harsh, but it’s true. Conflict drives your topic forward, challenges the attendee’s thinking, and makes your dialog memorable. ### Principle #9: Instill a Sense of Urgency Abstracts are sales pitches, but there’s no reason it should sound like you want to sell them something. It would be best if you had the attendee wish to come to your session and be excited. ### Principle #10: Dance with the Words – The Importance of Word Choice It would help if you tried different combinations of words. Word choice is essential for you’re the tone of your abstract. The English language has many words, so try a variety of them. Most words have unique contexts. Some are naturally lighter or heavier than others. Play with different words in your abstract and find the ones that best fit the one you want to deliver. # Conclusion I gained much insight from Julie’s book for helping me write better abstracts for the talks I will submit to conferences this year. The helpful information in this blog post is just some of the knowledge she shares in the book. I recommend you buy the book and learn more. **Categories:** Speaking **Tags:** creating talks, speaking, talk abstracts --- ### [I asked ChatGPT how to overcome Imposter Syndrome.](https://www.woodruff.dev/i-asked-chatgpt-how-to-overcome-imposter-syndrome/) **Published:** January 2, 2023 **Author:** Chris Woodruff **Excerpt:** How I am working to overcome Imposter Syndrome. **Content:** I suffer from Imposter Syndrome. I look at myself today and question if my voice is worth more than the great people in our community with great ideas and skills. I know I do have things to share, but I also, deep down, don’t know if they are worth enough that anyone would give a rat’s ass about them. I am trying to be better, and for nothing else, than to get the ideas out of my head, I will post them here on my blog. So to help with that, I asked [ChatGPT ](https://chat.openai.com/chat "ChatGPT ")to share what Imposter Syndrome is and how to work to overcome it. It’s good advice for an AI. I should print it out and have it on the wall behind my monitors when I am working. ***Imposter syndrome is a feeling of self-doubt and insecurity that can hold you back from reaching your full potential. It can be especially common for people who are successful or high-achievers, because they may feel like they are not qualified or deserving of their accomplishments.*** ***Here are some tips for overcoming imposter syndrome and making yourself better:*** - ***Acknowledge your accomplishments. It can be easy to dismiss your achievements as luck or to attribute them to someone else, but it’s important to recognize and own your successes. Take some time to reflect on your accomplishments and give yourself credit where it is due.*** - ***Recognize that imposter syndrome is a common experience. Many people, even those who are highly successful, have felt like imposters at some point in their lives. Knowing that you are not alone can help you feel more confident and less like an imposter.*** - ***Seek feedback from others. Ask trusted friends, colleagues, or mentors for honest feedback on your work and abilities. This can help you get a more objective perspective on your strengths and areas for improvement.*** - ***Practice self-compassion. Be kind to yourself and give yourself permission to make mistakes. Remember that everyone makes mistakes and that it’s a natural part of learning and growing.*** - ***Take action to improve yourself. Rather than dwelling on your insecurities, focus on taking action to improve yourself and your work. This can help you feel more confident and capable.*** ***By following these tips, you can begin to overcome imposter syndrome and build self-confidence in your abilities.*** **Categories:** Blog **Tags:** better me, imposter syndrome, personal --- ### [New Year and the Art of the Restart](https://www.woodruff.dev/new-year-and-the-art-of-the-restart/) **Published:** January 2, 2023 **Author:** Chris Woodruff **Excerpt:** My goals and desires fo 2023. **Content:** *“Don’t call it a comeback, I been here for years” – LL Cool J* Fuck!! I am not a big fan of me. Not that I hate myself or want to harm myself. Quite the opposite. I have disliked my choices over the last few years, primarily through the COVID pandemic. I have made some choices that I look back and see the laziness and procrastination of who I have been. I am getting rid of that person here on January 2, 2023. The thing about my life right now is that I feel I am stuck in a rut. Ruts may not be bad for short periods, but they suck if you have been in them for years. I have. I have been afraid of getting out of this deep rut because I cannot see outside of it, so the challenge to change has honestly been paralyzing. I need to adjust a few things and start turning what I feel is a large ship. I have four areas I want to lay out goals for making myself a better person and community member. First, I want to write more. I have been asked to write several books over the last couple of months. The thought of writing a book seems overwhelming, and I have spent the holidays wondering if that effort would be more valuable than just starting the blog again and writing 3-4 posts weekly. The blog seems to be a good start and will allow me to write about the many ideas I have in my head. So I have given myself a few rules for the blog. - Remember that everyone starts somewhere. No one has been an expert from day one. It’s okay to be a beginner and to make mistakes. Making mistakes is often an essential part of learning and growing. - Don’t compare yourself to others. Getting caught up in comparing your blog to others is easy, especially if you’re just starting. But it’s important to remember that everyone has their journey and unique voice. Focus on your blog and your own goals rather than trying to compete with others. - Be authentic to yourself. The best blogs are those that are authentic and genuine. Don’t try to be someone you’re not or write about things that don’t interest you. Instead, focus on sharing your thoughts, experiences, and perspective. - Don’t be afraid to ask for help. If you’re feeling overwhelmed or uncertain, don’t hesitate to reach out to others for help or advice. - Keep at it. Starting a new blog can be challenging, and it’s natural to feel like giving up sometimes. But if you’re passionate about blogging, it’s worth sticking with it. Keep writing, keep learning, and eventually, you’ll find your footing and build an audience. The next area I am to build better habits is to start a new podcast. I will not give too many details about the new venture at this time, but it will be a fun experience for whoever listens, the guest and myself. A future blog post will be coming soon. I also want to lay out my three new tech skills to learn for 2023. I have been doing this for the last ten years with poor to decent success. My push is to pick three new things to be better at and be a better technologist. This year the areas of improvement will be: - Learn the [programming language of Rust](https://www.rust-lang.org/ "programming language of Rust"). I have been interested in this language for a few years and want to learn something more at the metal of programming. Not interested in relearning C++, so Rust seems a good fit. - Be a better front-end developer. I need to learn more about CSS and designing the UX of applications. I don’t want to be an expert, but I need more knowledge in this space to be a better overall developer. - The last area of exploration and discovery is to gain more skill in creating video content. I have purchased licenses for several years for Techsmith’s Camtasia product to create videos. I will be creating a series about my [ASP.NET Web API workshop](https://woodruff.dev/asp-net-web-api-workshop/ "ASP.NET Web API workshop") this year, so this skill will be necessary. - The last area of improvement is to improve my mental and physical state. I am not in a poor state for either, but I feel I am not where I should be in terms of exercising and eating better. I also need to improve what is between my ears, and I am looking at meditation. By documenting my wants for 2023, I hope I am setting some reasonable goals to reach, and in 12 months, I can look back and feel that I got better for my family, my career, our community and fundamentally myself. **Categories:** Blog **Tags:** 2023, goals, personal --- ## Pages ### [Home](https://www.woodruff.dev/) **Published:** October 27, 2022 **Author:** Chris Woodruff **Content:** [ ![Logo](https://www.woodruff.dev/wp-content/themes/iteck/images/logo.png ) ![Logo](https://www.woodruff.dev/wp-content/themes/iteck/images/logo-white.png ) ](https://www.woodruff.dev/) - [Home](https://www.woodruff.dev/) - [Services](#) - [Fractional Architect](https://www.woodruff.dev/fractional-architect/) - [Software Forensic Expert Witness](https://www.woodruff.dev/expert-witness/) - [Micro-Consulting](https://www.woodruff.dev/micro-consulting/) - [Project-Based Contracts](https://www.woodruff.dev/project-based-contracts/) - [Retainer-Based Services](https://www.woodruff.dev/retainer-based-services/) - [Advisory & Board Roles](https://www.woodruff.dev/advisory-board-roles/) - [Blog & Insights](https://woodruff.dev/category/blog/) - [Contact](https://www.woodruff.dev/contact/) - [Press & Media](https://www.woodruff.dev/press-media/) - [About](https://www.woodruff.dev/about/) [](#)- [Home](https://www.woodruff.dev/) - [Services](#) - [Fractional Architect](https://www.woodruff.dev/fractional-architect/) - [Software Forensic Expert Witness](https://www.woodruff.dev/expert-witness/) - [Micro-Consulting](https://www.woodruff.dev/micro-consulting/) - [Project-Based Contracts](https://www.woodruff.dev/project-based-contracts/) - [Retainer-Based Services](https://www.woodruff.dev/retainer-based-services/) - [Advisory & Board Roles](https://www.woodruff.dev/advisory-board-roles/) - [Blog & Insights](https://woodruff.dev/category/blog/) - [Contact](https://www.woodruff.dev/contact/) - [Press & Media](https://www.woodruff.dev/press-media/) - [About](https://www.woodruff.dev/about/) [ Download CV ](https://woodruff.dev/wp-content/uploads/2026/03/Chris_Woodruff_Resume.pdf) Fractional Architect • Strategic Technology Advisor • Expert Witness # I’m Chris Woody Woodruff [ Linkedin ](https://www.linkedin.com/in/chriswoodruff/) [ Github ](https://github.com/cwoodruff) [ Youtube ](https://www.youtube.com/@ChrisWoodruff) [ Rss ](https://woodruff.dev/feed/) [ Whatsapp ](https://wa.me/16167246885) [ Reddit ](https://www.reddit.com/user/chriswoodruff/) [ Podcast ](https://thewoodyshow.com) ![Portrait of a smiling, bald man with a gray beard and glasses wearing a dark button-up shirt.](https://www.woodruff.dev/wp-content/uploads/2026/07/Chris-Woodruff-01-scaled.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item.png) ##### About Me I help teams untangle complexity in software systems—whether it's modernizing aging platforms, scaling new cloud-native solutions, or explaining technical decisions in high-stakes legal cases. After two decades building distributed systems with .NET, C#, and Azure, I’ve shifted from full-time execution to strategic impact. Today, I work as a fractional architect, technology advisor, and software expert witness, offering focused expertise exactly when and where it's needed. I work with companies navigating change—platform rewrites, cloud transitions, or critical architecture decisions. I also support legal teams and litigators with technical assessments, codebase analysis, and expert opinions in software-related litigation. ![](https://www.woodruff.dev/wp-content/uploads/2025/03/jbcc-badge-150x150.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/2025-microsoft-most-valuable-professional-mvp-150x150.png) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/ab_13_1.png) ###### Full Name Christopher Woodruff ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/ab_13_2.png) ###### Email Address chris@woodruff.dev ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/ab_13_3.png) ###### Phone +1 616.724.6885 ###### news ## Blog & Insights ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item-1024x1024.png) [Entity Framework Core](https://www.woodruff.dev/category/blog/efcore/) [###### The N+1 Query Problem in EF Core: Detection, Diagnosis, and Permanent Fixes ](https://www.woodruff.dev/the-n1-query-problem-in-ef-core/) An order summary page renders in 40 milliseconds against your development database. The same page takes eleven seconds in production. You open the profiler, and the controller method looks clean. You reread the LINQ, and it reads correctly. You check the indexes, and every column you need is covered... [Read More](https://www.woodruff.dev/the-n1-query-problem-in-ef-core/) [Entity Framework Core](https://www.woodruff.dev/category/blog/efcore/) [###### Add vs AddRange in EF Core: The Performance Myth You Need to Stop Repeating ](https://www.woodruff.dev/add-vs-addrange-in-ef-core-the-performance-myth-you-need-to-stop-repeating/) Somebody told you never to call Add() in a loop. Maybe it was a senior developer during code review. Maybe it was a Stack Overflow answer with four hundred upvotes. Maybe it was a blog post that ranks on page one for “improve entity framework performance.” The advice sounded authoritativ... [Read More](https://www.woodruff.dev/add-vs-addrange-in-ef-core-the-performance-myth-you-need-to-stop-repeating/) [Entity Framework Core](https://www.woodruff.dev/category/blog/efcore/) [###### 5 EF Core Performance Anti-Patterns That Entity Framework Extensions Eliminates ](https://www.woodruff.dev/5-ef-core-performance-anti-patterns-that-entity-framework-extensions-eliminates/) The Code You Already Wrote Every .NET team has at least one of these in production. It looked fine in review. It passed unit tests. It worked in staging against the seed data. Then real traffic hit, and now somebody is on call at three in the morning trying to work out why the nightly job has been r... [Read More](https://www.woodruff.dev/5-ef-core-performance-anti-patterns-that-entity-framework-extensions-eliminates/) [ More Blog Posts & Insights ](https://woodruff.dev/category/blog/) ###### services ## Essential Services ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item-1024x1024.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/architect.png) ##### [Fractional Architect/Leader](https://woodruff.dev/fractional-architect/) Bring seasoned architectural strategy to your team when you need it most, from roadmap to execution. High-impact technical leadership—without the full-time overhea [ Service Details ](https://woodruff.dev/fractional-architect/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/retainer.png) ##### [Retainer-Based Services](https://woodruff.dev/retainer-based-services/) Ongoing architecture support, available when your team needs clarity, feedback, or direction. Trusted guidance on call—because tech challenges don’t follow a schedule [ Service Details ](https://woodruff.dev/retainer-based-services/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/contract-projects.png) ##### [Project-Based Contracts](https://woodruff.dev/project-based-contracts/) I lead and execute focused architecture and modernization efforts with start-to-finish accountability. Clear deliverables, defined outcomes, real momentum [ Service Details ](https://woodruff.dev/project-based-contracts/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/expert-witness.png) ##### [Software Forensic Expert Witness](https://woodruff.dev/expert-witness/) Unbiased, expert analysis of software systems for litigation, IP disputes, and contract breakdowns. When code is on trial, I help make the technical case clear [ Service Details ](https://woodruff.dev/expert-witness/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/advisory.png) ##### [Advisory & Board Roles](https://woodruff.dev/advisory-board-roles/) I advise leadership teams on platform evolution, scaling, technical risk, and innovation. Strategic tech insight where it matters most—at the decision table [ Service Details ](https://woodruff.dev/advisory-board-roles/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/micro-consulting.png) ##### [Micro-Consulting](https://woodruff.dev/micro-consulting/) Fast, focused sessions to resolve architecture questions, performance issues, or critical forks in the road. One hour. One problem. Real answers [ Service Details ](https://woodruff.dev/micro-consulting/) ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/10/pattern1.png) ###### portfolio ## My Projects ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item-1024x1024.png) - All Works - Books - Podcasts - Workshops - Courses - Open-Source [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### Course ### Terraform Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) ](https://www.woodruff.dev/portfolio/terraform-workshop/) [###### Course ### Entity Framework Core Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [###### Course ### ASP.NET Web API Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [###### Podcast ### The Breakpoint Show ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/the-breakpoint-show.png) ](https://www.woodruff.dev/portfolio/the-breakpoint-show/) [###### Podcast ### The Woody Show ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/the-woody-show.png) ](https://www.woodruff.dev/portfolio/the-woody-show/) [###### Book ### ASP.NET Core Reimagined with htmx ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-core-htmx-book.png) ](https://www.woodruff.dev/portfolio/aspnet-core-htmx-book/) [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### Course ### Terraform Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) ](https://www.woodruff.dev/portfolio/terraform-workshop/) [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### Course ### Terraform Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) ](https://www.woodruff.dev/portfolio/terraform-workshop/) [###### Course ### Entity Framework Core Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### ### Entity Framework Core Course ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-course.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-course/) [###### Course ### htmx & Razor Pages Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [###### Course ### Terraform Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) ](https://www.woodruff.dev/portfolio/terraform-workshop/) [###### Course ### Entity Framework Core Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [###### Course ### ASP.NET Web API Workshop ![](https://itecktheme.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/39.png) ![](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [ More Works ](#0) ###### testimonials ## Users Feedback ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item-1024x1024.png) ![](https://woodruff.dev/wp-content/uploads/2025/08/natalie.jpeg) ###### “I had the pleasure of working with Chris on several projects for one of our key clients. Chris is a true professional. His technical skills are expert level. Chris is a strong team lead who goes above and beyond to ensure his team's success. Chris is highly sought after by industry experts and is in high demand. If you have an opportunity to leverage Chris in your organization, I would highly recommend him.” Natalie Greenwood / Global Senior Director of Advisory Services ![](https://woodruff.dev/wp-content/uploads/2025/08/ted.jpg) ###### “ User feedback is qualitative & quantitative data from customers on their likes, dislikes ” Ted Neward / Architect/Leader ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2022/11/3.jpeg) ###### “ Impressions, and requests about a product. Collecting and making sense of user feedback is critical. ” Dekson P. Pablo / CEO At Brator ###### call to action ## Any Project On Mind Whether in the boardroom or the courtroom, I bring clarity to complex software challenges—and help people make better decisions through better understanding. Let’s work together to solve what’s slowing you down. Address : Wyoming, MI 4941 Phone : +1 616.724.6885 Email : chris@woodruff.dev ![](https://www.woodruff.dev/wp-content/uploads/2022/10/item-1024x1024.png) Name Email Phone Company Subject Your inquiry aboutFractional LeadershipAdvisory & Board RolesConsultingLegal Work Message (optional) This form uses Akismet to reduce spam. [Learn how your data is processed.](https://akismet.com/privacy/) Δ --- ### [About](https://www.woodruff.dev/about/) **Published:** September 24, 2022 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)About # Chris Woody Woodruff ![](https://www.woodruff.dev/wp-content/uploads/2023/01/head7_rock.png) ![Chris Woody Woodruff](https://www.woodruff.dev/wp-content/uploads/2026/07/Chris-Woodruff-head.png) ## I Work For Your Incredible Success - My Mission - My Vision - My Goal **To simplify the complex and empower others through clear, strategic software architecture and honest technical insight.** Whether I'm advising a team, reviewing a system, or testifying in court, my mission is to bring clarity, value, and forward momentum to every challenge I take on. [ Learn More ](https://woodruff.dev/contact/) Email [chris@woodruff.dev](#0) **To be the trusted expert that teams, leaders, and legal professionals turn to when software decisions, and disputes, really matter.** I envision a world where great architecture is accessible, where legacy doesn’t mean liability, and where technical truth can be clearly communicated and confidently acted upon. [ Learn More ](#0) Email [chris@woodruff.dev](https://woodruff.dev/contact/) **To deliver meaningful impact, through every project, consult, or conversation, by showing that thoughtful architecture, expert guidance, and simplicity-first thinking lead to lasting success.** [ Learn More ](https://woodruff.dev/contact/) Email [chris@woodruff.dev](#0) ![](https://www.woodruff.dev/wp-content/uploads/2023/01/about2_pattern_l.png) ![](https://www.woodruff.dev/wp-content/uploads/2023/01/about2_pattern_r.png) ##### BIO Chris “Woody” Woodruff has spent more than 25 years making complex software simpler. A Microsoft MVP in .NET and Web Development (first awarded 2008, re-awarded 2025) and a director on the .NET Foundation board, he has architected distributed systems for companies from startups to Rocket Mortgage, served as a software forensics expert witness, and taught thousands of developers through 50+ conference talks across North America and Europe. He is the author of ASP.NET Core Reimagined with htmx and is writing a second book on network programming with C# and .NET. He co-hosts The Breakpoint Show podcast and publishes The Simplicity-First Review, a newsletter on architecture judgment in the age of AI. Today, Woody works as a fractional architect and leads the emerging practice of Agentic Relations™, helping companies make their APIs, documentation, and SDKs legible to AI coding agents like Claude, Copilot, and Cursor. His thesis is constant across all of it: as AI writes more of the world’s software, the scarce skill is knowing what not to build. He lives in West Michigan and takes his bourbon neat. --- ### [Press & Media](https://www.woodruff.dev/press-media/) **Published:** May 16, 2022 **Author:** Chris Woodruff **Content:** ## Press & Media Where has Woody Has Spoken and Shared Ideas ![](https://woodruff.dev/wp-content/uploads/2025/08/TechBullion-Transparent-Logo.png) ### Architecting Scalable Cloud Solutions for the Modern Enterprise Chris Woody Woodruff shares his journey and insights on building scalable, cloud-native solutions for modern enterprises—bridging decades of experience with today’s Azure-driven innovations. [ ](https://techbullion.com/chris-woody-woodruff-architecting-scalable-cloud-solutions-for-the-modern-enterprise/) ![](https://woodruff.dev/wp-content/uploads/2025/03/CDF.png) ### Rust-ifying Your C# Codebase: A Tale of Adventure and Transformation C# developers: Thinking about learning Rust? This talk by Chris Woodruff breaks down the why, how, and what to watch for. [ ](https://www.youtube.com/watch?v=K4DP21OlktM) ![](https://woodruff.dev/wp-content/uploads/2025/03/jetbrains.png) ### Enhancing ASP.NET Core Razor Pages With HTMX – A Simplicity-First Approach Discover how HTMX supercharges Razor Pages—bringing modern interactivity without the JavaScript bloat. [ ](https://www.youtube.com/watch?v=si_U3Umqtm8&t=4s) ![](https://woodruff.dev/wp-content/uploads/2025/03/TheAzure_3000.png) ### Discussion on C# Network Programming From sockets to SignalR, Chris Woodruff explores the power of C# for network programming—and why he wrote the book developers needed. [ ](https://www.youtube.com/watch?v=iHnAULyXGwM) ![](https://woodruff.dev/wp-content/uploads/2025/03/simplicityfirst.png) ### Kill the Bloat: The Controversial Clash Between SPAs, Server-Side Rendering, and the Power of Simplicity [ ](https://simplicity-first.dev/kill-the-bloat/) ![](https://woodruff.dev/wp-content/uploads/2025/03/simplicityfirst.png) ### Seizing Opportunities Through Simplicity: A Simplicity-First Approach [ ](https://simplicity-first.dev/seizing-opportunities-through-simplicity/) ![](https://woodruff.dev/wp-content/uploads/2025/03/simplicityfirst.png) ### Unlocking Business Growth Through Simplicity: A Simplicity-First Approach for Stakeholders and Investors [ ](https://simplicity-first.dev/unlocking-business-growth-through-simplicity/) ![](https://woodruff.dev/wp-content/uploads/2025/03/simplicityfirst.png) ### Simplicity Meets Sustainability: Aligning the Simplicity-First Initiative with Green Software Principles [ ](https://simplicity-first.dev/aligning-the-simplicity-first-initiative-with-green-software-principles/) ![](https://woodruff.dev/wp-content/uploads/2025/04/infoworld_logo.jpeg) ### How AI is transforming IDEs into intelligent development assistants [ ](https://www.infoworld.com/article/3849532/how-ai-is-transforming-ides-into-intelligent-development-assistants.html) ![](https://woodruff.dev/wp-content/uploads/2025/04/angularplusshow.png) ### The Angular Show - From Idea to Mic: Writing Winning Talk Proposals [ ](https://podtail.com/en/podcast/the-angular-show/s9e3-from-idea-to-mic-writing-winning-talk-proposa/) ![](https://woodruff.dev/wp-content/uploads/2025/05/dotnet-rocks.png) ### .NET Rocks! - C# Networking with Chris Woodruff [ ](https://podtail.com/en/podcast/-net-rocks/c-networking-with-chris-woodruff-2025-05-22/) ![](https://woodruff.dev/wp-content/uploads/2025/09/Logo-b-center-1024x374-1.png) ### Rust-ifying Your C# Codebase: A Tale of Adventure and Transformation [ ](https://youtu.be/K4DP21OlktM?si=lOtcvuwv_I1MGL71) --- ### [Retainer-Based Services](https://www.woodruff.dev/retainer-based-services/) **Published:** August 14, 2025 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Retainer-Based Services # Retainer-Based Services ![](https://www.woodruff.dev/wp-content/uploads/2025/08/retainer-based-services-header-1024x683.png) ### Service Overview **Consistent access to trusted expertise… on your schedule.** When you need ongoing architectural support, strategic input, or just a reliable expert to call when it counts, a retainer-based engagement offers flexibility with stability. I partner with your team on a regular cadence, helping you make confident decisions, stay aligned with best practices, and avoid costly missteps—without the complexity of formal projects or contracts. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### Our Work Process **Lightweight, reliable, and designed around your team’s rhythm.** - **Step 1 – Agreement:** We define the scope, time allocation (monthly or weekly), and communication channels. - **Step 2 – Onboarding:** I get familiar with your systems, roadmap, and team so I can provide high-leverage input quickly. - **Step 3 – Ongoing Support:** I provide consistent availability for architecture reviews, strategy calls, mentoring, or technical deep dives as needed. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **Help when you need it... without delay.** - Need help validating technical decisions before they’re locked in? - Running into recurring architecture or scaling issues? - Unsure how to grow your platform without introducing new risk? With a retainer in place, I’m just a message or call away—ready to provide calm, experienced guidance on demand. ### Going Beyond the Usual **Predictable Expertise:** Regular access to senior-level insight without unpredictable consulting fees **Relationship-Driven:** I build context over time, so I can offer advice tailored to your history and goals. **Proactive Support:** I don't just wait for questions—I help you spot risks, seize opportunities, and think ahead ## [Prev Service Project-Based Contracts](https://woodruff.dev/project-based-contracts/) ## [Next Service Advisory & Board Roles](https://woodruff.dev/advisory-board-roles/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [Project-Based Contracts](https://www.woodruff.dev/project-based-contracts/) **Published:** August 14, 2025 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Project-Based Contracts # Project-Based Contracts ![](https://www.woodruff.dev/wp-content/uploads/2025/08/project-based-contracts-header-1024x683.png) ### Service Overview **Focused outcomes. Defined scope. Real results.** When you need a clear technical deliverable, a project-based engagement is the best fit. Whether it’s an architecture redesign, performance remediation, or a modernization roadmap, I deliver end-to-end technical work with a defined beginning, middle, and end, and clear success criteria. You get clarity, velocity, and high-quality results, without uncertainty or scope creep. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### Our Work Process **Structure meets flexibility—engineered for impact.** - **Step 1 – Project Definition:** We collaborate to outline goals, scope, success metrics, and timeline. - **Step 2 – Execution:** I execute technical deliverables, collaborate with your team, and adjust to new findings as needed. - **Step 3 – Delivery & Transition:** You get clean handoffs, documentation, and implementation support to ensure lasting value. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **Projects often begin where pain begins.** - Legacy systems slowing you down? - Need a fresh set of eyes on a redesign, integration, or cloud migration? - Unclear how to scale a brittle architecture? We define a fixed project that addresses the core issues—and I deliver a focused, high-value solution on your terms. ### Going Beyond the Usual **Scalable Structure:** From single-phase fixes to phased multi-month efforts, I meet your needs without bloated process **Built for Handoff:** I make sure your team is set up to succeed, with documentation and coaching included. **Strategic Input Included:** You don’t just get the work—you get senior-level insight baked into every deliverable. ## [Prev Service Micro-Consulting](https://woodruff.dev/micro-consulting/) ## [Next Service Retainer-Based Services](https://woodruff.dev/retainer-based-services/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [Micro-Consulting](https://www.woodruff.dev/micro-consulting/) **Published:** August 14, 2025 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Micro-Consulting # Micro-Consulting ![](https://www.woodruff.dev/wp-content/uploads/2025/08/micro-consulting-header-1024x683.png) ### Service Overview **High-value insights—without the overhead.** Micro-consulting is designed for teams who need answers fast. Whether you’re facing a critical decision, a complex technical challenge, or just need a second set of expert eyes, I offer focused consulting engagements that deliver clarity and direction within hours, not weeks. No long-term contracts, no fluff—just solutions. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### My Work Process **Quick to engage, fast to deliver value.** - **Step 1 – Discovery:** We identify the issue or decision point during a short intake call. - **Step 2 – Engagement:** I dive into the context—code, architecture, performance, or design—depending on your need. - **Step 3 – Actionable Guidance:** Within a defined timebox, I deliver clear, practical recommendations you can act on immediately. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **From confusion to clarity—fast.** - Architecture or performance bottleneck? - Debating a tech stack or refactor decision? - Need an expert code review or design sanity check? I step in with experience, speed, and focus to help your team unblock and move forward—often in just one session. ### Going Beyond the Usual **Speed with Substance:** You get actionable feedback fast—grounded in real architectural experience. **No Waste, No Wait:** You don't need weeks of onboarding or hand-holding. We get right to the problem. **Mentorship Built In:** I explain not just what to do—but why—so your team grows with every session. ## [Prev Service Software Forensic Expert Witness](https://woodruff.dev/expert-witness/) ## [Next Service Project-Based Contracts](https://woodruff.dev/project-based-contracts/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [Fractional Architect](https://www.woodruff.dev/fractional-architect/) **Published:** September 28, 2022 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Fractional Architect # Fractional Architect ![](https://www.woodruff.dev/wp-content/uploads/2025/08/fractional-architect-header-1024x683.png) ### Service Overview **High-impact software architecture… when and how you need it.** Not every organization needs a full-time architect, but every growing team needs architectural guidance. As a Fractional Architect, I partner with you to provide technical leadership, platform strategy, and architectural clarity—on a flexible schedule and at a fraction of the cost of a permanent hire. Whether you’re building something new or fixing something fragile, I help ensure your software decisions scale with your business. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### My Work Process A practical, lightweight approach to driving technical clarity. **Step 1 – Discovery & Alignment**We define your goals, pain points, and existing tech landscape in a focused kickoff session. **Step 2 – Architecture Engagement**I embed with your team on a fractional basis, typically 4–16 hours per week, providing guidance, architecture reviews, decision support, and mentoring. **Step 3 – Iteration & Scaling** As your needs evolve, I adjust my involvement. Stepping in deeper during transitions or staying high-level when your team is stable. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **When things break down, I help find what’s broken and why.** Sometimes systems slow, teams stall, or architectural choices don’t age well. When that happens, I come in to assess and stabilize: - Conduct deep-dive technical assessments - Identify architecture bottlenecks or failure points - Guide teams through restructuring decisions - Deliver actionable recommendations with no fluff Think of it as an “architecture intervention” backed by experience and empathy. ### Going Beyond the Usual **Mentorship for Your Team -** I don’t just make decisions. I explain them. Your engineers grow as I guide. **Bridge Between Business and Dev** - I translate goals into scalable architecture, while keeping leadership in the loop. **Flexible by Design** - I adapt to your pace, team size, and current state. Whether that’s chaos or calm. ## [Prev Service Advisory & Board Roles](https://woodruff.dev/advisory-board-roles/) ## [Next Service Software Forensic Expert Witness](https://woodruff.dev/expert-witness/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [Software Forensic Expert Witness](https://www.woodruff.dev/expert-witness/) **Published:** August 14, 2025 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Software Forensic Expert Witness # Software Forensic Expert Witness ![](https://www.woodruff.dev/wp-content/uploads/2025/08/expert-witness-header-1024x683.png) ### Service Overview **Clear, objective software analysis—when the stakes are high.** When software is at the center of a legal dispute, clarity matters. As a Software Forensic Expert Witness, I bring decades of architectural and development experience to uncover what happened, how it happened, and why it matters. Whether it’s a code quality issue, a system failure, or an IP or licensing dispute, I provide in-depth technical analysis, expert reports, and courtroom-ready insights that attorneys and judges can trust. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### My Work Process **Thorough, defensible, and communication-first.** - **Step 1 – Legal & Technical Intake:** We start with a consultation to understand the case, legal questions, and scope of the technical issues. - **Step 2 – Discovery & Analysis:** I review source code, system architecture, documentation, and related evidence to identify key facts and technical patterns. - **Step 3 – Expert Reporting & Testimony:** You’ll receive clear, objective reports that explain complex issues without jargon. I’m available for deposition and trial testimony as needed. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **When the story isn’t clear from the code, I help find the truth.** - Identify whether a failure was caused by poor design, bad code, or integration issues - Evaluate software deliverables against contract or industry standards - Assess ownership, licensing, or originality in source code - Validate or challenge claims of IP misuse, negligence, or technical misrepresentation Whether you're defending or prosecuting, I help you make your case with confidence and clarity. ### Going Beyond the Usual **Developer-to-Attorney Translation:** I break down technical details in language that attorneys, juries, and judges can understand—without losing the nuance. **Deep Technical Credibility:** 25+ years in architecture and engineering, with books, talks, and real-world systems to back up every analysis. **Professionalism Under Pressure:** I’ve worked in high-stakes environments and bring calm, focused clarity to litigation—even under cross-examination. ## [Prev Service Fractional Architect](https://woodruff.dev/fractional-architect/) ## [Next Service Micro-Consulting](https://woodruff.dev/micro-consulting/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [Contact](https://www.woodruff.dev/contact/) **Published:** May 13, 2022 **Author:** Chris Woodruff **Content:** ## Get In Touch I will contact you after receive your request in 24h ## +1 616.724.6885 #### chris@woodruff.dev #### Wyoming MI 49418 Set up a 1:1 with Chris Woody Woodruff today! The field is required mark as \* Name Email Phone Company Subject Your inquiry aboutFractional LeadershipAdvisory & Board RolesConsultingLegal Work Message (optional) This form uses Akismet to reduce spam. [Learn how your data is processed.](https://akismet.com/privacy/) Δ --- ### [Advisory & Board Roles](https://www.woodruff.dev/advisory-board-roles/) **Published:** August 14, 2025 **Author:** Chris Woodruff **Content:** [Home](https://www.woodruff.dev)[Page](https://www.woodruff.dev//)Advisory & Board Roles # Advisory & Board Roles ### Service Overview **Strategic technical insight at the leadership level.** ![](https://www.woodruff.dev/wp-content/uploads/2025/08/advisory-board-roles-header-1024x683.png) As a technology advisor or board member, I bring a software architect’s mindset to strategic planning, platform decisions, and innovation initiatives. I help organizations navigate complex technical landscapes, assess risk, validate decisions, and future-proof their technology direction—all while aligning with business goals. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/34.png) ##### Our Work Process **Embedded strategically, engaged intentionally.** - **Step 1 – Alignment & Onboarding:** I meet with leadership to understand your mission, technology position, and growth trajectory. - **Step 2 – Active Participation:** I contribute in advisory meetings, strategy sessions, or board discussions, providing clear, actionable guidance on technical matters. - **Step 3 – Ongoing Insight:** I remain available for input between meetings to help leadership make informed decisions and avoid costly missteps. ![](https://iteck.themescamp.com/freelance-personal/wp-content/uploads/sites/22/2023/01/35.png) ##### Troubleshooting Process **Seeing the big picture when others are too close to the code.** - Uncertain how to scale or sunset legacy systems? - Need help navigating technical debt while planning new product investments? - Lacking internal architectural expertise at the leadership table? I provide clear, executive-level insight that connects technical strategy to business outcomes. ### Going Beyond the Usual **Cross-Functional Perspective:** I bridge the gap between engineering, product, and business leadership with clarity and context. **Deep Technical Expertise:** Decades of experience building, scaling, and evaluating platforms across industries **Forward-Thinking Guidance:** I help teams anticipate what's next—not just react to what's broken. ## [Prev Service Retainer-Based Services](https://woodruff.dev/retainer-based-services/) ## [Next Service Fractional Architect](https://woodruff.dev/fractional-architect/) ##### Services [ Fractional Architect ](https://woodruff.dev/fractional-architect/) [ Software Forensic Expert Witness ](https://woodruff.dev/expert-witness/) [ Micro-Consulting ](https://woodruff.dev/micro-consulting/) [ Project-Based Contracts ](https://woodruff.dev/project-based-contracts/) [ Retainer-Based Services ](https://woodruff.dev/retainer-based-services/) [ Advisory & Board Roles ](https://woodruff.dev/advisory-board-roles/) --- ### [404 Page](https://www.woodruff.dev/404-page/) **Published:** September 27, 2022 **Author:** Chris Woodruff **Content:** ![](https://www.woodruff.dev/wp-content/uploads/2023/01/rocket-1.png) ## Opps! Looks Like Here is Nothing. The page you’re looking for isn’t found. We suggest you back to home. It’s easy... [ Back To Home ](#0) ![404_1](https://www.woodruff.dev/wp-content/uploads/2023/01/404_1.png "404_1") --- ### [Books](https://www.woodruff.dev/books-2/) **Published:** February 24, 2025 **Author:** Chris Woodruff **Content:** ## Books ![](https://www.woodruff.dev/wp-content/uploads/2025/01/Book-Cover-Small.png) ## Beyond Boundaries - Networking Programming with C#12 and .NET 8 **“Beyond Boundaries – Networking Programming with C# 12 and .NET 8”** is your ultimate guide to mastering the art of network programming in modern .NET applications. Designed for developers of all skill levels, this book takes you on a journey through networking fundamentals. It gradually builds to advanced topics, enabling you to create efficient, scalable, and secure networked applications. Harnessing the power of **C# 12** and **.NET 8**, this book combines practical examples with real-world use cases, ensuring you understand the concepts and know how to apply them. Whether you’re working on APIs, real-time applications, message queues, or exploring cutting-edge protocols like QUIC, this book covers everything. [ Leanpub ](https://leanpub.com/csharp-networking/) [ Amazon ](https://www.amazon.com/dp/B0DRST83WX?ref=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&ref_=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&social_share=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&skipTwisterOG=1&bestFormat=true&newOGT=1) ## What You'll Learn 1. **Core Networking Concepts**: Gain a solid understanding of networking fundamentals and how they are applied in .NET applications. 2. **RESTful APIs and WebSockets**: Learn to create powerful APIs and real-time solutions with ASP.NET Core 8**.** 3. **Advanced Networking Topics**: Dive into WebHooks, SignalR, and message queuing for building robust communication layers. 4. **Emerging Protocols**: Explore the future of networking with QUIC and how to implement it in your applications. 5. **Security Best Practices**: Understand how to secure your networked applications against common threats. 6. **Performance and Scalability**: Discover techniques to optimize and scale your networked solutions effectively. Each chapter builds on the last, giving you a structured learning path with hands-on examples that you can directly apply to your projects. From foundational knowledge to future trends, this book equips you with the skills you need to stay ahead in the evolving field of network programming. ![ASP.NET Core Reimagined with htmx](https://www.woodruff.dev/wp-content/uploads/2025/04/ChatGPT-Image-Apr-1-2025-02_41_21-PM-683x1024.png) ## ASP.NET Core Reimagined with HTMX I’m thrilled to announce my new book, **“htmx Essentials for ASP.NET Core Developers”**—a guide to mastering dynamic, server-side interactions with Razor Pages. This book explores the power of **htmx** and how it can transform the way we build interactive web applications with ASP.NET Core. Whether you’re a seasoned developer or just getting started with Razor Pages, this book will provide practical insights and hands-on examples to elevate your skills. Here’s the twist: I’m **releasing each chapter online** as I write! You can follow the journey and access the chapters in real time at . Once the book is complete, it will be available as a full ebook. If you’re curious about unlocking the full potential of server-side interactivity, now is the time to dive in! Let me know your thoughts, and feel free to share this with anyone who could benefit. Let’s build better web applications together! [ Leanpub ](https://leanpub.com/csharp-networking/) [ Amazon ](https://www.amazon.com/dp/B0DRST83WX?ref=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&ref_=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&social_share=cm_sw_r_ffobk_cp_ud_dp_J98BTBNA8153AQEVFS2V&skipTwisterOG=1&bestFormat=true&newOGT=1) ## What You'll Learn 1. **Core Networking Concepts**: Gain a solid understanding of networking fundamentals and how they are applied in .NET applications. 2. **RESTful APIs and WebSockets**: Learn to create powerful APIs and real-time solutions with ASP.NET Core 8**.** 3. **Advanced Networking Topics**: Dive into WebHooks, SignalR, and message queuing for building robust communication layers. 4. **Emerging Protocols**: Explore the future of networking with QUIC and how to implement it in your applications. 5. **Security Best Practices**: Understand how to secure your networked applications against common threats. 6. **Performance and Scalability**: Discover techniques to optimize and scale your networked solutions effectively. Each chapter builds on the last, giving you a structured learning path with hands-on examples that you can directly apply to your projects. From foundational knowledge to future trends, this book equips you with the skills you need to stay ahead in the evolving field of network programming. --- ### [Privacy Policy](https://www.woodruff.dev/privacy-policy-2/) **Published:** January 29, 2025 **Author:** Chris Woodruff **Content:** **Effective Date:** January 29, 2025 Welcome to Chris Woodruff’s Personal Website (the “Website”). Your privacy is important to me. This Privacy Policy explains how I collect, use, and protect information about visitors to this Website. ### 1. **Information Collected** I use Google Analytics to gather information about how visitors interact with the Website. This includes: - Pages visited and time spent on each page - Referring websites or links - Browser type, device information, and operating system - General geographic location (based on IP address) Google Analytics collects this data using cookies and other tracking technologies. This information helps me understand user engagement and improve the Website’s content and functionality. ### 2. **How Your Information is Used** The information collected through Google Analytics is used to: - Monitor website traffic and performance - Identify popular content and improve user experience - Understand how visitors find and navigate the Website I do not collect personal data such as names, emails, or addresses unless you voluntarily provide them (e.g., through contact forms). ### 3. **Third-Party Services & Data Sharing** Google Analytics processes data under its own privacy policy, which you can review here. I do not sell, trade, or share your personal data with third parties for marketing purposes. However, Google may use aggregated analytics data as outlined in its privacy policy. ### 4. **Cookies & Tracking Technologies** Cookies are small text files stored on your device to track interactions with the Website. You can control or disable cookies in your browser settings. However, disabling cookies may affect how the Website functions. To opt out of Google Analytics tracking, you can install the [Google Analytics Opt-out Browser Add-on](https://tools.google.com/dlpage/gaoptout). ### 5. **Your Rights & Choices** - You can manage or delete cookies via your browser settings. ### 6. **Changes to This Privacy Policy** I may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated effective date. ### 7. **Contact Information** If you have any questions about this Privacy Policy, please get in touch with me at: **Email:** cwoodruff@live.com **Website:** woodruff.dev --- ## Portfolio ### [Entity Framework Core Course](https://www.woodruff.dev/portfolio/entity-framework-core-course/) **Published:** August 16, 2025 **Author:** Chris Woodruff **Content:** ## Entity Framework Core Course ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time **Portfolio Tags:** Open-Source --- ### [ASP.NET Web API Course](https://www.woodruff.dev/portfolio/hemera-ios-app-design-4/) **Published:** October 30, 2022 **Author:** Chris Woodruff **Content:** ## ASP.NET Web API Course ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) [ Terraform Workshop ](https://www.woodruff.dev/portfolio/terraform-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Terraform Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc https://example.domain Our … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) [ htmx & Razor Pages Workshop ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) htmx & Razor Pages Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) [ ASP.NET Web API Workshop ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) [ Entity Framework Core Workshop ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Entity Framework Core Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … **Portfolio Categories:** Course **Portfolio Tags:** Open-Source --- ### [Beyond Boundaries - Networking Programming with C# 12 and .NET 8](https://www.woodruff.dev/portfolio/csharp-networking-book/) **Published:** August 15, 2025 **Author:** Chris Woodruff **Content:** ## # Beyond Boundaries - Networking Programming with C# 12 and .NET 8 ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-core-htmx-book.png) [ ASP.NET Core Reimagined with htmx ](https://www.woodruff.dev/portfolio/aspnet-core-htmx-book/) [Book](https://www.woodruff.dev/portfolio_category/books/) ASP.NET Core Reimagined with htmx Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … **Portfolio Categories:** Book --- ### [ASP.NET Core Reimagined with htmx](https://www.woodruff.dev/portfolio/aspnet-core-htmx-book/) **Published:** August 15, 2025 **Author:** Chris Woodruff **Content:** ## ASP.NET Core Reimagined with htmx ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/csharp-networking-book.png) [ Beyond Boundaries – Networking Programming with C# 12 and .NET 8 ](https://www.woodruff.dev/portfolio/csharp-networking-book/) [Book](https://www.woodruff.dev/portfolio_category/books/) Beyond Boundaries – Networking Programming with C# 12 and .NET 8 Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to … **Portfolio Categories:** Book --- ### [The Woody Show](https://www.woodruff.dev/portfolio/the-woody-show/) **Published:** August 15, 2025 **Author:** Chris Woodruff **Content:** ## Notero - Easy Notes App ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/about_s4_wave.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/1-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/3-scaled.jpg) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/4-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/5-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/hand.png) ## “The team at @itecK is incredibly dedicated, knowledgeable, and helpful. The finished product was beautiful, and worth every penny. I would absolutely recommend Iteck Labs.” ## - **JHON HENRY** , CEO AT NOTERO JSC - ## SHARE THIS PROJECT Twitter Facebook-f Pinterest Youtube Linkedin-in ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/the-breakpoint-show.png) [ The Breakpoint Show ](https://www.woodruff.dev/portfolio/the-breakpoint-show/) [Podcast](https://www.woodruff.dev/portfolio_category/podcasts/) Notero – Easy Notes App Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … **Portfolio Categories:** Podcast --- ### [The Breakpoint Show](https://www.woodruff.dev/portfolio/the-breakpoint-show/) **Published:** August 15, 2025 **Author:** Chris Woodruff **Content:** ## Notero - Easy Notes App ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/about_s4_wave.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/1-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/3-scaled.jpg) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/4-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/5-1.png) ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/hand.png) ## “The team at @itecK is incredibly dedicated, knowledgeable, and helpful. The finished product was beautiful, and worth every penny. I would absolutely recommend Iteck Labs.” ## - **JHON HENRY** , CEO AT NOTERO JSC - ## SHARE THIS PROJECT Twitter Facebook-f Pinterest Youtube Linkedin-in ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/the-woody-show.png) [ The Woody Show ](https://www.woodruff.dev/portfolio/the-woody-show/) [Podcast](https://www.woodruff.dev/portfolio_category/podcasts/) Notero – Easy Notes App Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … **Portfolio Categories:** Podcast --- ### [ASP.NET Web API Workshop](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) **Published:** August 16, 2025 **Author:** Chris Woodruff **Content:** ## ASP.NET Web API Workshop ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Your **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2022/10/aspnet-web-api-course.png) [ ASP.NET Web API Course ](https://www.woodruff.dev/portfolio/hemera-ios-app-design-4/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Course Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … [Open-Source](https://www.woodruff.dev/porto_tag/open-source/) ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) [ Terraform Workshop ](https://www.woodruff.dev/portfolio/terraform-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Terraform Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc https://example.domain Our … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) [ Entity Framework Core Workshop ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Entity Framework Core Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) [ htmx & Razor Pages Workshop ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) htmx & Razor Pages Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … **Portfolio Categories:** Course --- ### [Entity Framework Core Workshop](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) **Published:** August 16, 2025 **Author:** Chris Woodruff **Content:** ## Entity Framework Core Workshop ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) [ htmx & Razor Pages Workshop ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) htmx & Razor Pages Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) [ Terraform Workshop ](https://www.woodruff.dev/portfolio/terraform-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Terraform Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc https://example.domain Our … ![img](https://www.woodruff.dev/wp-content/uploads/2022/10/aspnet-web-api-course.png) [ ASP.NET Web API Course ](https://www.woodruff.dev/portfolio/hemera-ios-app-design-4/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Course Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … [Open-Source](https://www.woodruff.dev/porto_tag/open-source/) ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) [ ASP.NET Web API Workshop ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … **Portfolio Categories:** Course --- ### [Terraform Workshop](https://www.woodruff.dev/portfolio/terraform-workshop/) **Published:** August 16, 2025 **Author:** Chris Woodruff **Content:** ## Terraform Workshop ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2022/10/aspnet-web-api-course.png) [ ASP.NET Web API Course ](https://www.woodruff.dev/portfolio/hemera-ios-app-design-4/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Course Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … [Open-Source](https://www.woodruff.dev/porto_tag/open-source/) ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) [ ASP.NET Web API Workshop ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/htmx-razor-pages-workshop.png) [ htmx & Razor Pages Workshop ](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) htmx & Razor Pages Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) [ Entity Framework Core Workshop ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Entity Framework Core Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … **Portfolio Categories:** Course --- ### [htmx & Razor Pages Workshop](https://www.woodruff.dev/portfolio/htmx-razor-pages-workshop/) **Published:** August 16, 2025 **Author:** Chris Woodruff **Content:** ## htmx & Razor Pages Workshop ## Client ##### Microsoft Holing Ltd, Australia Area ## Services ##### IT Consultation, Design, Cloud Service ## Date ##### February 25th, 2022 Release started ## Team ##### Designers - Developers Operators - Manager ![](https://iteck.smartinnovates.com/demo4/wp-content/uploads/sites/5/2022/04/2mobiles.png) ![](https://www.woodruff.dev/wp-content/uploads/2022/10/bubbls.png) Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc [https://example.domain](#0) ## Our **Challenge** Create an unconventional yet user-friendly website – innovative, with a clean & simple design thatcommunicates and showcases multi-media content: “Site that spreads love.” - Develop easy-to-find and easy-to-navigate mobile friendly website - Showcase each type of content: interactive books, animated stories and picture books, audio stories. Create an experience people want to share with others - Persuade to download app and subscribe ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_1.png) ![](https://iteck.smartinnovates.com/demo1/wp-content/uploads/sites/2/2022/03/ch_2.png) ## Solution & **Result** Our approach was to present the site as a visual editorial platform with quarterly features based on events and occasions the brand was focused on. Each quarterly focus would be marked by the hero and custom tags that filter content. There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. - Create & Save your notes with multi-media - Web Clipper Extension - Complete note editor with rich text options - Automatically sync in real time ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/aspnet-web-api-workshop.png) [ ASP.NET Web API Workshop ](https://www.woodruff.dev/portfolio/asp-net-web-api-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/ef-core-workshop.png) [ Entity Framework Core Workshop ](https://www.woodruff.dev/portfolio/entity-framework-core-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Entity Framework Core Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … ![img](https://www.woodruff.dev/wp-content/uploads/2025/08/terraform-workshop.png) [ Terraform Workshop ](https://www.woodruff.dev/portfolio/terraform-workshop/) [Course](https://www.woodruff.dev/portfolio_category/courses/) Terraform Workshop Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc https://example.domain Our … ![img](https://www.woodruff.dev/wp-content/uploads/2022/10/aspnet-web-api-course.png) [ ASP.NET Web API Course ](https://www.woodruff.dev/portfolio/hemera-ios-app-design-4/) [Course](https://www.woodruff.dev/portfolio_category/courses/) ASP.NET Web API Course Client Microsoft Holing Ltd, Australia Area Services IT Consultation, Design, Cloud Service Date February 25th, 2022 Release started Team Designers – Developers Operators – Manager Stay focused and productive with a clean and clutter-free note space. The flexible ways to organize your notes: hashtags, nested notebooks, pinning notes to the top of the note list, etc … [Open-Source](https://www.woodruff.dev/porto_tag/open-source/) **Portfolio Categories:** Course --- ## Custom Footer ### [Footer](https://www.woodruff.dev/footer/footer/) **Published:** November 7, 2022 **Author:** Chris Woodruff **Content:** ## Shall we have a chat? If you have a project or collaboration that you would like to discuss with us, or if you are curious to hear more about how we can help you, we look forward to hearing from you. - [ info@webexample.com ](#0) - [ +908 (908) 678 77 ](#0) Copyright & Design By [@ThemesCamp](#) – 2022 [ Facebook-f ](#0) [ Twitter ](#0) [ Behance ](#0) [ Youtube ](#0) [ Linkedin-in ](#0) --- ## Categories ### [Blog](https://www.woodruff.dev/category/blog/) --- ### [Speaking](https://www.woodruff.dev/category/speaking/) --- ### [Random C#](https://www.woodruff.dev/category/blog/random-csharp/) --- ### [Network Book Sample](https://www.woodruff.dev/category/blog/network-book-sample/) --- ### [Rust](https://www.woodruff.dev/category/blog/rust/) --- ### [Entity Framework Core](https://www.woodruff.dev/category/blog/efcore/) --- ### [Terraform](https://www.woodruff.dev/category/blog/terraform/) --- ### [HTTP REST](https://www.woodruff.dev/category/blog/http-rest/) --- ### [htmx](https://www.woodruff.dev/category/blog/htmx/) --- ### [Genetic Algorithms](https://www.woodruff.dev/category/blog/genetic-algorithms/) --- ### [AI](https://www.woodruff.dev/category/blog/ai/) --- ### [Business of Software](https://www.woodruff.dev/category/blog/biz-software/) --- ### [Simplicity-First](https://www.woodruff.dev/category/blog/simplicity-first/) --- ### [fun tech](https://www.woodruff.dev/category/blog/fun-tech/) --- ### [Patterns](https://www.woodruff.dev/category/blog/patterns/) --- ### [Developer Experience](https://www.woodruff.dev/category/blog/developer-experience/) --- ## Tags ### [personal](https://www.woodruff.dev/tag/personal/) --- ### [2023](https://www.woodruff.dev/tag/2023/) --- ### [goals](https://www.woodruff.dev/tag/goals/) --- ### [imposter syndrome](https://www.woodruff.dev/tag/imposter-syndrome/) --- ### [better me](https://www.woodruff.dev/tag/better-me/) --- ### [speaking](https://www.woodruff.dev/tag/speaking/) --- ### [creating talks](https://www.woodruff.dev/tag/creating-talks/) --- ### [talk abstracts](https://www.woodruff.dev/tag/talk-abstracts/) --- ### [EF Core](https://www.woodruff.dev/tag/ef-core/) --- ### [Data](https://www.woodruff.dev/tag/data/) --- ### [.NET](https://www.woodruff.dev/tag/net/) --- ### [Book](https://www.woodruff.dev/tag/book/) --- ### [C#](https://www.woodruff.dev/tag/c/) --- ### [dotnet](https://www.woodruff.dev/tag/dotnet/) --- ### [network](https://www.woodruff.dev/tag/network/) --- ### [programming](https://www.woodruff.dev/tag/programming/) --- ### [development](https://www.woodruff.dev/tag/development/) --- ### [personal journey](https://www.woodruff.dev/tag/personal-journey/) --- ### [books](https://www.woodruff.dev/tag/books/) --- ### [developers](https://www.woodruff.dev/tag/developers/) --- ### [new year](https://www.woodruff.dev/tag/new-year/) --- ### [reading](https://www.woodruff.dev/tag/reading/) --- ### [learning](https://www.woodruff.dev/tag/learning/) --- ### [rust](https://www.woodruff.dev/tag/rust/) --- ### [databases](https://www.woodruff.dev/tag/databases/) --- ### [Entity Framework Core](https://www.woodruff.dev/tag/entity-framework-core/) --- ### [asp.net](https://www.woodruff.dev/tag/asp-net/) --- ### [web development](https://www.woodruff.dev/tag/web-development/) --- ### [htmx](https://www.woodruff.dev/tag/htmx/) --- ### [terraform](https://www.woodruff.dev/tag/terraform/) --- ### [devops](https://www.woodruff.dev/tag/devops/) --- ### [IaC](https://www.woodruff.dev/tag/iac/) --- ### [infrastructure](https://www.woodruff.dev/tag/infrastructure/) --- ### [spatial](https://www.woodruff.dev/tag/spatial/) --- ### [Azure](https://www.woodruff.dev/tag/azure/) --- ### [simplicity-first](https://www.woodruff.dev/tag/simplicity-first/) --- ### [software philosophy](https://www.woodruff.dev/tag/software-philosophy/) --- ### [design](https://www.woodruff.dev/tag/design/) --- ### [architectur](https://www.woodruff.dev/tag/architectur/) --- ### [HTTP](https://www.woodruff.dev/tag/http/) --- ### [REST](https://www.woodruff.dev/tag/rest/) --- ### [webdev](https://www.woodruff.dev/tag/webdev/) --- ### [asp.net core](https://www.woodruff.dev/tag/asp-net-core/) --- ### [ai](https://www.woodruff.dev/tag/ai/) --- ### [genetic algorithms](https://www.woodruff.dev/tag/genetic-algorithms/) --- ### [business of software](https://www.woodruff.dev/tag/business-of-software/) --- ### [architecture](https://www.woodruff.dev/tag/architecture/) --- ### [github](https://www.woodruff.dev/tag/github/) --- ### [scripting](https://www.woodruff.dev/tag/scripting/) --- ### [python](https://www.woodruff.dev/tag/python/) --- ### [open-source](https://www.woodruff.dev/tag/open-source/) --- ### [law](https://www.woodruff.dev/tag/law/) --- ### [licensing](https://www.woodruff.dev/tag/licensing/) --- ### [patterns](https://www.woodruff.dev/tag/patterns/) --- ### [MSSQL](https://www.woodruff.dev/tag/mssql/) --- ### [distributed](https://www.woodruff.dev/tag/distributed/) --- ### [systems thinking](https://www.woodruff.dev/tag/systems-thinking/) --- ### [htmxRazor](https://www.woodruff.dev/tag/htmxrazor/) --- ### [wed components](https://www.woodruff.dev/tag/wed-components/) --- ### [UX](https://www.woodruff.dev/tag/ux/) --- ### [IDEs](https://www.woodruff.dev/tag/ides/) --- ## Portfolio Categories ### [Book](https://www.woodruff.dev/portfolio_category/books/) --- ### [Podcast](https://www.woodruff.dev/portfolio_category/podcasts/) --- ### [Course](https://www.woodruff.dev/portfolio_category/courses/) --- ## Portfolio Tags ### [Open-Source](https://www.woodruff.dev/porto_tag/open-source/) ---