Chapter 6. The New .NET Antipatterns

Out with the old! To modernize .NET applications and get the benefits of cloud native—remember, it’s about software that’s more scalable, more adaptable to change, more tolerant of failure, and more manageable—we need a different approach. How we built software a decade ago was a reflection of the use cases, technologies, and knowledge we had then. Although we don’t apologize for that, all of those aspects have evolved. And quickly! In this chapter, we take a look at a handful of things that we used to do with our .NET apps and why they now represent antipatterns.

These items are “antipatterns” because they make it more difficult for your software to behave in a cloud-native fashion. These patterns are not inherently awful, but they no longer reflect best practices. For each item, I call out why you should avoid it, and what you should do instead.

.NET Application Architecture Antipatterns

These antipatterns directly relate to how you build the functionality of your application. As you modernize your .NET software, these are things that demand refactoring.

In-Process State

ASP.NET makes it super easy to store and retrieve values across user requests. Are you planning on stashing customer-entered data through a multipage wizard experience? Just use the following:

Session["CustomerName"] = txtCustomerName.Text;

Although ASP.NET offers multiple storage options for session state, the default behavior is to store this information in the host server’s memory. Combine this with sticky session routing from your load balancer, and you were good to go.

So what’s the problem? Scalability and fault tolerance. When you pin anything to an individual server, you’re asking for trouble. What happens if you’re in a container that gets shut down? Or what if server instances rapidly scale out to handle load, but your user can’t escape the overburdened server on which their session is trapped? Cloud natives store session information in a highly available, off-box database that all application instances share.

Integrated Windows Authentication

I’m guilty of using this a lot during my earlier days of coding. For corporate application users, this Windows capability made it easy to create a single sign-on (SSO) experience. Just mix Internet Explorer, Windows desktops, Active Directory, and an IIS web server for free SSO! Although this combo transparently handled the various security handshakes, it also tightly coupled you to Windows environments. At times, it also demanded interactive sessions in which a user needed to key in their credentials.

How is this in conflict with cloud-native principles? It negatively affects software scale and portability. This pattern doesn’t work outside of domain-joined Windows Server environments. So there goes any Linux servers in your architecture. It also means that non-.NET applications have a trickier time authenticating and authorizing users in this environment. Microsoft’s recommendation for modern web apps? Use OpenID Connect for authentication. You can still use Active Directory as an identity provider if you want, but through OpenID Connect and OAuth 2.0, you take advantage of a standard flow and interface that are usable across operating systems and programming languages.

Using Custom ISAPI Filters or IIS Modules and Handlers

There’s no question that Internet Information Server (IIS) for Windows is a powerful web server. For years, Windows-based programmers took advantage of IIS extensibility to augment their web applications. We developed C-based ISAPI filters that acted on individual sites, or all sites on the server. These filters changed incoming request data, modified responses, performed custom logging, and much more. In later versions of IIS (starting with 7.0), we could use .NET to build modules, which affect all requests, or handlers, which affect specific request paths or extensions.

Now? Stop creating these. It makes it more difficult to change your software later, and affects manageability. We want software that doesn’t require any preconfiguration on the target host. This ensures more consistent, speedy deployments and scaling exercises. Everything our software needs to run is part of the deployment package, and scoped to just that deployment. You can’t expect that serverwide configurations or ISAPI filters are preinstalled anywhere. Put any HTTP request handling into code that’s part of your app. Or, take advantage of the built-in request handling capabilities included in your application platform or service mesh. Just get those capabilities out of the host!

Using the Local Disk for Storage

It’s hard to think of an application that doesn’t use some sort of storage. One straightforward choice for .NET developers is storage that’s attached to the host machine. It’s just so easy! Every Windows virtual machine has a C: drive, at minimum. Heck, the Microsoft documentation for the File class has you create a C: emp folder to try out the code. Why not use this accessible storage to stash uploaded images or stream out media files?

Using local storage limits your scalability and affects your fault tolerance. In a cloud-native world, hosts are ephemeral. They live for short periods. Treat anything on a local disk as replaceable. Instead of using local storage, switch to a highly available file share or, even better, object storage. Modern object storage—in the public cloud or on-premises—offers HTTP APIs, strong durability, and impressive scale. You can still use local storage, but treat it as a scratch location for temporary content.

Building and Running Windows Services

Chapter 2 discusses the many types of .NET software. Windows Services run as long-running background jobs without a user interface. They often start when the machine starts, and can run in their own security context. These types of apps offer a useful mechanism for executing scheduled activities—empty out an FTP share every evening—or never-ending processing, such as pulling new purchase orders from a job queue.

Why aren’t these friendly to cloud-native architectures? You might find them slowing down your change rate or limiting your manageability. First, like IIS antipatterns, they’re Windows specific. Second, Windows Services aren’t platform managed; they’re managed by the OS. A server manages its Windows Services. In a cloud-native world, platforms manage software. They schedule and monitor the workloads. And finally, Windows Services aren’t a truly native part of .NET Core. There are workarounds, but as of this writing, it’s not straightforward. Your best bet for background work is to create .NET Framework or .NET Core console applications that are deployed to a platform and easily scale to meet demand.

Leveraging the MSDTC

Back in the day, I spent many hours wrangling with the Microsoft Distributed Transaction Coordinator (MSDTC), a native Windows component for executing two-phase-commit transactions across distributed resources. The capability is tantalizing: create an all-or-nothing operation set that spans databases, message queues, and file-systems. The reality didn’t always match the dream. There weren’t many supported backend systems, and I inevitably bumped into some strange edge case. Your modern apps might not use MSDTC, but I’d bet that you have some modernization candidates that are drenched in distributed transactions.

