Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
163033 stories
·
33 followers

MSVC Build Tools Version 14.52 Release Candidate Now Available

1 Share

We are pleased to announce the availability of the release candidate for Microsoft C++ (MSVC) Build Tools version 14.52. With this release, C++23 is ready for production use and available through the /std:c++23 compiler option; see below for details. This is the last MSVC v14.52 preview before it moves to stable status and full support. You can download it from the Visual Studio 2026 Insiders Channel. As this is the release candidate, it is now the default toolset installed with C++ workloads in this channel. To check if it’s installed simply make sure one or both of these components are checked in the installer:

  1. MSVC Build Tools for x64/x86 (Latest)
  2. MSVC Build Tools for ARM64/ARM64EC (Latest)

The version number printed by cl.exe and link.exe will be at least 14.52.37025 / 19.52.37025.

We appreciate any feedback that you may have. Please report any issues on Visual Studio Developer Community so we can address them by the time we move out of preview.

Version 14.52 is expected to go stable in November and will ship with the 18.12 release of Visual Studio (Stable Channel).

This post summarizes the work done by the MSVC team between the 14.51 and 14.52 releases. This encompasses changes across the compiler frontend, backend, linker, standard library, and related tools.

C++ standards conformance

  • C++23 language mode: Use /std:c++23 to select C++23 language rules. The existing /std:c++23preview spelling still works and is an alias for /std:c++23. The preview form is now deprecated and will be removed in a future update.
  • Immediate escalation: C++23 immediate escalation (P2564R3) lets eligible constexpr functions and lambdas become immediate when their use requires it. It is enabled in C++23 and later modes; C++20 builds can opt in with /Zc:immediateEscalation.
  • Unicode identifiers: C++23 and C23 modes recognize the mathematical compatibility identifier ranges from P3658R1.
  • Constexpr allocation in modules: Module interfaces can represent constexpr functions that contain new T[n] expressions.
  • Preprocessor behavior: _Pragma works in more macro-expansion cases and is supported under /E.
  • Constexpr cmath: Support for C++ compile-time cmath functions remains off-by-default under /std:c++23 and may be enabled via the /Zc:cmath switch. Please see https://devblogs.microsoft.com/cppblog/msvc-c23-constexpr-cmath-with-llvm-libc/ for additional details.

C++/CLI, C, and language tooling

  • /clr is now available in C++23 and later modes.
  • The opt-in /Zc:functionToObjectPtr switch rejects implicit conversions from function pointers to void*.
  • The compiler now accepts selected constexpr constructor-template instantiations involving virtual base classes. Constructing those virtual bases during constant evaluation remains unsupported, so this is limited to the declaration checks for a constexpr function.
  • In C code, C4815 and C4816 now warn when a structure is allocated without storage for its flexible array member or when a structure with a flexible array member is passed by value.
  • The Linux C++ IntelliSense parser maps C++20 and C++23 standard options to the corresponding language modes.
  • Argument-dependent lookup now handles more names that are not exported from a module, and nested template and module instantiation is less prone to failure.
  • Module serialization preserves class data members and template information more consistently. Type merging also works better when several modules share a header unit.
  • Header units preserve macro whitespace so stringized macros survive a round trip. Module processing also handles types first defined in the global module fragment more reliably.
  • Math intrinsics and math headers now work more consistently with modules and constant evaluation.
  • Caret diagnostics use less memory, and several diagnostics reproduce the original source expression more accurately.
  • Preprocessor diagnostics better identify malformed and nested constant expressions.
  • The compiler handles invalid type-trait inputs and malformed function-parameter scopes more consistently.
  • The compiler warns when overriding a deprecated non-pure virtual function.

Code generation and optimization

Inlining:

  • LTCG now considers which fields of a small return object the caller reads, so inlining can discard work that computes unused fields.
  • Whole-program optimization can favor an inline when doing so exposes parts of a local object and makes unused stores removable.
  • A large outlined loop body can now inline through a small wrapper called from a loop.
  • With PGO, small size-optimized virtual-call targets can be speculatively inlined when the profile, size, and growth limits allow it.
  • The inliner follows function-pointer values through aggregate assignments, preserving more indirect-call opportunities in template adapters.
  • Type-based devirtualization now works alongside profile-guided inlining. Hot/cold information also does more to prevent cold code from being pulled into hot callers.

Loop and scalar optimization:

  • The compiler can move more invariant loads out of loops after proving that each load executes on every iteration and remains safe across aliased pointers.
  • Equality-tested loops shed more loop-carried work, and strength reduction handles more wrapping integer induction expressions.
  • When three or more multiplications share a divisor, the optimizer can compute one reciprocal and reuse it instead of restoring repeated divisions.
  • Copy propagation through aliased fields is on by default and understands known-size memcpy and memset operations.
  • x64 conditional selection handles more nonzero constants and safe aliased operands.
  • Unsigned comparisons and induction-variable ranges now establish more nonzero, ordering, and non-overflow facts for later optimization.
  • Scalar conditional expressions gain more value-equivalence, incoming-value, multiply, and shift simplifications.
  • Calls use restrict arguments more precisely when determining memory interference, exposing more optimization around those calls.
  • The x86 optimizer performs more local and cross-block copy propagation.
  • The compiler recognizes more rotate idioms and dynamic bit-field extraction controls, and removes redundant POPCNT zero extension.
  • Small-aggregate optimization removes padding-only assignments, decomposes qualifying whole-struct loads, and forwards eligible loads through local memcpy operations.
  • Constant conversions can fold without /fp:fast when the transformation is otherwise valid.
  • Earlier loop-invariant motion and broader expression reassociation expose more work to later optimization.
  • Larger functions reuse more repeated expressions. Min/max expressions, mixed-signedness compare diamonds, and repeated vector-element extraction also simplify in more cases.
  • More indirect calls to noexcept functions become tail calls. Static constant arrays can also disappear when their storage is unnecessary.
  • Loop unswitching handles more nested-loop cases before vectorization, while straight-line vectorization handles more merged values.
  • Jump chains and induction variables simplify in more cases without losing useful predicates or signedness information.
  • The compiler emits less attribute data, scans exception regions fewer times, bounds pathological dead-use searches, and processes large chained function expressions in linear rather than quadratic time.

