Joe Mayo Modern Recipes for Professional Developers C# Cookbook
(This page has no text content)
Joe Mayo C# Cookbook Modern Recipes for Professional Developers
978-1-492-09369-5 [LSI] C# Cookbook by Joe Mayo Copyright © 2022 Mayo Software, LLC. All rights reserved. Printed in the United States of America. Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472. O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com. Acquisitions Editor: Amanda Quinn Development Editor: Angela Rufino Production Editor: Katherine Tozer Copyeditor: Justin Billing Proofreader: Piper Editorial Consulting, LLC Indexer: WordCo Indexing Services, Inc. Interior Designer: David Futato Cover Designer: Karen Montgomery Illustrator: Kate Dullea October 2021: First Edition Revision History for the First Edition 2021-09-29: First Release See http://oreilly.com/catalog/errata.csp?isbn=9781492093695 for release details. The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. C# Cookbook, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc. The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
Table of Contents Preface. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . vii 1. Constructing Types and Apps. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1 Managing Object End-of-Lifetime 2 1.2 Removing Explicit Dependencies 6 1.3 Delegating Object Creation to a Class 10 1.4 Delegating Object Creation to a Method 12 1.5 Designing Application Layers 16 1.6 Returning Multiple Values from a Method 21 1.7 Converting from Legacy to Strongly Typed Classes 24 1.8 Making Classes Adapt to Your Interface 28 1.9 Designing a Custom Exception 30 1.10 Constructing Objects with Complex Configuration 33 2. Coding Algorithms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 2.1 Processing Strings Efficiently 38 2.2 Simplifying Instance Cleanup 41 2.3 Keeping Logic Local 43 2.4 Operating on Multiple Classes the Same Way 45 2.5 Checking for Type Equality 48 2.6 Processing Data Hierarchies 52 2.7 Converting from/to Unix Time 55 2.8 Caching Frequently Requested Data 58 2.9 Delaying Type Instantiation 60 2.10 Parsing Data Files 63 3. Ensuring Quality. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 3.1 Writing a Unit Test 68 iii
3.2 Versioning Interfaces Safely 72 3.3 Simplifying Parameter Validation 74 3.4 Protecting Code from NullReferenceException 76 3.5 Avoiding Magic Strings 81 3.6 Customizing Class String Representation 83 3.7 Rethrowing Exceptions 85 3.8 Managing Process Status 90 3.9 Building Resilient Network Connections 91 3.10 Measuring Performance 94 4. Querying with LINQ. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 4.1 Transforming Object Shape 98 4.2 Joining Data 100 4.3 Performing Left Joins 104 4.4 Grouping Data 108 4.5 Building Incremental Queries 111 4.6 Querying Distinct Objects 116 4.7 Simplifying Queries 120 4.8 Operating on Sets 123 4.9 Building a Query Filter with Expression Trees 127 4.10 Querying in Parallel 134 5. Implementing Dynamic and Reflection. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139 5.1 Reading Attributes with Reflection 140 5.2 Accessing Type Members with Reflection 144 5.3 Instantiating Type Members with Reflection 150 5.4 Invoking Methods with Reflection 159 5.5 Replacing Reflection with Dynamic Code 162 5.6 Performing Interop with Office Apps 164 5.7 Creating an Inherently Dynamic Type 169 5.8 Adding and Removing Type Members Dynamically 172 5.9 Calling Python Code from C# 174 5.10 Calling C# Code from Python 177 6. Programming Asynchronously. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 6.1 Creating Async Console Applications 182 6.2 Reducing Memory Allocations for Async Return Values 184 6.3 Creating Async Iterators 187 6.4 Writing Safe Async Libraries 190 6.5 Updating Progress Asynchronously 193 6.6 Calling Synchronous Code from Async Code 195 6.7 Waiting for Parallel Tasks to Complete 198 iv | Table of Contents
6.8 Handling Parallel Tasks as They Complete 201 6.9 Cancelling Async Operations 206 6.10 Disposing of Async Resources 209 7. Manipulating Data. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 7.1 Generating Password Hashes 215 7.2 Encrypting and Decrypting Secrets 219 7.3 Hiding Development Secrets 222 7.4 Producing JSON 224 7.5 Consuming JSON 227 7.6 Working with JSON Data 232 7.7 Consuming XML 239 7.8 Producing XML 243 7.9 Encoding and Decoding URL Parameters 246 7.10 Flexible DateTime Reading 249 8. Matching with Patterns. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253 8.1 Converting Instances Safely 253 8.2 Catching Filtered Exceptions 257 8.3 Simplifying Switch Assignments 258 8.4 Switching on Property Values 261 8.5 Switching on Tuples 263 8.6 Switching on Position 266 8.7 Switching on Value Ranges 269 8.8 Switching with Complex Conditions 270 8.9 Using Logical Conditions 273 8.10 Switching on Type 275 9. Examining Recent C# Language Highlights. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 9.1 Simplifying Application Startup 280 9.2 Reducing Instantiation Syntax 281 9.3 Initializing Immutable State 284 9.4 Creating Immutable Types 286 9.5 Simplifying Immutable Type Assignments 291 9.6 Designing for Record Reuse 292 9.7 Returning Different Method Override Types 294 9.8 Implementing Iterators as Extension Methods 297 9.9 Slicing Arrays 299 9.10 Initializing Entire Modules 301 Table of Contents | v
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305 Index. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 vi | Table of Contents
Preface Why I Wrote This Book In the course of a career, we collect many tools. Whether concepts, techniques, patterns, or reusable code, these tools help us get our job done. The more we collect, the better, because we have so many problems to solve and applications to build. C# Cookbook contributes to your toolset by providing you with a variety of recipes. Things change over time, including programming languages. As of this writing, the C# programming language is over 20 years old, and software development has changed during its lifetime. There are a lot of recipes that could be written; this book acknowledges the evolution of C# over time and the fact that modern C# code makes us more productive. This book is full of recipes that I’ve used throughout my career. In addition to stating a problem, presenting code, and explaining the solution, each discussion includes deeper insight into why each recipe is important. Throughout the book, I’ve avoided advocacy of process or absolute declarations of “you must do it this way” because much of what we do in creating software requires trade-offs. In fact, you’ll find sev‐ eral discussions of what the consequences or trade-offs are with a recipe. This respects the fact that you can consider to what extent a recipe applies to you. Who This Book Is For This book assumes that you already know basic C# syntax. That said, there are recipes for various levels of developers. Whether you’re a beginner, intermediate, or senior developer, there should be something for you. If you’re an architect, there might be some interesting recipes that help you get back up to speed on the latest C# techniques. vii
How This Book Is Organized When brainstorming for this book, the entire focus was on answering the question “What do C# developers need to do?” Looking at the list, certain patterns emerged and evolved into chapters: • One of the first things I do when writing code is to build the types and organize the application. So I wrote Chapter 1 to show how to create and organize types. You’ll see recipes dealing with patterns because that’s how I code. • After creating types, we add type members, like methods, and the logic they con‐ tain, which is a natural category of recipes for Chapter 2. • What good is code unless it works well? That’s why I added Chapter 3, which contains recipes that help improve the quality of code. While this chapter is packed with useful recipes, you’ll want to check out the recipe that shows how to use nullable reference types. While Chapters 1 through 3 follow the “What do C# developers need to do?” theme, from Chapter 4 to the end of the book I break away, taking a technology-specific focus: • Many people think of Language Integrated Query (LINQ) as a database technol‐ ogy. While LINQ is useful for working with databases, it’s also excellent for in- memory data manipulation and querying. That’s why Chapter 4 discusses what you can do with the in-memory provider, called LINQ to Objects. • Reflection was part of C# 1, but dynamic programming came along later in C# 4. I think it’s important to discuss both technologies in Chapter 5 and even show how dynamic programing can be better than reflection in some situations. Also, be sure to check out the Python interop recipes. • Async programming was a great addition to C# and seems straightforward, on the surface. Chapter 6 covers async with recipes that explain several important features you might not be aware of. • All apps use data, whether securing, parsing, or serializing. Chapter 7 includes several recipes covering different things you want to do with data. It focuses on some of the newer libraries and algorithms you might want to use for working with data. • One of the largest transformations of the C# language occurred over the last few versions in the area of pattern matching. There are so many that I was able to fill Chapter 8 with only pattern-matching recipes. • C# continues to evolve and Chapter 9 captures recipes dedicated to C# 9. We’ll look at some of the new features and discuss how to apply them. While I provide insight in the discussion, remember that sometimes new features can become viii | Preface
more integral to a language in later versions. If you’re into the cutting edge, these recipes are pretty interesting. Conventions Used in This Book The following typographical conventions are used in this book: Italic Indicates new terms, URLs, email addresses, filenames, and file extensions. Constant width Used for program listings, as well as within paragraphs to refer to program ele‐ ments such as variable or function names, databases, data types, environment variables, statements, and keywords. Constant width bold Shows commands or other text that should be typed literally by the user. Constant width italic Shows text that should be replaced with user-supplied values or by values deter‐ mined by context. This element signifies a tip or suggestion. This element signifies a general note. This element indicates a warning or caution. Preface | ix
Using Code Examples Supplemental material (code examples, exercises, etc.) is available for download at https://github.com/JoeMayo/csharp-nine-cookbook. If you have a technical question or a problem using the code examples, please send email to bookquestions@oreilly.com. This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission. We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “C# Cookbook by Joe Mayo (O’Reilly). Copyright 2022 Mayo Software, LLC, 978-1-492-09369-5.” If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com. O’Reilly Online Learning For more than 40 years, O’Reilly Media has provided technol‐ ogy and business training, knowledge, and insight to help companies succeed. Our unique network of experts and innovators share their knowledge and expertise through books, articles, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, visit http://oreilly.com. x | Preface
How to Contact Us Please address comments and questions concerning this book to the publisher: O’Reilly Media, Inc. 1005 Gravenstein Highway North Sebastopol, CA 95472 800-998-9938 (in the United States or Canada) 707-829-0515 (international or local) 707-829-0104 (fax) We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/c-sharp-cb. Email bookquestions@oreilly.com to comment or ask technical questions about this book. For news and information about our books and courses, visit http://oreilly.com. Find us on Facebook: http://facebook.com/oreilly Follow us on Twitter: http://twitter.com/oreillymedia Watch us on YouTube: http://www.youtube.com/oreillymedia Acknowledgments From concept to delivery, there are many people involved in creating a new book. I would like to recognize the people who helped on C# Cookbook. Amanda Quinn, Senior Content Acquisitions Editor, helped form the concept for the book and provided feedback as I outlined its contents. Angela Rufino, Content Devel‐ opment Editor, got me started on standards and tools, provided feedback on my writ‐ ing, and was incredibly helpful during the entire process. Bassam Alugili, Octavio Hernandez, and Shadman Kudchikar were tech editors, correcting errors, adding excellent insight, and sharing new ideas. Katherine Tozer, Production Editor and Vendor Coordinator, kept me up to date on new early releases and coordinated other production items. Justin Billing, Copyeditor, did a great job at improving my writing. There are many other people behind the scenes that made this book possible. I would like to thank all of you. I am grateful for your contributions and wish you all the best. Preface | xi
(This page has no text content)
CHAPTER 1 Constructing Types and Apps One of the first things we do as developers is design, organize, and create new types. This chapter helps with these tasks by offering several useful recipes for setting up the project, managing object lifetime, and establishing patterns. Establishing Architecture When you’re first setting up a project, you have to think about the overall architec‐ ture. There’s a concept called separation of concerns wherein each part of an applica‐ tion has a specific purpose (e.g., the UI layer interacts with users, a business logic layer manages rules, and a data layer interacts with a data source). Each layer has a purpose or responsibilities and contains the code to perform its operations. In addition to promoting more loosely coupled code, separation of concerns makes it easier for developers to work with that code because it’s easier to find where a certain operation occurs. This makes it easier to add new features and maintain existing code. The benefits of this are higher quality applications and more productive work. It pays to get started right, which is why we have Recipe 1.5. Related to loosely coupling code, there’s inversion of control (IoC), which helps decouple code and promotes testability. Recipe 1.2 explains how this works. When we look at ensuring quality, in Chapter 3, you’ll see how IoC fits in to unit testing. Applying Patterns A lot of the code we write is Transaction Script, where the user interacts with a UI and the code performs some type of create, read, update, or delete (CRUD) operation in the database and returns the result. Other times, we have complex interactions 1
between objects that are difficult to organize. We need other patterns to solve these hard problems. This chapter presents a few useful patterns in a rather informal manner. The idea is that you’ll have some code to rename and adapt to your purposes and a rationale on when a given pattern would be useful. As you read through each pattern, try to think about other code you’ve written or other situations where that pattern would have simplified the code. If you run into the problem of having different APIs to different systems and needing to switch between them, you’ll be interested in reading Recipe 1.8. It shows how to build a single interface to solve this problem. Managing Object Lifetime Other important tasks we perform relate to object lifetime—that is, instantiating objects in memory, holding objects in memory for processing, and the subsequent release of that memory when the object is no longer needed. Recipes 1.3 and 1.4 show a couple of nice factory patterns that let you decouple object creation from code. This fits in with the IoC concepts mentioned previously. One way to manage object creation is through a fluid interface, where you can include optional settings (via methods) and validate before object construction. Another important object-lifetime consideration is disposal. Think about the draw‐ backs of excessive resource consumption, including memory use, file locks, and any other object that holds operating system resources. These problems often result in application crashes and are difficult to detect and fix. Performing proper resource cleanup is so important that it’s the first recipe we’ll cover in the book. 1.1 Managing Object End-of-Lifetime Problem Your application is crashing because of excessive resource usage. Solution Here’s the object with the original problem: using System; using System.IO; public class DeploymentProcess { StreamWriter report = new StreamWriter("DeploymentReport.txt"); 2 | Chapter 1: Constructing Types and Apps
public bool CheckStatus() { report.WriteLine($"{DateTime.Now} Application Deployed."); return true; } } And here’s how to fix the problem: using System; using System.IO; public class DeploymentProcess : IDisposable { bool disposed; readonly StreamWriter report = new StreamWriter("DeploymentReport.txt"); public bool CheckStatus() { report.WriteLine($"{DateTime.Now} Application Deployed."); return true; } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { // disposal of purely managed resources goes here } report?.Close(); disposed = true; } } ~DeploymentProcess() { Dispose(disposing: false); } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } This is the Main method, using this object: 1.1 Managing Object End-of-Lifetime | 3
static void Main(string[] args) { using (var deployer = new DeploymentProcess()) { deployer.CheckStatus(); } } Discussion The problem in this code was with the StreamWriter report. Whenever you’re using some type of resource, such as the report file reference, you need to release (or dis‐ pose) that resource. The specific problem here occurs because the app, through the StreamWriter, requested a file handle from the Windows OS. This app owns that file handle, and Windows expects the owning app to release the handle. If your app closes without releasing that handle, Windows prevents all applications, including subse‐ quent running of your app, from accessing that file. In the worst case, everything crashes in a hard-to-find scenario that involves multiple people over a number of hours debugging a critical production problem. This occurs because Windows believes that file is still in use. The solution is to implement the dispose pattern, which involves adding code that makes it easy to release resources. The solution code implements the IDisposable interface. IDisposable only specifies the Dispose() method, without parameters, and there’s more to be done than just adding that method, including another Dispose method overload that keeps track of what type of disposal to do and an optional finalizer. Complicating the implementation is a field and parameter that control disposal logic: disposed and disposing. The disposed field ensures that this object gets disposed only one time. Inside the Dispose(bool) method, there’s an if statement, ensuring that if disposed is true (the object has been disposed), then it won’t execute any dis‐ posal logic. The first time through Dispose(bool), disposed will be false and the code in the if block will execute. Make sure that you also set disposed to true to ensure this code doesn’t run anymore—the consequences of not doing so bring expo‐ sure to unpredictable errors like an ObjectDisposedException. The disposing parameter tells Dispose(bool) how it’s being called. Notice that Dispose() (without parameters) and the finalizer call Dispose(bool). When Dis pose() calls Dispose(bool), disposing is true. This makes it easy for calling code, if written properly, to instantiate DeploymentProcess in a using statement or wrap it in the finally block of a try/finally. The finalizer calls Dispose(bool) with disposing set to false, meaning that it isn’t being run by calling application code. The Dispose(bool) method uses the dispos 4 | Chapter 1: Constructing Types and Apps
ing value to determine whether it should release managed resources. Unmanaged resources are released regardless of whether Dispose() or the finalizer calls Dispose(bool). Let’s clarify what is happening with the finalizer. The .NET CLR garbage collector (GC) executes an object’s finalizer when it cleans that object from memory. The GC can make multiple passes over objects, calling finalizers being one of the last things it does. Managed objects are instantiated and managed by the .NET CLR, and you don’t have control over when they’re released, which could potentially result in out-of- order releases. You have to check the disposing value to prevent an ObjectDisposed Exception in case the dependent object was disposed by the GC first. What the finalizer gives you is a way to clean up unmanaged resources. An unman‐ aged resource, such as the file handle that StreamWriter obtained, doesn’t belong to the .NET CLR; it belongs to the Windows OS. There are situations where developers might need to explicitly call into a Win32/64 dynamic link library (DLL) to get a han‐ dle to an OS or third-party device. The reason you need the finalizer is that if your object doesn’t get disposed properly, there’s no other way to release that handle, which could crash your system for the same reason we need to release managed objects. So, the finalizer is a just-in-case mechanism to ensure the code that needs to release the unmanaged resource will execute. A lot of applications don’t have objects that use unmanaged resources. In that case, don’t even add the finalizer. Having the finalizer adds overhead to the object because the GC has to do accounting to recognize objects that do have finalizers and call them in a multi-pass collection. Omitting the finalizer avoids this. On a related note, remember to call GC.SuppressFinalize in the Dispose() method. This is another optimization telling the GC to not call the finalizer for this object, because all resources—managed and unmanaged—are released when the application calls IDisposable.Dispose(). Generally, you should always call GC.SuppressFinalize in Dispose(), even if the class doesn’t have a finalizer. That said, there are some nuances that you might be interested in. If a class is both sealed and doesn’t have a finalizer, you can safely omit the call to GC.SuppressFinalize. However, classes that aren’t sealed could potentially be inherited by another class that does include a final‐ izer. In this case, calling GC.SuppressFinalize protects against improper implementations. For classes without finalizers, GC.SuppressFinalize has no effect. If you chose to leave out the call to GC.SuppressFinalize and the class has a finalizer, the CLR will call that finalizer. 1.1 Managing Object End-of-Lifetime | 5
The Main method shows how to properly use the DeploymentProcess object. It instantiates and wraps the object in a using statement. The object exists in memory until the using statement block ends. At that time, the program calls the Dispose() method. 1.2 Removing Explicit Dependencies Problem Your application is tightly coupled and difficult to maintain. Solution Define the types you need: public class DeploymentArtifacts { public void Validate() { System.Console.WriteLine("Validating..."); } } public class DeploymentRepository { public void SaveStatus(string status) { System.Console.WriteLine("Saving status..."); } } interface IDeploymentService { void PerformValidation(); } public class DeploymentService : IDeploymentService { readonly DeploymentArtifacts artifacts; readonly DeploymentRepository repository; public DeploymentService( DeploymentArtifacts artifacts, DeploymentRepository repository) { this.artifacts = artifacts; this.repository = repository; } public void PerformValidation() 6 | Chapter 1: Constructing Types and Apps
Comments 0
Loading comments...
Reply to Comment
Edit Comment