So is MSDTC just complicated, or actually an antipattern? It’s the latter. It doesn’t work with modern (cloud) data sources or messaging technologies and is Windows only. And, most important, it represents a pattern (“distributed transactions”) that is counter to the cloud-native focus on scalability and fault tolerance. Transactions are difficult in the first place, but when you begin spanning (long-running) processes and geographies, it becomes a cost-prohibitive approach.

The solution takes us back up to the software design. You want single-responsibility services that might use a synchronous transaction internally, but don’t require cross-service transactions. This often requires you to think of individual transactions, and asynchronously handing off to the next step. Your .NET web application might call a service to charge the customer’s credit card, commit that transaction, and then hand off to a service that queues the product for shipping. Those two activities aren’t in a single distributed transaction. They commit individually, but with safeguards (e.g., retries, success indicators) to ensure that the overall order process completes.

API Calls That Require User Permission

If you use Windows, you’re painfully aware of those User Account Control (UAC) notifications that pop up and ask you to confirm a request for elevated access. This happens when software running on behalf of a standard user wants to do something that requires administrative permission. On the surface, that’s fine and a good practice. For server-based software for which no one is there to “approve” the request, though, it’s a killer to scalability and software-run-by-software.

You’ll want to adopt a model in which services have necessary access to host resources with whatever identity they operate under. Anything that requires administrator intervention is a no-no. Drop any of that .NET code that requests UAC elevation, and ensure that the app permissions are sufficient for anything that needs to be accessed on the host machine or container.

Configuration and Instrumentation Antipatterns

This group of antipatterns relates to how to store configuration data and instrument your applications.

Using web.config for Environment-Specific Values

I have a confession. I used to love adding key/value pairs to my ASP.NET web.config files. What an easy way to stash configuration settings! I could change connection strings or feature flags without recompiling the code. With a small enough web farm, I might even change these values directly in production. What was a questionable practice 10 years ago, however, is now an obvious no-no.

Putting any editable configuration values into the application package limits manageability and introduces the risk of mismatched configurations among app instances. Anything deployed as part of the application should be versioned, and any changes should trigger a new deployment. Changing anything manually in an environment is a recipe for disaster. If you want the same immutable application package as you deploy between environments, you need to externalize the configuration. That means using environment variables—or even better, an external configuration store—for any values that are environment specific.

machineKey in the machine.config File

The machineKey is used in ASP.NET to protect Forms authentication data and view-state data. Every server node in a farm needs the same machineKey value. Traditionally, this value is managed via the IIS Manager, and the generated key is stored in the server-wide machine.config file.

When deploying .NET software to application platforms, you’ll want to do something different. You might not have an IIS Manager experience handy and want to scale in a cloud-native way. This means overriding the machineKey value in the web.config file. This way, the necessary value is part of the immutable application package and is not dependent on anything on the host server.

Using the Windows Registry or Windows-Specific Logging

The Windows Registry is a database that stores a hierarchy of settings. Windows itself uses the Windows Registry to store settings, and many software packages plant values there. If you build software for Windows, you’re also familiar with writing information to the Event Log. All of these represent something that’s convenient for development, but a hindrance to scale and manageability.

If your code depends on the Windows Registry, you’ll have a more difficult time porting the application to .NET Core. Obviously, the Registry isn’t available on a Linux server. Although Windows Containers do offer access to a locally scoped Windows Registry, this database isn’t a versioned configuration store, and you shouldn’t use it for important values.

If you write application information to the Windows Event Log, you’re restricting your manageability. Today’s operators don’t want to terminal into individual servers to scrape local logs. Rather, your application should use libraries or platforms that ship logs to a central place. This improves your ability to troubleshoot problems while ensuring that you don’t lose valuable data if a host goes away.

Application Dependencies and Deployment Anti-Patterns

This final section reviews anti-patterns to avoid when bundling applications and deploying them to each environment.

Global Assembly Cache Dependencies

The Global Assembly Cache (GAC) is a machine-wide collection of assemblies. When you register an assembly in the GAC, it can be used by any app on the server. Before an assembly can be registered, it must be strong-named—that is, signed with a key. The point of strong-naming is to create a unique name for the key and support side-by-side versioning for a given assembly. The GAC was created to eliminate DLL Hell—the classic case in which multiple apps break when a shared component is updated.

Why does the GAC go against your cloud-native principles? A 12-factor app declares its dependencies and includes what it needs to run. You can’t assume anything exists on the target server. Any components needed by your application should be a part of your app. Otherwise, you’re stuck with complex routines to prepare a server before deploying an instance of your app. You can’t autoscale if that’s the case!

Interactive Installations

You should use Windows Installers only when deploying to the GAC, according to Microsoft. That’s typically an interactive deployment in which an administrator clicks a wizard to choose installation locations and deployment settings. You can have unattended installations, but this is just one example of something that limits you in your quest to become cloud native.

Avoid having any stage of software deployment require human intervention. This means that your deployment environment can’t run on anything with an interactive Windows Installer. No software drivers, Windows Services, or Windows extensions that have an installation wizard. Why? Because it means you can’t scale on demand to system-generated environments. Cloud natives use automated platforms to build identical environments and keep them up to date. Ensure that everything your software depends on is bin deployable, and ready to run immediately on any automation-created server.

Summary

In this chapter, we reviewed a handful of antipatterns. Often, the hardest part about using a new technology or paradigm is unlearning what we’ve been doing for so long. If you’ve been doing the items listed here, don’t feel bad. Many of these patterns were suitable at one point. But now it’s time to evolve and adopt patterns that promote scale, fault tolerance, changeability, and manageability.

Chapter 7 focuses on modern libraries and services that help you modernize your .NET apps by introducing new cloud-native patterns to your code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.175.253