Vectorization:

  • On x64 and ARM64, the compiler vectorizes more code that selects values through branches.
  • Integer reductions such as sum += a[i] - b[i] can be reassociated into a form the loop vectorizer recognizes.
  • On x64 and ARM64, vector loads and stores reach copy propagation and scalar replacement earlier. Full-width vectors remain intact when splitting them would discard useful vector operations.
  • The vectorizer recognizes more adjacent-address patterns and is more willing to pack user-written SIMD values when the result can remain in a vector register.
  • A parameter-derived pointer advanced by a loop is recognized as non-aliasing a local array in more cases, avoiding an unnecessary runtime overlap check.
  • The loop vectorizer handles qualifying memory-only swaps and multi-stream copies, including std::swap on arrays of trivial types.
  • More loop-control expressions shared by a following conditional simplify after the loop.

ARM64

  • ARM64 now shares address calculations across related loads and stores, including array-of-structures loops based on function parameters. This removes repeated arithmetic and creates more opportunities for paired loads and stores.
  • Known NEON constants can be built without unnecessary data loads. Scalar loads, broadcasts, and lane inserts also fold into NEON load instructions in more cases.
  • The compiler combines more NEON construction and lane operations, removes unused lane inserts, and eliminates redundant mask comparisons.
  • The loop vectorizer now handles additional floor, ceil, trunc, and round operations.
  • Tail-call decisions are now made per call, so one ineligible call no longer blocks another eligible call in the same function.
  • ARM64 and ARM64EC inlining accounts for floating-point and vector register pressure. This preserves profitable native ARM64 inlines while avoiding excessive spills on restricted ARM64EC targets.
  • Native ARM64 SVE2 supports code generation and disassembly for lane-indexed svmullb and svmullt intrinsics. Execution requires SVE2-capable hardware.
  • LUTI2 and LUTI4 lookup-table support includes BF16 forms. Floating-point absolute minimum/maximum instructions and Common Short Sequence Compression operations are also supported. Each family requires the corresponding processor feature.
  • ARM64_FPMR names the FPMR register for _ReadStatusReg and _WriteStatusReg; armasm64 and the disassembler recognize the same name. Execution requires FEAT_FPMR hardware and OS permission.
  • Native ARM64 supports __preserve_none and [[msvc::musttail]], including specialized interpreter dispatch. The __preserve_none calling convention is C-only and is not supported on ARM64EC.
  • With /feature:sme, the compiler accepts SME attributes and ZA-state types on supported external declarations. Eligible scalar noexcept calls can perform the required streaming-mode transitions, including selected function-pointer, reference, whole-program, and Control Flow Guard cases. Streaming function definitions, ZA-bearing calls, unsupported pointer forms, and vector or aggregate arguments remain unsupported. ARM64EC is excluded.
  • The native ARM64 CRT supplies the AAPCS64 SME state-management routines for TPIDR2, ZA, and ZT0. Availability depends on processor and operating-system state support; ARM64EC is excluded.
  • On ARM64, the /Zc:cmath math runtime now supports the floating-point environment. This initial implementation reports math errors through floating-point exceptions rather than errno.
  • ARM64 stack layout can reuse slots for locals in mutually exclusive switch cases, greatly reducing the frame size of large switches.
  • Stateless SVE all-true predicates can move out of loops when the usual safety and profitability checks allow it.
  • Proven-dead switch edges are removed before switch expansion, exposing more control-flow and value optimizations.
  • Bitfield operations use narrower immediate forms where possible.
  • Register assignment preserves more adjacent load pairs and makes better spill/rematerialization choices for frequently used values.
  • Used-lane tracking now crosses more scalar floating-point operations, exposing dead vector work.
  • The compiler recognizes more vector permutations, per-lane rotations, and vector bit-clear immediate forms.
  • SVE comparisons and the MATCH/NMATCH/WHILE families reuse condition flags more effectively. NEON-to-SVE bridge moves, upper-lane float extraction, and by-element multiply addressing also produce shorter sequences.

