Coding assistants can speed up .NET modernization, but they can still miss the architecture, behavioral dependencies, testing requirements, and operational details that matter in production. This field guide shares common failure patterns and practical review habits to help engineering teams use AI as an accelerator without handing critical modernization decisions to generated code.

Coding assistants have become part of how we estimate and execute modernization. They read a .NET Framework solution faster than a new teammate. They draft a migration plan, sketch project files, and propose replacements for APIs that have been obsolete since the last decade. Used with a senior engineer in the loop, they shorten the boring parts of a proof of concept.

They also fail in patterns that look convincing in a pull request. After several modernization efforts, including work we have already described around GitHub Copilot's modernization agent, the misses are no longer surprising. They cluster around architecture, framework behavior, tests, and the parts of a codebase that only exist because production required them.

This post is a field guide to those misses, and to the review habits that keep them from reaching a release branch.

They describe a clean target and understate the path

Ask an assistant to modernize a Framework 4.8 application and it will often describe a tidy end state: SDK-style projects, current target frameworks, updated packages, and a list of breaking API substitutions. That end state is usually directionally right. The plan is rarely complete about the order of operations.

Real solutions have setup projects, old installer logic, MSBuild targets copied forward from 2014, binding redirects that exist because two vendors disagreed, and shared libraries that still serve an application you are not migrating this quarter. The assistant will propose moving everything at once because that is the shape of the samples it has seen. A workable plan usually isolates a vertical slice, gets that slice building under the new SDK, and only then expands the surface area.

When we let the generated plan stand unedited, estimates collapse at the first project that still uses a designer file or a custom build step. The assistant did not hide that complexity out of malice. It optimized for a narrative of progress.

They confuse compiling with behaving the same way

A modernization assistant is at its best when the failure is local and mechanical: a namespace that moved, a package that needs a new major version, a configuration section that now belongs in a different file. It is much weaker on behavioral change that still compiles.

Configuration binding is a regular example. The old code read settings with a pattern the team had patched around for years. The generated replacement reads the same keys through new APIs and misses the fallback order production depended on. Authentication cookies, same-site behavior, and outgoing HTTP defaults have changed under the same class names. Encoding and culture defaults have surprised more than one reporting screen.

We treat a green build as the start of review, not the end. The checklist is unfashionable on purpose: compare configuration at runtime, hit the authentication paths, exercise file and network access, and run the batch jobs that nobody wants to demo. Assistants do not volunteer those tests unless you already wrote them.

They rewrite working code into a generic style

Give an assistant a service that has grown around a vendor SDK or a state machine the business invented, and it will often "simplify" the code into a textbook repository and manager pair. The result is easier to read and sometimes wrong.

The original awkwardness is frequently load-bearing. Maybe the extra lock exists because a nightly import overlaps a user action. Maybe the odd mapping exists because a partner sends duplicate identifiers. Maybe the static helper is ugly and also the only place a regulatory calculation is allowed to live.

Assistants are trained to prefer common structure. Modernization is often the work of preserving uncommon structure while changing the platform under it. Our review rule is that a generated refactor must explain which production behavior it is trying to keep. If the pull request only says the new code is cleaner, it goes back.

They invent compatibility that is not there

Package suggestions are a mixed bag. Current, well-known libraries are handled reasonably. The trouble starts with older commercial components, interop assemblies, and internal packages that have no public migration story.

The assistant will recommend a successor package because the names are similar, or it will say a library supports the new target framework because a later version does. We have seen proposed replacements that drop an encryption mode the partner still requires, and suggestions to swap a battle-tested library for a newer one that has not been used with the rest of the stack.

Dependency decisions stay with the engineer who can check license terms, support status, and the actual assembly surface. The assistant can prepare the comparison. It should not close it.

They write tests that follow the new code instead of the old contract

Generated tests tend to assert that the modernized method returns whatever the modernized method now returns. That is circular. The tests you need during a platform change encode the old contract: inputs from production, output shapes consumers already rely on, error codes partners already handle.

If those fixtures do not exist, writing them is part of the modernization, not an optional extra. We use assistants to scaffold test projects and to propose cases from existing samples. We do not accept a suite whose expected values were produced by the migrated code on the same day.

UI and integration tests have the same issue in a different coat. Selectors and timing that were already fragile get copied forward. The assistant cannot tell a flaky wait from a real race unless you give it failure history.

They miss operations: hosting, secrets, and the bits around the sln file

The solution file is not the system. Modernization also moves how the application is hosted, how secrets arrive, how logs are shipped, and how a release is signed off. Assistants will generate an optimistic Dockerfile or suggest a generic host builder and then stop.

They rarely account for scheduled tasks that still assume a working directory on a particular server, for file shares that an app pool identity can access, or for certificates that were installed by a process nobody documented. They will put connection strings into the new configuration format and leave the rotation procedure untouched.

We keep a separate work list for those items and do not let a generated migration checklist mark them complete because the template included a heading called "deployment."

What we do instead of hoping for a better model

The working pattern is consistent across tools. The assistant reads the repository and drafts an assessment. An engineer who has shipped on the current stack edits the plan against reality. The first code changes are small and reversible. Behavioral tests are written or recovered before large refactors. Generated diffs are reviewed with the same suspicion we would apply to a contractor who had only read the README.

We still use the tools. Assessment is faster. Boilerplate dies quicker. API mapping is a useful first pass. The remaining work is the part modernization has always required: deciding what the system is actually supposed to do when the framework no longer papers over it.

A coding assistant is a junior teammate with extraordinary recall and no production scars. On .NET modernization, that is valuable and insufficient. Keep the engineer accountable for behavior, dependencies, tests, and operations, and the assistant stays an accelerator. Hand those decisions to the generated plan, and you will modernize the architecture diagram while the running system keeps its old surprises.