x64 and x86

  • Speed-optimized x64 builds have more strategies for fixed-length memcmp. Eligible copies larger than 256 bytes use the runtime copy path instead of a large inline sequence.
  • x64 removes more redundant zero extensions before jump-table address calculations and lowers unsigned division by a constant power of two to a shift.
  • Constant f64x2 assignments can combine so a later vector operation folds to one 128-bit constant move.
  • 32-bit x86 builds can inline fmin, fmax, and copysign; ARM64 can inline copysign.
  • AVX2 and AVX-512 targets can build wider vectors from smaller register values without a memory round trip.
  • AVX-512 code generation uses suitable VEX moves for intrinsic operands and folds eligible single-use masked arithmetic followed by a blend into one merge-masked operation.
  • Builds using /feature:APX gain more arithmetic and conditional-selection forms, fewer redundant moves and comparisons, and APX-aware unrolling and strength-reduction decisions for scalar integer loops.
  • APX support now spans the ABI, assembler, disassembler, and debugging tools. R30 and R31 are nonvolatile, register assignment treats them as killed across setjmp, MASM and the disassembler handle eligible extended-register encodings, and DIA reads APX register context.
  • APX-enabled x64 code generation can fold eligible operands and makes better spill, recomputation, and fold choices.
  • Unwind V3 support now spans code generation, linking, ML64, DIA, and binary instrumentation. It covers chained runtime-function records, large prologs and epilogs, .PUSHFRAME/.POPFRAME, and stack walking. ML64 requires /unwindv3; compiler emission is on by default only for APX functions, and stack walking requires a V3-aware operating system and debugger.
  • x64 recognizes more condition-code-safe INC/DEC forms, including eligible atomic fetch-add sequences.
  • AVX2 intrinsic lowering removes unnecessary scalar-to-vector moves in qualifying _mm_set_sd and _mm256_set1_pd paths.
  • APX code generation includes more new-destination and memory-operand forms, shorter LEA replacements for eligible INC/DEC and SUB sequences, and broader shift, compare, conditional-compare, conditional-move, and constant-propagation support.
  • All-bits-set vector constants and functions containing paired register operations receive better code generation.
  • Shift-range analysis models the hardware shift-count mask, and adjacent two-element double stores combine more effectively.
  • Under /O1 and /Os, additional pre-allocation optimizations and size-aware instruction costs reduce code size.
  • RDPRU uses its required fixed-register constraints.

Standard Library

See the STL Changelog – MSVC Build Tools 14.52 for a complete list of all of the changes in this release, grouped by features, LWG issue resolutions, bug fixes, performance improvements, etc. Some highlights:

  • The STL is C++23 feature complete now that P0533R9 “constexpr For <cmath> And <cstdlib>” has been implemented.
    • This is initially supported for MSVC only, under /std:c++23 /Zc:cmath. We expect to make /std:c++23 imply /Zc:cmath in a future release, but it is currently opt-in. Clang and EDG support will be enabled when the necessary compiler machinery becomes available.
    • For more info, read MSVC C++23: constexpr cmath with LLVM Libc by Cody Miller (MSVC compiler front-end dev) and Bringing Correctly Rounded Math to Production with LLVM-libc by Michael Jones and Tue Ly (LLVM-libc maintainers at Google).
    • And under /std:c++latest /Zc:cmath, we’ve mostly implemented C++26 P1383R2 ‘s constexpr additions for <cmath>. (No <complex> changes yet.) A few functions are not yet implemented by LLVM-libc (see LLVM libc math status ) and accordingly are not yet constexpr. For double, the functions acosh, asinh, atanh, cosh, sinh, tanh, and erf are missing. For float and double, the functions erfc, lgamma, and tgamma are missing. (MSVC uses 64-bit long double, so we don’t need a third implementation of each function.) As soon as they become available, we’ll activate them as constexpr in future releases.
  • 19 LWG issue resolutions, which are bugfixes in the Standard.
  • Improved <regex> performance in several scenarios.
    • Special thanks to Julian Müller (@muellerj2 on GitHub) for this ongoing correctness and performance overhaul of <regex>.
  • Added ARM64 SVE vectorized implementations of several STL algorithms.
    • Also fixed regressions and improved performance in some of the ARM64/ARM64EC Neon vectorized implementations that were previously added.
    • Special thanks to Hari Limaye at Arm (@hazzlim on GitHub) for this ongoing vectorization work.

AddressSanitizer

  • Improved runtime performance by removing duplicated quarantine work
  • Use MSDIS (MSVC disassembler) as a fallback instruction-length decoder for interception, reducing “unrecognized instruction” failures. This is on by default and can be disabled via ASAN_OPTIONS=windows_use_msdis_decoder=false . To trace what instructions were intercepted by MSDIS instead of the manual instruction decoder, set verbosity=3 and search for logs prefixed with “MSDIS instruction decoder:”.
  • Runtime performance improvements including heap address caching, more efficient memory-mapped heap checks, and faster allocator flag assignments, resulting in significant performance gains across multiple runtime scenarios.

Build tools and diagnostics

  • The C++ frontend accepts more valid designated-initialization forms.
  • The C++ frontend accepts more deeply nested initializers, and the legacy preprocessor allows larger macro buffers.
  • Code Analysis SARIF output includes rule names, descriptions, and help links by default, plus tags where available.
  • LNK2022 and LNK4227 name the relevant metadata item. LNK4323 explains a missing ARM64EC/native object pair.
  • Incremental linking trims excess growth space from program-database files.
  • The disassembly library recognizes MIPS32 and MIPS64 Release 6 instructions and ARM64 matrix-state start/stop instructions.
  • /analyze:log:format:sarifandxml writes both Code Analysis log formats.
  • LNK4321 identifies duplicate C++ module exports while generating an import library.
  • C5089 identifies the experimental combination of /fsanitize=address with /clr, including /clr:netcore.
  • In C++, two new level-4 warnings diagnose implicit and explicit conversions between HRESULT/SCODE and bool-like types. They are off by default and do not apply to C.
  • The opt-in /PDB4GBSTREAMS option raises the size limit for the PDB global/public symbol-record stream to roughly 4 GiB. Other PDB streams retain their existing limits.
  • MASM and the disassembly library recognize the x86 int1/ICEBP instruction (0xF1).
  • LTCG emits C4744 when extern declarations disagree on type.
  • PE reproducibility improves because the image hash no longer includes the PDB age.
  • Tag-based Code Analysis warning groups simplify ruleset authoring.
  • Debugger clients can release a program-database file handle without discarding loaded data, allowing the file to be rebuilt sooner. Debug information also merges module and header-unit types more reliably.
  • Debug records can identify alternate object names, and type-record processing performs fewer allocations and improves throughput on large programs.
  • Linker and assembler diagnostics are clearer, and malformed object files, libraries, and module metadata are handled more defensively.

Community Tickets fixed

These public Developer Community tickets were fixed or completed during this development interval, including changes later serviced to MSVC 14.51.

Share your feedback

Thank you for the reports and reproductions that helped us investigate these issues. Please report problems on Developer Community , including a small reproducer, the compiler options, and the target architecture.

The post MSVC Build Tools Version 14.52 Release Candidate Now Available appeared first on C++ Team Blog.

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Microsoft and GitHub Keep Giving AI Agents More Power -- and More Restrictions

1 Share
Recent releases expand AI coding agents' capabilities while adding sandboxing, monitoring and enterprise controls that can restrict their autonomy and even their availability.
Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

React for Vue Developers, Part 4: Forms, useFormStatus and useOptimistic

1 Share

React 19 for Vue 3 Developers, Part 4: Where in Vue you use v-model and ref or use a library, React leans on the plain HTML <form> and hooks.

In Part 3 of this series we met Actions, and we saw a form call one through the action prop. This time we stay in that world and go deeper. Forms are where a lot of React 19 finally clicks, so this is the fun part.

In Vue, you already have a comfortable setup for forms. You bind inputs with v-model, you track a loading ref by hand, and for anything extra you reach for a library like VeeValidate. React 19 takes a different route. It leans on the plain HTML <form> and gives you a few small hooks that remove most of the manual work.

None of these hooks is big on its own. Put together, though, they replace a lot of the form glue you write by hand in Vue, and that is the whole point. Let’s take a look.

React Basics for Vue Developers 4 - forms and actions, useFormStatus, useOptimistic
Image generated with AI

Controlled Inputs: The Manual v-model

The first thing every Vue developer misses on day one is v-model. React does not have it. There is no single attribute that both shows a value and updates it when the user types. You wire those two halves up yourself.

NameField.vue

<script setup>
import { ref } from 'vue'

const name = ref('')
</script>

<template>
  <input v-model="name" />
  <p>Hello, {{ name }}</p>
</template>

NameField.jsx

import { useState } from 'react'

function NameField() {
  const [name, setName] = useState('')

  return (
    <>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <p>Hello, {name}</p>
    </>
  )
}

Let’s break it down. In Vue, v-model does two jobs at once. It puts name into the input, and it writes the input back into name when you type. In React, you do both jobs by hand. value={name} shows the current value, and onChange runs on every keystroke and calls setName with the new text. You read the typed value from event.target.value.

An input wired up like this, where React owns the value through state, is called a controlled input. React controls what is in the box, not the browser. So v-model="name" becomes value={name} plus onChange, and that pair is something you will type a lot.

Notice this is more code than Vue for the simple case, and I won’t pretend otherwise. The upside comes later, because having the value in your own state makes validation, formatting and conditional logic very direct. You already have the value, so you just use it.

What about a form with five fields? In Vue you would add five v-model bindings and move on. A controlled approach in React means five pieces of state, one per field, each with its own value and onChange. That is a lot of wiring for a big form. It is the main reason the next pattern exists. When a form is really just “fill it in and submit,” you do not have to control every input at all.

Letting the Form Do the Work

Here is the part that surprised me coming from Vue. You do not always need controlled inputs. React 19 can read the values straight from the form when it submits, the way plain HTML always could.

Remember Actions from Part 3. When you pass a function to a form’s action prop, React calls it on submit and hands it the form’s FormData. That means you can skip the per-input state and just read the values at the end.

SignupForm.jsx

'use client'

import { useActionState } from 'react'

function SignupForm() {
  const [error, signupAction, isPending] = useActionState(
    async (previousState, formData) => {
      const email = formData.get('email')
      const response = await fetch('https://myapp.com/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email }),
      })
      if (!response.ok) {
        return 'Something went wrong, please try again'
      }
      return null
    },
    null
  )

  return (
    <form action={signupAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>Sign up</button>
      {error && <p>{error}</p>}
    </form>
  )
}

We met useActionState in Part 3, so here is the short recap. You give it your async action and a starting state. It gives you back three things:

  • The current state: I use it to hold an error message, or null when things went fine.
  • A wrapped action: The function you pass to the form’s action.
  • isPending: A boolean React sets to true while the action runs, then back to false.

Notice there is no ref for the email input. The input just has a name, and we read it with formData.get('email') when the form submits. There is also no isSubmitting flag that you flip by hand, and no try/finally. React tracks the pending state for you and clears it when the action settles.

Big disclaimer though. Uncontrolled inputs like this are great for simple submits, but the moment you need live validation as the user types, or you need to react to a value before submit, you will want controlled inputs again. Both styles are fine. Pick the one the app actually needs.

useFormStatus: Pending State Without Prop Drilling

Now, a common problem. You have a submit button, and you want it to show a spinner while the form is submitting. In the example above, that was easy because isPending lived right next to the button. But real forms get split into pieces. Your button often lives in its own small component, several layers down from the form.

In Vue, you would pass an isSubmitting prop down through each layer, or reach for provide and inject to skip the middle. React 19 gives you a hook made for exactly this. useFormStatus reads the status of the nearest <form> above it, with no props at all.

SubmitButton.jsx

'use client'

import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button disabled={pending}>
      {pending ? 'Sending...' : 'Sign up'}
    </button>
  )
}

Let’s break it down. The button asks useFormStatus for the current form status and reads pending from it. When the parent form is submitting, pending is true, so the button disables itself and swaps its label. The button never receives a prop. It looks up the tree, finds the form it sits inside and reads the status from there.

There is one rule that trips people up, so read this twice. useFormStatus reports on the form it is inside of, not a form in the same component. So the button has to be rendered within the <form>, and it has to be its own component. If you called useFormStatus in the same component that renders the <form>, it would not see that form. Think of it like Vue’s inject. It only sees what an ancestor provides, so it has to sit below the form in the tree.

The hook comes from react-dom, not react, because it is tied to the DOM <form> element. That is an easy import to get wrong the first time.

useFormStatus gives you more than pending. It also returns:

  • pending: true while the form is submitting.
  • data: The FormData being sent, so a child can read what the user just submitted.
  • method and action: The HTTP method and the action function the form is using.

Most days, you only reach for pending. But data is handy when a nested component needs to show what is on its way to the server, like a preview of the comment someone just posted.

useOptimistic: Instant UI, Built In

The last hook is my favorite, and if you read my earlier article on Optimistic Updates with TanStack Query in Vue, the idea will feel familiar. An optimistic update shows the result right away, before the server confirms it. If the request fails, you roll back. It makes an app feel instant.

In the Vue TanStack piece, we did this through the query cache. We wrote the new value into the cache, kept a snapshot and rolled back on error. React 19 bakes the same idea into a hook called useOptimistic, and it does the rollback for you.

LikeButton.jsx

'use client'

import { useOptimistic } from 'react'

function LikeButton({ likes, likeAction }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (current) => current + 1
  )

  const handleLike = async () => {
    addOptimisticLike()
    await likeAction()
  }

  return (
    <form action={handleLike}>
      <button>{optimisticLikes} likes</button>
    </form>
  )
}

Let’s break it down. useOptimistic takes the real value, here the likes count from the server and a function that says how to change it for the optimistic view. It gives you back two things:

  • optimisticLikes: The value to show. While an action is running, this is the optimistic version. The rest of the time it is the real value.
  • addOptimisticLike: The function you call to apply the optimistic change.

When the user clicks, we call addOptimisticLike() first, so the count jumps up right away. Then we await likeAction() to tell the server. Here is the nice part. If the action fails, React throws away the optimistic value and shows the real one again. You do not write the rollback yourself, which is the piece that was always fiddly in the manual Vue version.

One thing to keep in mind about success. The count only stays up if the real likes value updates too, usually because the action returns the fresh count or the data gets refetched. When the action finishes, useOptimistic lines back up with whatever the real value is at that point. So it is not a place to store state, it is a way to show the change early while the real value catches up.

The update function is worth a second look. It receives the current value, and if you pass a value into addOptimisticLike, it receives that too. So you are not stuck with “add one.” A very common shape is a list. You show a new comment in the list the moment the user hits send, then let the server catch up. The pattern is the same as the counter. You just describe how to add the item instead of how to bump a number.

Notice that this only works while an action is running. useOptimistic is built to pair with the Actions and forms we have been using all through this article, so it fits right into the same flow.

Where Validation Lives

One honest gap before we wrap up. Vue developers often lean on VeeValidate for validation, and React does not ship anything like that. React 19 gives you the form plumbing, the pending state and optimistic updates, but it does not give you a validation library. In React, you either check values inside your action and return errors, like we did above, or you bring in a library. The common one is React Hook Form, usually with a schema tool like Zod.

Checking inside the action is nicer than it sounds. You read the values from formData, run your checks and return an error when something is off. Then you show it, exactly like the error state we used in the signup form. For more than one field, return an object keyed by field name:

async (previousState, formData) => {
  const email = formData.get('email')
  if (!email.includes('@')) {
    return { email: 'That does not look like an email' }
  }
  // save the user here, then:
  return null
}

Now error holds that object, and each input can show its own message by reading something like error?.email next to it. It is manual work, but it is easy to follow, and for a login or signup form it is all you need.

For big forms with lots of rules, a library saves you from writing every check by hand. That is where React Hook Form and Zod come in. A Vue developer who has used VeeValidate with a schema will find the ideas familiar, even though the API looks different. We will line these up properly in the next part, so I will keep it short here. For now, just know that “which validation library” is a real question you answer on a React team.

Wrapping Up

Forms are where React 19 pays off. You give up v-model and wire inputs yourself with value and onChange, which is the controlled-input pattern. For simple submits you can skip that and let the form hand you its FormData through an Action.

useFormStatus lets a nested button read the form’s pending state with no props, as long as it sits inside the form. And useOptimistic gives you instant UI with automatic rollback, the same idea as the TanStack optimistic updates you may already know from Vue, now built into React.

For the details, the React docs on useFormStatus and useOptimistic are the best place to go, and they show more of the shapes each hook returns.

Next time, we finish the series with a full cheat sheet that lines up each Vue tool next to its React counterpart. It covers routing, state management, data fetching and validation, so you have one page to keep open on the job.

Happy submitting!


Want to build a beautifully designed, feature-rich form with ease? Check out the KendoReact Form component.

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

How to Model Money in ASP.NET Core

1 Share

Working with monetary values in C# seems simple until improper implementation introduces rounding errors, inconsistencies and financial losses that are difficult to detect. Learn best practices for coding financial systems in ASP.NET Core.

Imagine you need to implement a new financial module where money is the system’s core entity: payments, reimbursements, recurring fees, discounts, interest and taxes all involve it daily.

At first glance, the implementation seems simple. After all, you just need to create a few decimal properties, perform mathematical operations and persist the values to the database. Right? Not always.

Small implementation decisions can lead to major problems. In systems processing thousands or millions of transactions, differences of just a few cents can turn into significant financial losses. For this reason, treating money merely as a number is often a mistake. Monetary values should have their own behaviors, rules and constraints reflected in the code.

In this article, we will explore patterns and best practices for handling money in C# applications, ranging from choosing the right data type to creating a Value Object that makes the domain safer, more expressive and less prone to errors.

Common Mistakes When Dealing with Monetary Values

Before discussing patterns, it is important to understand the most common errors made when implementing financial features. Seemingly harmless decisions during development are among the leading causes of monetary inconsistencies. Let’s look at some of the main ones.

Using double or float for Monetary Values

A frequent mistake is using floating-point types (double or float) to represent monetary values.

double value = 0.1 + 0.2;

Console.WriteLine(value);

Running in Fiddle: https://dotnetfiddle.net/TpFz0k

Double sum result

Note that the result was an extremely long value. This happens because double and float store values using binary floating-point representation, which cannot accurately represent many decimal numbers.

For financial calculations, always prefer the decimal type, which was designed to perform monetary operations with precision, as shown in the example below:

decimal value = 0.1m + 0.2m;

Console.WriteLine(value);

Running in Fiddle: https://dotnetfiddle.net/bmQSR6

Decimal sum result

Representing Money as a Primitive Type

Another mistake is treating money simply as a property of the decimal type:

public decimal Price { get; set; }

The problem with this approach is that it raises some important questions: Which currency does this value represent? Can negative values be used? How should rounding be handled? Can values in different currencies be added together?

A primitive type cannot enforce these business rules, making the system more vulnerable to errors. A more robust approach would be to use a Value Object, as we will see later on.

Rounding Rules Scattered Throughout the Application

Financial systems often have specific rounding requirements. In this context, it is not uncommon to find applications that perform rounding operations in various parts of the code, as shown in the example below.

var total = Math.Round(subtotal, 2);
var discount = Math.Round(discountValue, 2);
var tax = Math.Round(taxValue, 2);

When rounding rules change, every instance must be reviewed, increasing the risk of something being overlooked and potentially leading to financial losses.

A better approach is to centralize monetary operations in a dedicated class responsible for applying business rules consistently and explicitly, all in one place.

Allowing Invalid Monetary States

When implementing financial systems, it is common to have a domain that imposes constraints not always guaranteed by the code. For example, a product price cannot be negative, an invoice total cannot be less than zero, and a discount cannot exceed the order value.

When money is represented as a primitive type, it becomes easy to create invalid states. For example:

decimal productPrice = -100m;

The compiler accepts this, but the business rule does not. Therefore, a dedicated class for representing money can validate these rules when instances are created.

Mixing Business Logic with Monetary Calculations

Financial calculations often end up scattered across services, controllers and repositories. However, as the application evolves, this logic becomes duplicated and difficult to understand:

var subtotal = quantity * unitPrice; 
var discount = subtotal * 0.10m; 
var tax = subtotal * 0.15m; 
var total = subtotal - discount + tax;

A better approach would be to encapsulate the behavior of monetary values within dedicated domain objects. This would help maintain calculation consistency and facilitate the system’s evolution.

Handling Monetary Values the Right Way

In the previous section, we looked at some of the main mistakes developers make when implementing systems that handle monetary values. Now, let’s explore the best practices and recommended approaches for building financial systems that are more reliable and less prone to errors.

A Pattern Called Money

Martin Fowler, in his book Patterns of Enterprise Application Architecture, presents Money as a pattern for modeling monetary values. The idea is simple: money is not just a number. A monetary value is composed of, at least, two inseparable elements: The value (amount) and the currency.

This modeling prevents invalid situations, such as adding dollars to euros, for example, or comparing values of different currencies without adequate conversion.

Furthermore, an instance of Money must be immutable. Instead of changing its internal state, operations such as addition or subtraction return a new instance containing the result of the operation. This makes the code more predictable and reduces side effects.

Implementing Money in an ASP.NET Core Application

Imagine that we are developing a financial system to control a digital wallet. The system must record deposits and withdrawals and must consider different currencies. In addition to having an updated wallet balance, we need to keep a history of all transactions.

Following Martin’s pattern, it makes sense to model money as a domain concept, in this case, a Value Object, and not just as a primitive type.

To maintain the focus of the post, some implementation details will be omitted, but you can access the complete code in this GitHub repository: Wallet API source code.

Within the “Domain” layer, we could create the following entities: Money, Currency, WalletAccount and Transaction:

Money:

namespace Wallet.Api.Domain;

public sealed record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentOutOfRangeException(nameof(amount));

        if (string.IsNullOrWhiteSpace(currency) || !Domain.Currency.IsValid(currency))
            throw new ArgumentException(nameof(currency));

        Amount = decimal.Round(amount, 2, MidpointRounding.ToEven);
        Currency = currency;
    }

    public Money Add(Money value)
    {
        EnsureSameCurrency(value);

        return new Money(Amount + value.Amount, Currency);
    }

    public Money Subtract(Money value)
    {
        EnsureSameCurrency(value);

        if (Amount < value.Amount)
            throw new InvalidOperationException("Insufficient balance.");

        return new Money(Amount - value.Amount, Currency);
    }

    private void EnsureSameCurrency(Money value)
    {
        if (Currency != value.Currency)
            throw new InvalidOperationException("Currencies must match.");
    }

    public override string ToString() => $"{Amount:N2} {Currency}";
}

Note that here Money is treated as a domain concept rather than a primitive type. This is precisely what Fowler proposes in his pattern. Money has its own business rules, such as currency, rounding and valid operations. So it should not be represented simply by a decimal number scattered throughout the application.

By encapsulating these rules within the Money class, we eliminate Primitive Obsession, one of the code smells described by Fowler. Instead of multiple methods accepting separate decimal and string arguments, there is a single object that accurately represents the concept of money.

Immutability is another key point. Both Amount and Currency have only getters and are defined exclusively in the constructor. The Add and Subtract methods never modify the current instance. Instead, they always return a new Money object.

Furthermore, according to Eric Evans, Value Objects should be immutable whenever possible. Fowler also recommends this approach for Money, as monetary values represent a mathematical concept. If two operations shared the same mutable instance, various hard-to-detect bugs could arise, such as issues with concurrent operations, unexpected side effects or the accidental alteration of shared balances. By always returning a new instance, the class makes such situations impossible.

Note also the Currency property, which illustrates one of the most important points of the Money pattern advocated by Fowler. Money is never just a number: the values 100 USD, 100 EUR and 100 BRL represent completely different things. This is precisely why Fowler states that Money consists of two inseparable pieces of information: Amount and Currency.

Another important point is that we use MidpointRounding.ToEven (known as Banker’s Rounding), which is frequently recommended in financial applications because it reduces the statistical bias caused by always rounding up.

Currency:

namespace Wallet.Api.Domain;
public static class Currency
{
    public const string USD = "USD";
    public const string EUR = "EUR";
    public const string BRL = "BRL";

    private static readonly HashSet<string> _valid = [USD, EUR, BRL];

    public static bool IsValid(string currency) => _valid.Contains(currency);
}

The Currency class plays a significant role within the domain. It centralizes the concept of valid application currencies, preventing arbitrary values from being used when creating a Money object.

A key point is that it avoids the use of “magic strings” scattered throughout the system. Without this class, one might commonly see code like new Money(50, "USD");. The problem with this approach is that typos go unnoticed by the compiler and can lead to serious application errors, such as new Money(100, "Us"); for example.

By defining a dedicated Currency class, the code becomes much safer and more expressive. In other words, it not only reduces typos but also communicates intent more clearly.

WalletAccount:

namespace Wallet.Api.Domain;

public class WalletAccount
{
    private WalletAccount()
    {
    }

    public Guid Id { get; }

    public Money Balance { get; private set; }

    public WalletAccount(Guid id, string currency)
    {
        Id = id;
        Balance = new Money(0, currency);
    }

    public void Deposit(Money amount)
    {
        Balance = Balance.Add(amount);
    }

    public void Withdraw(Money amount)
    {
        Balance = Balance.Subtract(amount);
    }
}

The WalletAccount class represents the core domain entity responsible for the digital wallet. Unlike Money, which is a Value Object, WalletAccount possesses its own identity, represented by an Id. In Domain-Driven Design, this is precisely the defining characteristic of an entity: it is recognized not merely by its attributes, but by a unique identity that remains constant throughout its lifecycle.

Note that we utilize the previously created Money object. Another key point is that the entity never allows the balance to be modified directly. The Balance property features a private setter, preventing any part of the application from arbitrarily assigning a new value. This preserves encapsulation and routes all changes through business methods defined by the entity itself.

The Deposit and Withdraw methods exemplify this behavior. Instead of exposing the balance for other classes to perform manual calculations, we define operations that make sense within the domain. This approach follows a key object-oriented principle known as Tell, Don’t Ask. Rather than retrieving the balance, performing calculations externally, and then assigning a new value, we simply instruct the wallet itself to execute the desired operation.

Another interesting detail is that the entity fully delegates money-related rules to the Money object. When a deposit is made, the wallet does not need to know how to add monetary values; it simply asks the Money object to perform that operation. The same applies to withdrawals. This results in a solid distribution of responsibilities: the wallet handles account-specific rules, while the Money object handles rules related to money itself.

WalletAccount manages the wallet’s behavior, whereas Money encapsulates everything regarding monetary values. Consequently, the model remains organized, easy to understand and simpler to evolve.

Transaction:

namespace Wallet.Api.Domain;

public enum TransactionType { Deposit, Withdraw }

public class Transaction
{
    private Transaction() { }

    public Guid Id { get; } = Guid.NewGuid();
    public Guid WalletAccountId { get; private set; }
    public TransactionType Type { get; private set; }
    public decimal Amount { get; private set; }
    public string Currency { get; private set; } = default!;
    public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;

    public static Transaction Create(Guid walletAccountId, TransactionType type, Money money)
        => new() { WalletAccountId = walletAccountId, Type = type, Amount = money.Amount, Currency = money.Currency };
}

The Transaction class is responsible for recording an event that occurred within the domain: a deposit or a withdrawal. The entity is created via the static Create method. Rather than exposing a public constructor, we centralize all creation logic in a single place.

An interesting detail is that the method receives a Money object, yet the entity does not store it directly. Instead, it copies only the Amount and Currency to its own properties. This makes sense when considering financial history. A transaction represents an immutable record of an event that occurred at a specific moment. It does not need to maintain a reference to the Money object used during the operation, it only needs to persist the information representing that historical fact.

It is also important to note that the example shown in this post is for educational purposes, serving as a starting point. In production scenarios, transactions would require a more complex implementation, potentially utilizing asynchronous events via the Event Sourcing pattern.

⏳Saving Time with NodaMoney

NodaMoney is a .NET library inspired by the Money pattern described by Martin Fowler. It provides a ready-to-use implementation for representing monetary values, encapsulating concepts such as amount, currency, rounding, mathematical operations, currency comparison and formatting.

Instead of creating our own Money class, we can use the Money class provided by NodaMoney.

Note that if we run the code below in Fiddle, the result will be the value already correctly formatted in dollars, simply by using the NodaMoney NuGet package:

using System;
using NodaMoney;

var salary = new Money(5000m, "USD");

Console.WriteLine(salary);

Money with NodaMoney

Running in Fiddle: https://dotnetfiddle.net/XRYYMw

However, if we try to add two values in different currencies, we will get an error, verifying that no inconsistent additions occur:

using System;
using NodaMoney;

var salary = new Money(5000m, "EUR");
var bonus = new Money(1200m, "USD");

var total = salary + bonus;

Console.WriteLine(total);

Different currencies error

Running in Fiddle: https://dotnetfiddle.net/82TgT4

NodaMoney features automatic rounding:

using System;
using NodaMoney;

var value = new Money(10.999m, "USD");
Console.WriteLine(value);

NodaMoney round-ups

Running in Fiddle: https://dotnetfiddle.net/B1xoOs

NodaMoney features an interesting function for allocating amounts, used to split money without losing any cents.

Consider an example where we need to split $100 among three people.

using System;
using NodaMoney;

var prize = new Money(100m, "USD");

var shares = prize.Split(3);

foreach (var share in shares)
{
    Console.WriteLine(share);
}

This is important because it avoids rounding issues and ensures that the sum of the parts exactly equals the original amount.

NodaMoney split function

Running in Fiddle: https://dotnetfiddle.net/LN8eZV

Final Thoughts

In the examples presented in the section on common errors, money was treated merely as a decimal value. Any part of the code could alter these values, and the rules were scattered throughout the codebase.

After implementing Fowler’s Money pattern, we arrived at a model that clearly reflects the domain’s intent. Money became a Value Object, rules were centralized, invalid states were eliminated and the code began to better communicate its intent through methods and behaviors.

We also explored using NodaMoney to save development time, as it encapsulates the key functions for handling money safely in ASP.NET Core.

However, more important than avoiding rounding errors is building a model that makes errors difficult to occur in the first place. When the domain is responsible for financial rules, the application becomes more predictable and easier to evolve.

I hope this post has helped you understand how to implement resilient systems when dealing with monetary values.

Read the whole story
alvinashcraft
2 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

AI has moved the bottlenecks in software development

1 Share

As an experienced software developer and the owner of a software tooling company I’d really like to continue to grow, I’m fully embracing the central role that AI assisted development now takes. I can’t say that I completely like how things have changed and we’re still clearly adapting as we go.

Just to complain a bit and try to make sense of our new world order, here’s where I think the bottle necks in software development have moved to, at least for me:

CI via GitHub Actions. We the entire OSS community are hammering GitHub in a probably unsustainable way and CI runs are now the ultimate bottle neck for Critter Stack development. This of course is after we got very aggressive with AI tools to expand the testing that runs in CI to the point where Wolverine currently has 46 CI gates and Marten has 32 (some of that is from matrix testing on different JSON serializers, .NET versions, and PostgreSQL versions). That explosion in usage plus GitHub implementing some rate limiting on the number of available action runners is definitely an impediment. More and more, I’m leaning back into using local testing runs to gate commits where I think that is suitably low risk. I think it’s also worth thinking hard about making CI runs be more selective about what CI gates are necessary for different pull requests or commits as another way of reducing usage.

I don’t know how I feel about this yet, but we’re occasionally opting for much bigger pull requests like this one that spans 8-9 issues. I know there’s a bit of a backlash against pull requests right now for plenty of reasons I’m quite sympathetic to, but pull requests provide tracking and visibility into progress and release notes for us. I’d like to keep using pull requests for that reason only for right now, but it’s something we need to evaluate changing.

Test execution time. Most of the Critter Stack tools almost have to be tested through their integration with infrastructure like databases and message brokers. To make that much worse, we have to frequently stand up and trash .NET applications with different configurations to test more use cases. The advent of AI has also made it much easier to build in far, far more test coverage to explore more permutations, “soak” tests, and other exploratory testing. Yea for more test coverage! Boo for the increased time to run test suites. Also, and this is a good problem to have, AI is making it much easier for our users to add actionable issues for normally hard to observe problems in the tools they find from their own exploratory testing (which is also easier to do now and you should!) — and those issues tend to require specialized testing harnesses to recreate their exact problem and tend to be expensive tests to run.

My own energy level. The increased mental overload from context switching, code reviews, and generally trying to make decisions fast enough and well enough to keep the agents spinning makes me burn out faster during the day. I take more breaks now than I did before.

Development box resources. I got unexpectedly lucky by needing to replace my main development box earlier this year right before memory prices spiked. In all seriousness, memory and CPU utilization for locally running Docker containers and our heavyweight test suites “limits” how many parallel agents I run — but that number is admittedly more than I can generally handle mentally anyway.

Nuget.org indexing time. I doubt this impacts many people nearly as much as it does me, but we’re frequently having to do cross repository work in the Critter Stack lately, and the latency of Nuget.org indexing newly published packages is noticeable, if better than I think it was this time last year.

Sorry, I don’t have a ton of answers to any of these problems or expect them to be the exact same issues you might be experiencing yourself. But hey, it’s at least good to start seeing where the bottlenecks are and concentrate our efforts to solving those problems first I guess.

And apropos of nothing, my colleague is lounging on the job again today and I’m pretty sure that anyone walking into my office is going to know right off the bat that this is the lair of a Gen X guy who finally has the posters he wish he’d had as a kid.



Read the whole story
alvinashcraft
3 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

AI Is Making Code Generation Cheaper—and Engineering Judgment More Valuable

1 Share

AI can make implementation dramatically faster. It cannot make the engineering trade-offs for us.

Read the whole story
alvinashcraft
3 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories