Pro C#10 CHAPTER 16 Building and Configuring Class Libraries

CHAPTER 16

Building and Configuring Class Libraries

For most of the examples so far in this book, you have created “stand-alone” executable applications, in which all the programming logic was packaged within a single assembly (.dll) and executed using
dotnet.exe (or a copy of dotnet.exe named after the assembly). These assemblies were using little more
than the .NET base class libraries. While some simple .NET programs may be constructed using nothing more than the base class libraries, chances are it will be commonplace for you (or your teammates) to isolate reusable programming logic into custom class libraries (
.dll files) that can be shared among applications.
In this chapter, you’ll start by learning the details of partitioning types into .NET namespaces. After this, you will take a deep look at class libraries in .NET, the difference between a .NET and .NET Standard, application configuration, publishing .NET console applications, and packaging your libraries into reusable NuGet packages.

Defining Custom Namespaces (Updated 10.0)
Before diving into the aspects of library deployment and configuration, the first task is to learn the details of packaging your custom types into .NET namespaces. Up to this point in the text, you’ve been building
small test programs that leverage existing namespaces in the .NET universe (System, in particular). However, when you build larger applications with many types, it can be helpful to group your related types into custom namespaces. In C#, this is accomplished using the namespace keyword. Explicitly defining custom namespaces is even more important when creating shared assemblies, as other developers will need to reference the library and import your custom namespaces to use your types. Custom namespaces also prevent name collisions by segregating your custom classes from other custom classes that might have the same name.
To investigate the issues firsthand, begin by creating a new .NET Console Application project named CustomNamespaces. Now, assume you are developing a collection of geometric classes named Square, Circle, and Hexagon. Given their similarities, you would like to group them into a unique namespace called CustomNamespaces.MyShapes within the CustomNamespaces.exe assembly.

■ Guidance While you are free to use any name you choose for your namespaces, the naming convention is typically similar to CompanyName.ProductName.AssemblyName.Path.

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_16

625

While the C# compiler has no problems with a single C# code file containing multiple types,
this can be problematic when working in a team environment. If you are working on the Circle class and your co-worker needs to work on the Hexagon class, you would have to take turns working in the monolithic file or face difficult-to-resolve (well, at least time-consuming) merge conflicts.
A better approach is to place each class in its own file with a namespace definition. To ensure each type is packaged into the same logical group, simply wrap the given class definitions in the same namespace scope. The following example uses the pre–C# 10 namespace syntax, where each namespace declaration wrapped its contents with an opening and closing curly brace, like so:

// Circle.cs
namespace CustomNamespaces.MyShapes
{
// Circle class
public class Circle { / Interesting methods… / }
}
// Hexagon.cs
namespace CustomNamespaces.MyShapes
{
// Hexagon class
public class Hexagon { / More interesting methods… / }
}
// Square.cs
namespace CustomNamespaces.MyShapes
{
// Square class
public class Square { / Even more interesting methods…/}
}

One of the updates in C# 10 is the addition of file-scoped namespaces. This eliminates the need for opening and closing curly braces wrapping the contents. Simply declare the namespace, and everything that comes after that namespace declaration is included in the namespace. The following code sample produces the same result as the example with the namespaces wrapping their contents:

// Circle.cs
namespace CustomNamespaces.MyShapes;
// Circle class
public class Circle { / Interesting methods… / }
// Hexagon.cs
namespace CustomNamespaces.MyShapes;
// Hexagon class
public class Hexagon { / More interesting methods… / }
// Square.cs
namespace CustomNamespaces.MyShapes;
// Square class
public class Square { / Even more interesting methods…/}

This change by the C# team (along with global using statements and implicit global using statements) has eliminated a lot of the boilerplate code that was necessary in versions of C# prior to C# 10. In fact, as I updated the book for this version, I was able to remove on average two pages per chapter, just by removing the (now) unnecessary using statements and curly braces!

Notice how the CustomNamespaces.MyShapes namespace acts as the conceptual “container” of these classes. When another namespace (such as CustomNamespaces) wants to use types in a separate namespace, you use the using keyword, just as you would when using namespaces of the .NET base class libraries, as follows:

// Make use of types defined the MyShapes namespace. using CustomNamespaces.MyShapes;

Hexagon h = new Hexagon(); Circle c = new Circle(); Square s = new Square();

For this example, the assumption is that the C# files that define the CustomNamespaces.MyShapes namespace are part of the same Console Application project; in other words, all the files are compiled into a single assembly. If you defined the CustomNamespaces.MyShapes namespace within an external assembly, you would also need to add a reference to that library before you could compile successfully. You’ll learn all the details of building applications that use external libraries during this chapter.

Resolving Name Clashes with Fully Qualified Names
Technically speaking, you are not required to use the C# using keyword when referring to types defined in external namespaces. You could use the fully qualified name of the type, which, as you may recall from Chapter 1, is the type’s name prefixed with the defining namespace. Here’s an example:

// Note we are not importing CustomNamespaces.MyShapes anymore! CustomNamespaces.MyShapes.Hexagon h = new CustomNamespaces.MyShapes.Hexagon(); CustomNamespaces.MyShapes.Circle c = new CustomNamespaces.MyShapes.Circle(); CustomNamespaces.MyShapes.Square s = new CustomNamespaces.MyShapes.Square();

Typically, there is no need to use a fully qualified name. Not only does it require a greater number of keystrokes, it also makes no difference whatsoever in terms of code size or execution speed. In fact, in CIL code, types are always defined with the fully qualified name. In this light, the C# using keyword is simply a typing time-saver.
However, fully qualified names can be helpful (and sometimes necessary) to avoid potential name clashes when using multiple namespaces that contain identically named types. Assume you have a new namespace termed CustomNamespaces.My3DShapes, which defines the following three classes, capable of rendering a shape in stunning 3D:

// Another shape-centric namespace.
//Circle.cs
namespace CustomNamespaces.My3DShapes;
// 3D Circle class. public class Circle { }
//Hexagon.cs
namespace CustomNamespaces.My3DShapes;
// 3D Hexagon class. public class Hexagon { }
//Square.cs
namespace CustomNamespaces.My3DShapes;
// 3D Square class. public class Square { }

If you update the top-level statements as shown next, you are issued several compile-time errors, because both namespaces define identically named classes:

// Ambiguities abound!
using CustomNamespaces.MyShapes; using CustomNamespaces.My3DShapes;

// Which namespace do I reference?
Hexagon h = new Hexagon(); // Compiler error! Circle c = new Circle(); // Compiler error! Square s = new Square(); // Compiler error!

The ambiguity can be resolved using the type’s fully qualified name, like so:

// We have now resolved the ambiguity.
CustomNamespaces.My3DShapes.Hexagon h = new CustomNamespaces.My3DShapes.Hexagon(); CustomNamespaces.My3DShapes.Circle c = new CustomNamespaces.My3DShapes.Circle(); CustomNamespaces.MyShapes.Square s = new CustomNamespaces.MyShapes.Square();

Resolving Name Clashes with Aliases
The C# using keyword also lets you create an alias for a type’s fully qualified name. When you do so, you define a token that is substituted for the type’s full name at compile time. Defining aliases provides a second way to resolve name clashes. Here’s an example:

using MyShapes; using My3DShapes;

// Resolve the ambiguity for a type using a custom alias. using The3DHexagon = My3DShapes.Hexagon;

// This is really creating a My3DShapes.Hexagon class. The3DHexagon h2 = new The3DHexagon();

There is another (more commonly used) using syntax that lets you create an alias for a namespace instead of a type. For example, you could alias the CustomNamespaces.My3DShapes namespace, and create an instance of the 3D Hexagon as follows:

using ThreeD = CustomNamespaces.My3DShapes; ThreeD.Hexagon h2 = new ThreeD.Hexagon();

■Note Be aware that overuse of C# aliases for types can result in a confusing code base. if other programmers on your team are unaware of your custom aliases for types, they could have difficulty locating the real types in the project(s).

Creating Nested Namespaces
When organizing your types, you are free to define namespaces within other namespaces. The base class libraries do so in numerous places to provide deeper levels of type organization. For example, the IO namespace is nested within System to yield System.IO. In fact, you already created nested namespaces in the previous example. The multipart namespaces (CustomNamespaces.MyShapes and CustomNamespaces. My3DShapes) are nested under the root namespace, CustomNamespaces.
As you have seen already throughout this book, the .NET project templates add the initial code for console applications in a file named Program.cs. This file contains a namespace named after the project and a single class, Program. This base namespace is referred to as the root namespace. In our current example, the root namespace created by the .NET template is CustomNamespaces. To nest the MyShapes and My3DShapes namespaces inside the root namespace, there are three options. The first is to simply nest the namespace keyword, like this (using pre-C# 10 syntax):

namespace CustomNamespaces
{
namespace MyShapes
{
// Circle class public class Circle
{
/ Interesting methods… /
}
}
}

The second option, using C# 10 (and later), uses a file-scoped namespace followed by a block-scoped namespace for the nested namespace:

namespace CustomNamespaces; namespace MyShapes
{
// Circle class public class Circle
{
/ Interesting methods… /
}
}

The third option (and more commonly used) is to use “dot notation” in the namespace definition, as we did with the previous class examples:

namespace CustomNamespaces.MyShapes;
// Circle class public class Circle
{
/ Interesting methods… /
}

Namespaces do not have to directly contain any types. This allows developers to use namespaces to provide a further level of scope.

■Guidance a common practice is to group files in a namespace by directory. Where a file lives in the directory structure has no impact on namespaces. however, it does make the namespace structure clearer to other developers. therefore, many developers and code linting tools expect the namespaces to match the folder structures.

Change the Root Namespace Using Visual Studio 2022
As mentioned, when you create a new C# project using Visual Studio (or the .NET CLI), the name of your application’s root namespace will be identical to the project name. From this point on, when you use Visual Studio to insert new code files using the Project ➤ Add New Item menu selection, types will automatically be wrapped within the root namespace and have directory path appended. If you want to change the name of the root namespace, simply access the “Default namespace” option using the Application/General tab of the project’s properties window (see Figure 16-1).

Figure 16-1. Configuring the default/root namespace

■Note the Visual studio property pages still refer to the root namespace as the Default namespace. You will see next why i refer to it as the root namespace.

Change the Root Namespace Using the Project File
If not using Visual Studio (or even with Visual Studio), you can also configure the root namespace by updating the project (*.csproj) file. With .NET projects, editing the project file in Visual Studio is as easy as double-clicking the project file in Solution Explorer (or right-clicking the project file in Solution Explorer

and selecting “Edit project file”). Once the file is open, update the main PropertyGroup by adding the
RootNamespace node, like this:


Exe
net6.0
enable
disable
CustomNamespaces2

So far, so good. Now that you have seen some details regarding how to package your custom types into well-organized namespaces, let’s quickly review the benefits and format of the .NET assembly. After this, you will delve into the details of creating, deploying, and configuring your custom class libraries.

The Role of .NET Assemblies
.NET applications are constructed by piecing together any number of assemblies. Simply put, an assembly is a versioned, self-describing binary file hosted by the .NET Runtime. Now, despite that .NET assemblies have the same file extensions (.exe or .dll) as previous Windows binaries, they have little in common under the hood with those files. Before unpacking that last statement, let’s consider some of the benefits provided by the assembly format.

Assemblies Promote Code Reuse
As you have built your Console Application projects over the previous chapters, it might have seemed that all the applications’ functionality was contained within the executable assembly you were constructing. Your applications were leveraging numerous types contained within the always-accessible .NET base class libraries.
As you might know, a code library (also termed a class library) is a .dll that contains types intended to be used by external applications. When you are creating executable assemblies, you will no doubt be leveraging numerous system-supplied and custom code libraries as you create your application. Do be aware, however, that a code library need not take a .dll file extension. It is perfectly possible (although certainly not common) for an executable assembly to use types defined within an external executable file. In this light, a referenced *.exe can also be considered a code library.
Regardless of how a code library is packaged, the .NET platform allows you to reuse types in a language-independent manner. For example, you could create a code library in C# and reuse that library in any other .NET programming language. It is possible not only to allocate types across languages but also to derive from them. A base class defined in C# could be extended by a class authored in Visual Basic.
Interfaces defined in F# can be implemented by structures defined in C# and so forth. The point is that when you begin to break apart a single monolithic executable into numerous .NET assemblies, you achieve a language-neutral form of code reuse.

Assemblies Establish a Type Boundary
Recall that a type’s fully qualified name is composed by prefixing the type’s namespace (e.g., System) to its name (e.g., Console). Strictly speaking, however, the assembly in which a type resides further
establishes a type’s identity. For example, if you have two uniquely named assemblies (say, MyCars.dll and YourCars.dll) that both define a namespace (CarLibrary) containing a class named SportsCar, they are considered unique types in the .NET universe.

Assemblies Are Versionable Units
.NET assemblies are assigned a four-part numerical version number of the form ...

. (If you do not explicitly provide a version number, the assembly is automatically assigned a version of 1.0.0.0, given the default .NET project settings.) This number allows multiple versions of the same assembly to coexist in harmony on a single machine.

Assemblies Are Self-Describing
Assemblies are regarded as self-describing, in part because they record in the assembly’s manifest every external assembly they must be able to access to function correctly. Recall from Chapter 1 that a manifest is a blob of metadata that describes the assembly itself (name, version, required external assemblies, etc.).
In addition to manifest data, an assembly contains metadata that describes the composition (member names, implemented interfaces, base classes, constructors, etc.) of every contained type. Because an assembly is documented in such detail, the .NET Runtime does not consult the Windows system registry to resolve its location (quite the radical departure from Microsoft’s legacy COM programming model). This separation from the registry is one of the factors that enables .NET applications to run on other operating systems besides Windows as well as supporting multiple versions of .NET on the same machine.
As you will discover during this chapter, the .NET Runtime makes use of an entirely new scheme to resolve the location of external code libraries.

Understanding the Format of a .NET Assembly
Now that you’ve learned about several benefits provided by the .NET assembly, let’s shift gears and get a better idea of how an assembly is composed under the hood. Structurally speaking, a .NET assembly (*.dll or *.exe) consists of the following elements:
•An operating system (e.g., Windows) file header
•A CLR file header
•CIL code
•Type metadata
•An assembly manifest
•Optional embedded resources
While the first two elements (the operating system and CLR headers) are blocks of data you can typically always ignore, they do deserve some brief consideration. Here’s an overview of each element.

Installing the C++ Profiling Tools
The next few sections use a utility call dumpbin.exe, and it ships with the C++ profiling tools. To install them, type C++ profiling tools in the quick search bar, and click the prompt to install the tools, as shown in Figure 16-2.

Figure 16-2. Installing the C++ profiling tools from Quick Launch

This will bring up the Visual Studio installer with the tools selected. Alternatively, you can launch the Visual Studio installer yourself and select the components shown in Figure 16-3.

Figure 16-3. Installing the C++ profiling tools

The Operating System (Windows) File Header
The operating system file header establishes the fact that the assembly can be loaded and manipulated by the target operating system (in the following example, Windows). This header data also identifies the kind of application (console-based, GUI-based, or *.dll code library) to be hosted by the operating system.

Open the CarLibrary.dll file (in the book’s repo or created later in this chapter) using the dumpbin.exe
utility (via the developer command prompt) with the /headers flag as so:
dumpbin /headers CarLibrary.dll
This displays the assembly’s operating system header information (shown in the following when built for Windows). Here is the (partial) Windows header information for CarLibrary.dll:

Dump of file carlibrary.dll PE signature found
File Type: DLL

FILE HEADER VALUES
14C machine (x86)
3 number of sections 877429B3 time date stamp
0 file pointer to symbol table
0 number of symbols
E0 size of optional header 2022 characteristics
Executable
Application can handle large (>2GB) addresses DLL

Now, remember that most .NET programmers will never need to concern themselves with the format of the header data embedded in a .NET assembly. Unless you happen to be building a new .NET language compiler (where you would care about such information), you are free to remain blissfully unaware of the grimy details of the header data. Do be aware, however, that this information is used under the covers when the operating system loads the binary image into memory.

The CLR File Header
The CLR header is a block of data that all .NET assemblies must support (and do support, courtesy of the C# compiler) to be hosted by the .NET Runtime. In a nutshell, this header defines numerous flags that enable the runtime to understand the layout of the managed file. For example, flags exist that identify the location of the metadata and resources within the file, the version of the runtime the assembly was built against, the value of the (optional) public key, and so forth. Execute dumpbin.exe again with the /clrheader flag.

dumpbin /clrheader CarLibrary.dll

You are presented with the internal CLR header information for a given .NET assembly, as shown here:

Dump of file CarLibrary.dll File Type: DLL

clr Header:

48 cb
2.05 runtime version

2158 [ B7C] RVA [size] of MetaData Directory
1 flags
IL Only
0 entry point token
0 [ 0] RVA [size] of Resources Directory
0 [ 0] RVA [size] of StrongNameSignature Directory
0 [ 0] RVA [size] of CodeManagerTable Directory
0 [ 0] RVA [size] of VTableFixups Directory
0 [ 0] RVA [size] of ExportAddressTableJumps Directory
0 [ 0] RVA [size] of ManagedNativeHeader Directory Summary
2000 .reloc
2000 .rsrc
2000 .text

Again, as a .NET developer, you will not need to concern yourself with the gory details of an assembly’s CLR header information. Just understand that every .NET assembly contains this data, which is used behind the scenes by the .NET Runtime as the image data loads into memory. Now turn your attention to some information that is much more useful in your day-to-day programming tasks.

CIL Code, Type Metadata, and the Assembly Manifest
At its core, an assembly contains CIL code, which, as you recall, is a platform- and CPU-agnostic intermediate language. At runtime, the internal CIL is compiled on the fly using a just-in-time (JIT) compiler, according to platform- and CPU-specific instructions. Given this design, .NET assemblies can indeed execute on a variety of architectures, devices, and operating systems. (Although you can live a happy and productive life without understanding the details of the CIL programming language, Chapter 18 offers an introduction to the syntax and semantics of CIL.)
An assembly also contains metadata that completely describes the format of the contained types, as well as the format of external types referenced by this assembly. The .NET Runtime uses this metadata to resolve the location of types (and their members) within the binary, lay out types in memory, and facilitate remote method invocations. You’ll check out the details of the .NET metadata format in Chapter 17 during your examination of reflection services.
An assembly must also contain an associated manifest (also referred to as assembly metadata). The manifest documents each module within the assembly, establishes the version of the assembly, and documents any external assemblies referenced by the current assembly. As you will see over the course of this chapter, the CLR makes extensive use of an assembly’s manifest during the process of locating external assembly references.

Optional Assembly Resources
Finally, a .NET assembly may contain any number of embedded resources, such as application icons, image files, sound clips, or string tables. In fact, the .NET platform supports satellite assemblies that contain nothing but localized resources. This can be useful if you want to partition your resources based on a specific culture (English, German, etc.) for the purposes of building international software. The topic of building satellite assemblies is outside the scope of this text. Consult the .NET documentation for information on satellite assemblies and localization if you are interested.

Class Libraries vs. Console Applications
So far in this book, the examples were almost exclusively .NET Console applications. If you are reading this book as a current .NET Framework developer, these are like .NET console applications, with the main difference being the configuration process (to be covered later) and, of course, that they run on .NET Core. Console applications have a single-entry point (either a specified Main() method or top-level statements), can interact with the console, and can be launched directly from the operating system. Another difference
between .NET Framework and .NET console applications is that console applications in .NET are launched using the .NET Application Host (dotnet.exe).
Class libraries, on the other hand, don’t have an entry point and therefore cannot be launched directly. They are used to encapsulate logic, custom types, and so on, and they are referenced by other class libraries and/or console applications. In other words, class libraries are used to contain the things talked about in “The Role of .NET Assemblies” section.

.NET Standard vs. .NET (Core) Class Libraries
.NET (including .NET Core/.NET 5/.NET 6) class libraries run on .NET Core, and .NET Framework class libraries run on the .NET Framework. While it’s pretty straightforward, there is a problem with this. Assume you have a large .NET Framework code base in your organization, with (potentially) years of development behind you and your team. There is probably a significant amount of shared code leveraged by the applications you and your team have built over the years. Perhaps it’s centralized logging, reporting, or domain-specific functionality.
Now you (and your organization) want to move to the new .NET for all new development. What about all that shared code? The effort to rewrite all of your legacy code into .NET 6 assemblies could be significant, and until all of your applications were moved to .NET 6, you would have to support two versions (one in
.NET Framework and one in .NET 6). That would bring productivity to a screeching halt.
Fortunately, the builders of .NET (Core) thought through this scenario. .NET Standard is a new type of class library project that was introduced with .NET Core 1.0 and can be referenced by .NET Framework as well as .NET (Core) applications. Before you get your hopes up, though, there is a catch with .NET 5 and
.NET 6. More on that shortly.
Each .NET Standard version defines a common set of APIs that must be supported by all .NET versions (.NET, .NET Core, Xamarin, etc.) to conform to the standard. For example, if you were to build a class library as a .NET Standard 2.0 project, it can be referenced by .NET 4.61+ and .NET Core 2.0+ (plus various versions of Xamarin, Mono, Universal Windows Platform, and Unity).
This means you could move the code from your .NET Framework class libraries into .NET Standard 2.0 class libraries, and they can be shared by .NET (Core) and .NET Framework applications. That’s much better than supporting duplicate copies of the same code, one for each framework.
Now for the catch. Each .NET Standard version represents the lowest common denominator for the frameworks that it supports. That means the lower the version, the less that you can do in your class library. While .NET Framework and .NET 6 projects can reference a .NET Standard 2.0 library, you cannot use a significant number of C# 8.0 features in a .NET Standard 2.0 library, and you can’t use any new features from C# 9.0 or later. You must use .NET Standard 2.1 for full C# 8.0+ support. And .NET 4.8 (the latest/last version of the original .NET Framework) only goes up to .NET Standard 2.0.
It’s still a good mechanism for leveraging existing code in newer applications up to and including .NET Core 3.1, but not a silver bullet. With the unification of the frameworks (.NET, Xamarin, Mon, etc.) with .NET 6, the usefulness of .NET Standard is slowly fading into the past.

Configuring Applications with Configuration Files
While it is possible to keep all the information needed by your .NET application in the source code, being able to change certain values at runtime is vitally important in most applications of significance. One of the more common options is to use one or more configuration files shipped (or deployed) along with your application’s executable.
The .NET Framework relied mostly on XML files named app.config (or web.config for ASP.NET applications) for configuration. While XML-based configuration files can still be used, the most common method for configuring .NET applications is with JavaScript Object Notation (JSON) files..

■Note if you are not familiar with Json, it is a name-value pair format where each object is enclosed in curly braces. Values can also be objects using the same name-value pair format. Chapter 20 covers working with Json files in depth.

To illustrate the process, create a new .NET Console application named FunWithConfiguration and add the following package reference to your project:

dotnet new console -lang c# -n FunWithConfiguration -o .\FunWithConfiguration -f net6.0 dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration
dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration.Binder dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration.Json

This adds a reference for configuration subsystem, the JSON file–based .NET configuration subsystem, and the binding extensions for configuration into your project. Start by adding a new JSON file into your project named appsettings.json. Update the project file to make sure the file is always copied to the output directory when the project is built.



Always

Finally, update the appsettings.json file to match the following:

{
“CarName”: “Suzy”
}

The final step to adding the configuration into your app is to read in the configuration file and get the
CarName value. Update the Program.cs file to the following:
using Microsoft.Extensions.Configuration; IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(“appsettings.json”, true, true)
.Build();

The new configuration system starts with a ConfigurationBuilder. The path that the configuration build will start to look for the files being added is set with the SetBasePath() method. Then the configuration file is added with the AddJsonFile() method, which takes three parameters. The first parameter is the path and name of the file. Since this file is in the same location as the base path, there isn’t any path information in the string, just the filename. The second parameter sets whether the file is optional (true) or required (false), and the final parameter determines if the configuration should use a file watcher to look for any changes in the file (true) or to ignore any changes during runtime (false). The final step
is to build the configuration into an instance of IConfiguration using the Build() method. This instance provides access to all the configured values.

■ Note file watchers are covered in Chapter 20.

Once you have an instance of IConfiguration, you can get the values from the configuration files much the same way as calling the ConfigurationManager in .NET 4.8. Add the following to the bottom of the Main() method, and when you run the app, you will see the value written to the console:

Console.WriteLine($”My car’s name is {config[“CarName”]}”);

If the request name doesn’t exist in the configuration, the result will be null. The following code still runs without an exception; it just doesn’t display a name in the first line and displays True in the second line:

Console.WriteLine($”My car’s name is {config[“CarName2″]}”); Console.WriteLine($”CarName2 is null? {config[“CarName2″] == null}”);

There is also a GetValue() method (and its generic version GetValue()) that can retrieve primitive values from the configuration. The following example shows both of these methods to get the CarName:

Console.WriteLine($”My car’s name is {config.GetValue(typeof(string),”CarName”)}”); Console.WriteLine($”My car’s name is {config.GetValue(“CarName”)}”);

These methods return the default value (e.g., null for reference types, 0 for numeric types) if the name requested doesn’t exist. The following code returns 0 for the CarName2 property:

Console.WriteLine($”My car’s name is {config.GetValue(“CarName2″)}”);

These methods will throw an exception if the value found for the name can’t be intrinsically cast to the requested datatype. For example, trying to cast the CarName property to an int will throw an InvalidOperationException as shown by the following code:

try
{
Console.WriteLine($”My car’s name is {config.GetValue(“CarName”)}”);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($”An exception occurred: {ex.Message}”);
}

■ Note the GetValue() method is designed to work with primitive types. for complex types, use the
Bind() or Get()/Get() methods, covered in the “Working with objects” section.

Multiple Configuration Files
More than one configuration file can be added into the configuration system. When more than one file is used, the configuration properties are additive, unless any names of the name-value pairs conflict. When there is a name conflict, the last one in wins. To see this in action, add another file named appsettings. development.json, and set the project to always copy it to the output directory.



Always

Update the JSON to the following:

{
“CarName”: “Logan”
}

Now update the code that creates the instance of the IConfiguration interface to the following (update in bold):

IConfiguration config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(“appsettings.json”, true, true)
.AddJsonFile(“appsettings.development.json”, true, true)
.Build();

When you run this program, you will see the name of the car is indeed Logan, and not Suzy.

Working with Objects (Updated 10.0)
Our sample JSON file so far is extremely simple with a single name-value pair. In real-world projects, application configuration is usually more complex than a single property. Update the appsettings. development.json file to the following, which adds a new Car object to the JSON:

{
“CarName”: “Suzy”,
“Car”: {
“Make”:”Honda”,
“Color”: “Blue”, “PetName”:”Dad’s Taxi”
}
}

To access multilevel JSON values, the key used for searching is the hierarchy of the JSON, with each level separated by colons (:). For example, the key for the Car object’s Make property is Car:Make. Update the top-level statements to the following to get all the Car properties and display them:

Console.Write($”My car object is a {config[“Car:Color”]} “); Console.WriteLine($”{config[“Car:Make”]} named {config[“Car:PetName”]}”);

Instead of traversing the hierarchy of names, entire sections can be retrieved using the GetSection() method. Once you have the section, you can then get the values from the section using the simple name format, as shown in the following example:

IConfigurationSection section = config.GetSection(“Car”); Console.Write($”My car object is a {section[“Color”]} “); Console.WriteLine($”{section[“Make”]} named {section[“PetName”]}”);

As a final note for working with objects, you can use the Bind() method to bind configuration values to an existing instance of an object or the Get() method to create a new object instance. These are similar to the GetValue() method but work with nonprimitive types. To get started, create a simple Car class at the end of the top-level statements:

public class Car
{
public string Make {get;set;} public string Color {get;set;} public string PetName { get; set; }
}

Next, create a new instance of a Car class, and then call Bind() on the section passing in the Car instance:

var c = new Car(); section.Bind(c);
Console.Write($”My car object is a {c.Color} “); Console.WriteLine($”{c.Make} named {c.PetName}”);

If the section isn’t configured, the Bind() method will not update the instance but will leave all of the properties as they existed prior to the call to Bind(). The following will leave the Color set to Red and the remaining properties null:

var notFoundCar = new Car { Color = “Red”}; config.GetSection(“Car2”).Bind(notFoundCar); Console.Write($”My car object is a {notFoundCar.Color} “);
Console.WriteLine($”{notFoundCar.Make} named {notFoundCar.PetName}”);

The Get() method creates a new instance of the specified type from a section of the configuration. The non-generic version of the method returns an object type, so the return value must be cast to the specific type before being used. Here is an example using the Get() method to create an instance of the Car class from the Car section of the configuration:

var carFromGet = config.GetSection(nameof(Car)).Get(typeof(Car)) as Car; Console.Write($”My car object (using Get()) is a {carFromGet.Color} “); Console.WriteLine($”{carFromGet.Make} named {carFromGet.PetName}”);

If the named section isn’t found, the Get() method returns null:

var notFoundCarFromGet = config.GetSection(“Car2″).Get(typeof(Car)); Console.WriteLine($”The not found car is null? {notFoundCarFromGet == null}”);

The generic version returns an instance of the specified type without having to perform a cast. If the section isn’t found, the method returns null.

//Returns a Car instance
var carFromGet2 = config.GetSection(nameof(Car)).Get(); Console.Write($”My car object (using Get()) is a {carFromGet.Color} “); Console.WriteLine($”{carFromGet.Make} named {carFromGet.PetName}”);

//Returns null
var notFoundCarFromGet2 = config.GetSection(“Car2”).Get(); Console.WriteLine($”The not found car is null? {notFoundCarFromGet2 == null}”);

The Bind() and Get()/Get() methods use reflection (covered in the next chapter) to match the names of the public properties on the class to the names in the configuration section in a case-insensitive manner. For example, if you update appsettings.development.json to the following (notice the casing change of the petName property), the previous code will still work:

{
“CarName”: “Suzy”, “Car”: {
“Make”:”Honda”,
“Color”: “Blue”,
“petName”:”Dad’s Taxi”
}
}

If a property in the configuration doesn’t exist in the class (or the name is spelled differently), then that particular configuration value (by default) is ignored. If you update the JSON to the following, the Make and Color properties are populated, but the PetName property on the Car object isn’t:

{
“CarName”: “Suzy”, “Car”: {
“Make”:”Honda”,
“Color”: “Blue”,
“PetNameForCar”:”Dad’s Taxi”
}
}

The Bind(), Get() and Get() methods can optionally take an Action to further refine the process of updating (Bind()) or instantiating (Get()/Get()) class instances. The BinderOptions class is listed here:

public class BinderOptions
{
public bool BindNonPublicProperties { get; set; } //Defaults to false public bool ErrorOnUnknownConfiguration { get; set; } //Defaults to false
}

If the ErrorOnUnknownConfiguration option is set to true, then an InvalidOperationException will be thrown if the configuration contains a name that doesn’t exist on the model. With the renamed configuration value (PetNameForCar), the following call throws the exception listed in the code sample:

try
{
_ = config.GetSection(nameof(Car)).Get(t=>t.ErrorOnUnknownConfiguration=true);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($”An exception occurred: {ex.Message}”);
}
/*
Error message: ‘ErrorOnUnknownConfiguration’ was set on the provided BinderOptions, but the following properties were not found on the instance of Car: ‘PetNameForCar’
*/

The other option allows for binding non-public properties. By default, both properties are false. If non- public properties should be bound from the configuration, set the BindNonPublicProperties, like this:

var carFromGet3 = config.GetSection(nameof(Car)).Get(t=>t.BindNonPublicPropert ies=true);

New in C# 10, the GetRequiredSection() method will throw an exception if the section isn’t configured. For example, the following code will throw an exception since there isn’t a Car2 section in the configuration:

try
{
config.GetRequiredSection(“Car2″).Bind(notFoundCar);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($”An exception occurred: {ex.Message}”);
}

Additional Configuration Options
In addition to using file-based configuration, there are options to use environment variables, Azure Key Vault, command-line arguments, and many more. Many of these are used intrinsically in ASP.NET Core, as you will see later in this book. You can also reference the .NET documentation for more information on using other methods for configuring applications.

Building and Consuming a .NET Class Library
To begin exploring the world of .NET class libraries, you’ll first create a *.dll assembly (named CarLibrary) that contains a small set of public types. If you are working in Visual Studio, name the solution file something meaningful (different than CarLibrary). You will add two projects into the solution later in this section. If you are working in Visual Studio Code, you don’t need to have a solution, although most developers find it useful to group related projects into a single solution.
As a reminder, you can create and manage solutions and projects through the .NET Command Line Interface (CLI). Use the following command to create the solution and class library:

dotnet new sln -n Chapter16_AllProjects
dotnet new classlib -lang c# -n CarLibrary -o .\CarLibrary -f net6.0 dotnet sln .\Chapter16_AllProjects.sln add .\CarLibrary

The first command creates an empty solution file named Chapter16_AllProjects (-n) in the current directory. The next command creates a new .NET 6.0 (-f) class library named CarLibrary (-n) in the subdirectory named CarLibrary (-o). The output (-o) location is optional. If left off, the project will be created in a subdirectory with the same name as the project name. The final command adds the new project to the solution.

■ Note the .net Cli has a good help system. to get help for any command, add -h to the command. for example, to see all templates, type dotnet new -h. to get more information about creating a class library, type dotnet new classlib -h.

Now that you have created your project and solution, you can open it in Visual Studio (or Visual Studio Code) to begin building the classes. After opening the solution, delete the autogenerated file Class1.cs.
The design of your automobile library begins with the EngineStateEnum and MusicMediaEnum enums. Add two files to your project named MusicMediaEnum.cs and EngineStateEnum.cs and add the following code to each file, respectively:

//MusicMediaEnum.cs namespace CarLibrary;
// Which type of music player does this car have? public enum MusicMediaEnum
{
MusicCd, MusicTape, MusicRadio, MusicMp3
}
//EngineStateEnum.cs namespace CarLibrary;
// Represents the state of the engine. public enum EngineStateEnum
{
EngineAlive, EngineDead
}

Next, insert a new C# class file into your project, named Car.cs, that will hold an abstract base class named Car. This class defines various state data via automatic property syntax. This class also has a single abstract method named TurboBoost(), which uses the EngineStateEnum enumeration to represent the current condition of the car’s engine. Update the file to the following code:

namespace CarLibrary;
// The abstract base class in the hierarchy. public abstract class Car
{
public string PetName {get; set;} public int CurrentSpeed {get; set;} public int MaxSpeed {get; set;}

protected EngineStateEnum State = EngineStateEnum.EngineAlive; public EngineStateEnum EngineState => State;
public abstract void TurboBoost();

protected Car(){}
protected Car(string name, int maxSpeed, int currentSpeed)
{
PetName = name;
MaxSpeed = maxSpeed;
CurrentSpeed = currentSpeed;
}
}

Now assume you have two direct descendants of the Car type named MiniVan and SportsCar. Each overrides the abstract TurboBoost() method by displaying an appropriate message via console message. Insert two new C# class files into your project, named MiniVan.cs and SportsCar.cs, respectively. Update the code in each file with the relevant code.

//SportsCar.cs namespace CarLibrary;
public class SportsCar : Car
{
public SportsCar(){ } public SportsCar(
string name, int maxSpeed, int currentSpeed)
: base (name, maxSpeed, currentSpeed){ }

public override void TurboBoost()
{
Console.WriteLine(“Ramming speed! Faster is better…”);
}
}

//MiniVan.cs namespace CarLibrary;
public class MiniVan : Car
{
public MiniVan(){ }

public MiniVan(
string name, int maxSpeed, int currentSpeed)
: base (name, maxSpeed, currentSpeed){ }

public override void TurboBoost()
{
// Minivans have poor turbo capabilities! State = EngineStateEnum.EngineDead;
Console.WriteLine(“Eek! Your engine block exploded!”);
}
}

Exploring the Manifest
Before using CarLibrary.dll from a client application, let’s check out how the code library is composed under the hood. Assuming you have compiled this project, run ildasm.exe against the compiled assembly. If you don’t have ildasm.exe (covered earlier in this book), it is also located in the Chapter 16 directory of this book’s repository.

ildasm /METADATA /out=CarLibrary.il .\CarLibrary\bin\Debug\net6.0\CarLibrary.dll

The Manifest section of the disassembled results starts with //Metadata version: 4.0.30319. Immediately following is the list of all external assemblies required by the class library, as shown here:

// Metadata version: v4.0.30319
.assembly extern System.Runtime
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 6:0:0:0
}
.assembly extern System.Console
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 6:0:0:0
}

Each .assembly extern block is qualified by the .publickeytoken and .ver directives. The
.publickeytoken instruction is present only if the assembly has been configured with a strong name. The
.ver token defines the numerical version identifier of the referenced assembly.

■ Note prior versions of the .net framework relied heavily on strong naming, which involved using a public/ private key combination. this was required on Windows for an assembly to be added into the global assembly Cache, but its need has severely diminished with the advent of .net Core.

After the external references, you will find a number of .custom tokens that identify assembly-level attributes (some system generated, but also copyright information, company name, assembly version, etc.). Here is a (very) partial listing of this chunk of manifest data:

.assembly CarLibrary
{

.custom instance void … TargetFrameworkAttribute …
.custom instance void … AssemblyCompanyAttribute …
.custom instance void … AssemblyConfigurationAttribute …
.custom instance void … AssemblyFileVersionAttribute …
.custom instance void … AssemblyProductAttribute …
.custom instance void … AssemblyTitleAttribute …

These settings can be set either using the Visual Studio property pages or editing the project file and adding in the correct elements. To edit the package properties in Visual Studio 2022, right-click the project in Solution Explorer, select Properties, and navigate to the Package menu in the left rail of the window. This brings up the dialog shown in Figure 16-4. For the sake of brevity, Figure 16-4 is only part of the dialog.

Figure 16-4. Editing assembly information using Visual Studio 2022’s Properties window

■ Note there are three different version fields in the package screen. the assembly version and the file version use the same schema, which is based on semantic versioning (https://semver.org). the first number is the major build version, the second is the minor build version, and the third is the patch number. the fourth number is usually used to indicate a build number. the package version number should adhere to semantic versioning, with just the {major}.{minor}.{patch} placeholders used. semantic versioning allows for an alphanumeric extension to the version, which is separated by a dash instead of a period (e.g., 1.0.0-rc). this denotes noncomplete versions, such as betas and release candidates.

a package version sets the nuget package version (nuget packaging is covered in more detail later in this chapter). the assembly version is used by .net during build and runtime to locate, link, and load assemblies. the file version is used only by Windows explorer, and it not used by .net.

Another way to add the metadata to your assembly is directly in the *.csproj project file. The following update to the main PropertyGroup in the project file does the same thing as filling in the form shown in Figure 16-4. Notice that the package version is simply called Version in the project file.


net6.0
enable
disable
Copyright 2021
Phil Japikse
Apress
Pro C# 10.0
CarLibrary
This is an awesome library for cars.
1.0.0.1
1.0.0.2
1.0.3

■ Note the rest of the entries from figure 16-4 (and the project file listing) are used when generating nuget packages from your assembly. this is covered later in the chapter.

Exploring the CIL
Recall that an assembly does not contain platform-specific instructions; rather, it contains platform-agnostic Common Intermediate Language (CIL) instructions. When the .NET Runtime loads an assembly into memory, the underlying CIL is compiled (using the JIT compiler) into instructions that can be understood by the target platform. For example, the TurboBoost() method of the SportsCar class is represented by the following CIL:

.method public hidebysig virtual
instance void TurboBoost() cil managed
{
.maxstack 8 IL_0000: nop
IL_0001: ldstr “Ramming speed! Faster is better…”
IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop
IL_000c: ret
}
// end of method SportsCar::TurboBoost

As with the other CIL examples in this book, most .NET developers don’t need to be deeply concerned with the details. Chapter 18 provides more details on its syntax and semantics, which can be helpful when you are building more complex applications that require advanced services, such as runtime construction of assemblies.

Exploring the Type Metadata
Before you build some applications that use your custom .NET library, examine the metadata for the types within the CarLibrary.dll assembly. For an example, here is the TypeDef for the EngineStateEnum:

TypeDef #2

TypDefName: CarLibrary.EngineStateEnum
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] Extends : [TypeRef] System.Enum
Field #1

Field Name: value
Flags : [Public] [SpecialName] [RTSpecialName] CallCnvntn: [FIELD]
Field type: I4 Field #2
Field Name: EngineAlive
Flags : [Public] [Static] [Literal] [HasDefault] DefltValue: (I4) 0
CallCnvntn: [FIELD]
Field type: ValueClass CarLibrary.EngineStateEnum Field #3
Field Name: EngineDead
Flags : [Public] [Static] [Literal] [HasDefault] DefltValue: (I4) 1
CallCnvntn: [FIELD]
Field type: ValueClass CarLibrary.EngineStateEnum

As explained in the next chapter, an assembly’s metadata is an important element of the .NET platform and serves as the backbone for numerous technologies (object serialization, late binding, extendable applications, etc.). In any case, now that you have looked inside the CarLibrary.dll assembly, you can build some client applications that use your types.

Building a C# Client Application
Because each of the CarLibrary project types has been declared using the public keyword, other .NET applications are able to use them as well. Recall that you may also define types using the C# internal keyword (in fact, this is the default C# access mode for classes). Internal types can be used only by the assembly in which they are defined. External clients can neither see nor create types marked with the internal keyword.

■ Note the exception to the internal rule is when an assembly explicitly allows access to another assembly using the InternalsVisibleTo attribute, covered shortly.

To use your library’s functionality, create a new C# Console Application project named CSharpCarClient in the same solution as CarLibrary. You can do this using Visual Studio (right-click the solution and select Add ➤ New Project) or using the command line (three lines, each executed separately).

dotnet new console -lang c# -n CSharpCarClient -o .\CSharpCarClient -f net6.0 dotnet add CSharpCarClient reference CarLibrary
dotnet sln .\Chapter16_AppRojects.sln add .\CSharpCarClient

The previous commands created the console application, added a project reference to the CarLibrary project for the new project, and added it to your solution.

■ Note the add reference command creates a project reference. this is convenient for development, as CsharpCarClient will always be using the latest version of Carlibrary. You can also reference an assembly directly. direct references are created by referencing the compiled class library.

If you still have the solution open in Visual Studio, you’ll notice that the new project shows up in Solution Explorer without any intervention on your part.
The final change to make is to right-click CSharpCarClient in Solution Explorer and select “Set as Startup Project.” If you are not using Visual Studio, you can run the new project by executing dotnet run in the project directory.

■ Note You can set the project reference in Visual studio as well by right-clicking the CsharpCarClient project in solution explorer, selecting add ➤ reference, and selecting the Carlibrary project from the project’s node.

At this point, you can build your client application to use the external types. Update the Program.cs file as follows:

// Don’t forget to import the CarLibrary namespace! using CarLibrary;

Console.WriteLine(“***** C# CarLibrary Client App *****”);
// Make a sports car.
SportsCar viper = new SportsCar(“Viper”, 240, 40); viper.TurboBoost();

// Make a minivan.
MiniVan mv = new MiniVan(); mv.TurboBoost();

Console.WriteLine(“Done. Press any key to terminate”); Console.ReadLine();

This code looks just like the code of the other applications developed thus far in the book. The only point of interest is that the C# client application is now using types defined within a separate custom library. Run your program and verify that you see the display of messages.
You might be wondering exactly what happened when you referenced the CarLibrary project. When a project reference is made, the solution build order is adjusted so that dependent projects (CarLibrary in this example) build first, and then the output from that build is copied into the output directory of the parent project (CSharpCarLibrary). The compiled client library references the compiled class library. When the client project is rebuilt, so is the dependent library, and the new version is once again copied to the target folder.

■ Note if you are using Visual studio, you can click the show all files button in solution explorer, and you can see all the output files and verify that the compiled Carlibrary is there. if you are using Visual studio Code, navigate to the bin/debug/net6.0 directory in the explorer tab.

When a direct reference instead of a project reference is made, the compiled library is also copied to the output directory of the client library, but at the time the reference is made. Without the project reference in place, the projects can be built independently of each other, and the files could become out of sync. In short, if you are developing dependent libraries (as is usually the case with real software projects), it is best to reference the project and not the project output.

Building a Visual Basic Client Application
Recall that the .NET platform allows developers to share compiled code across programming languages. To illustrate the language-agnostic attitude of the .NET platform, let’s create another Console Application project (VisualBasicCarClient), this time using Visual Basic (note that each of commands is a one-line command).

dotnet new console -lang vb -n VisualBasicCarClient -o .\VisualBasicCarClient -f net6.0 dotnet add VisualBasicCarClient reference CarLibrary
dotnet sln .\Chapter16_AllProjects.sln add VisualBasicCarClient

Like C#, Visual Basic allows you to list each namespace used within the current file. However, Visual Basic uses the Imports keyword rather than the C# using keyword, so add the following Imports statement within the Program.vb code file:

Imports CarLibrary
Module Program Sub Main() End Sub
End Module

Notice that the Main() method is defined within a Visual Basic module type. In a nutshell, modules are a Visual Basic notation for defining a class that can contain only static methods (much like a C# static
class). In any case, to exercise the MiniVan and SportsCar types using the syntax of Visual Basic, update your
Main() method as follows:

Sub Main()
Console.WriteLine(“***** VB CarLibrary Client App *****”)
‘ Local variables are declared using the Dim keyword.

Dim myMiniVan As New MiniVan() myMiniVan.TurboBoost()

Dim mySportsCar As New SportsCar() mySportsCar.TurboBoost() Console.ReadLine()
End Sub

When you compile and run your application (make sure to set VisualBasicCarClient as the startup project if you are using Visual Studio), you will once again find a series of message boxes displayed.
Furthermore, this new client application has its own local copy of CarLibrary.dll located under the bin\ Debug\net6.0 folder.

Cross-Language Inheritance in Action
An enticing aspect of .NET development is the notion of cross-language inheritance. To illustrate, let’s create a new Visual Basic class that derives from SportsCar (which was authored using C#). First, add a new class file named PerformanceCar.vb to your current Visual Basic application. Update the initial class definition by deriving from the SportsCar type using the Inherits keyword. Then, override the abstract TurboBoost() method using the Overrides keyword, like so:

Imports CarLibrary
‘ This VB class is deriving from the C# SportsCar.
Public Class PerformanceCar Inherits SportsCar
Public Overrides Sub TurboBoost()
Console.WriteLine(“Zero to 60 in a cool 4.8 seconds…”) End Sub
End Class

To test this new class type, update the module’s Main() method as follows:

Sub Main()

Dim dreamCar As New PerformanceCar()

‘ Use Inherited property. dreamCar.PetName = “Hank” dreamCar.TurboBoost() Console.ReadLine()
End Sub

Notice that the dreamCar object can invoke any public member (such as the PetName property) found up the chain of inheritance, although the base class was defined in a completely different language and in a completely different assembly! The ability to extend classes across assembly boundaries in a language- independent manner is a natural aspect of the .NET development cycle. This makes it easy to use compiled code written by individuals who would rather not build their shared code with C#.

Exposing internal Types to Other Assemblies
As mentioned earlier, internal classes are visible only to other objects in the assembly where they are defined. The exception to this is when visibility is explicitly granted to another project.
Begin by adding a new class named MyInternalClass to the CarLibrary project, and update the code to the following:

namespace CarLibrary;
internal class MyInternalClass
{
}

■ Note Why expose internal types at all? this is usually done for unit and integration testing. developers want to be able to test their code but not necessarily expose it beyond the borders of the assembly.

Using an Assembly Attribute
Chapter 17 will cover attributes in depth, but for now open the Car.cs class in the CarLibrary project, and add the following attribute and using statement:

using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo(“CSharpCarClient”)] namespace CarLibrary;

The InternalsVisibleTo attribute takes the name of the project that can see into the class that has the attribute set. Note that other projects cannot “ask” for this permission; it must be granted by the project holding the internal types.

■ Note previous versions of the .net framework allowed you to place assembly-level attributes into the AssemblyInfo.cs class, which still exists in .net but is autogenerated and not meant for developer consumption.

Now you can update the CSharpCarClient project by adding the following code to the top-level statements:

var internalClassInstance = new MyInternalClass();

This works perfectly. Now try to do the same thing in the VisualBasicCarClient Main method.

‘Will not compile
‘Dim internalClassInstance = New MyInternalClass()

Because the VisualBasicCarClient library was not granted permission to see the internals, the previous line of code will not compile.

Using the Project File
Another way to accomplish the same thing is to use the updated capabilities in the .NET project file. Comment out the attribute you just added and open the project file for CarLibrary. Add the following ItemGroup in the project file:



<_Parameter1>CSharpCarClient

This accomplishes the same thing as using the attribute on a class and, in my opinion, is a better solution, since other developers will see it right in the project file instead of having to know where to look throughout the project.

NuGet and .NET Core
NuGet is the package manager for the .NET Framework and .NET (Core). It is a mechanism to share software in a format that .NET applications understand and is the default mechanism for loading .NET and its related framework pieces (ASP.NET Core, EF Core, etc.). Many organizations package their standard assemblies for cross-cutting concerns (like logging and error reporting) into NuGet packages for consumption into their line-of-business applications.

Packaging Assemblies with NuGet
To see this in action, we will turn the CarLibrary into a NuGet package and then reference it from the two client applications.
The NuGet Package properties can be accessed from the project’s property pages. Right-click the CarLibrary project and select Properties. Navigate to the Package page and see the values that we entered before to customize the assembly. There are additional properties that can be set for the NuGet package (i.e., license agreement acceptance and project information such as URL and repository location).

■ Note all of the values in the Visual studio package page ui can be entered into the project file manually, but you need to know the keywords. it helps to use Visual studio at least once to fill everything out, and then you can edit the project file by hand. You can also find all the allowable properties in the .net documentation.

For this example, we don’t need to set any additional properties except to select the “Generate NuGet package on build” check box or update the project file with the following:


net6.0
enable
disable
Copyright 2021
Phil Japikse
Apress

Pro C# 10.0
CarLibrary
This is an awesome library for cars.
1.0.0.1
1.0.0.2
1.0.0.3
true

This will cause the package to be rebuilt every time the software is built. By default, the package will be created in the bin/Debug or bin/Release folder, depending on which configuration is selected.
Packages can also be created from the command line, and the CLI provides more options than Visual Studio. For example, to build the package and place it in a directory called Publish, enter the following commands (in the CarLibrary project directory). The first command builds the assembly, and the second packages up the NuGet package.

dotnet build -c Release
dotnet pack -o .\Publish -c Release

The CarLibrary.1.0.3.nupkg file is now in the Publish directory. To see its contents, open the file with any zip utility (such as 7-Zip), and you can see the entire content, which includes the assembly, but also additional metadata.

Referencing NuGet Packages
You might be wondering where the packages that were added in the previous examples came from. The location of NuGet packages is controlled by an XML-based file named NuGet.Config. On Windows, this file is in the %appdata%\NuGet directory. This is the main file. Open it, and you will see several package sources.



The previous file listing shows two sources. The first points to NuGet.org, which is the largest NuGet package repository in the world. The second is on your local drive and is used by Visual Studio as a cache of packages.
The important item to note is that NuGet.Config files are additive by default. To add additional sources without changing the list for the entire system, you can add additional NuGet.Config files. Each file is valid for the directory that it’s placed in as well as any subdirectory. Add a new file named NuGet.Config into the solution directory, and update the contents to this:


You can also reset the list of packages by adding into the node, like this:




■ Note if you are using Visual studio, you will have to restart the ide before the updated nuget.config
settings take effect.

Remove the project references from the CSharpCarClient and VisualBasicCarClient projects, and then add package references like this (from the solution directory):

dotnet add CSharpCarClient package CarLibrary dotnet add VisualBasicCarClient package CarLibrary

Once the references are set, build the solution and look at the output in the target directories (bin\ Debug\new6.0), and you will see the CarLibrary.dll in the directory and not the CarLibrary.nupkg file. This is because .NET unpacks the contents and adds in the assemblies contained as direct references.
Now set one of the clients as the startup project and run the application, and it works exactly like before. Next, update the version number of CarLibrary to 1.0.0.4, and repackage it. In the Publish directory,
there are now two CarLibrary NuGet packages. If you rerun the add package commands, the project will be updated to use the new version. If the older version was preferred, the add package command allows for adding version numbers for a specific package.

Publishing Console Applications (Updated .NET 5/6)
Now that you have your C# CarClient application (and its related CarLibrary assembly), how do you get it out to your users? Packaging up your application and its related dependencies is referred to as publishing. Publishing .NET Framework applications required the framework to be installed on the target machine, and then it was a simple copy of the application executable and related files to run your application on another machine.
As you might expect, .NET applications can also be published in a similar manner, which is referred to as a framework-dependent deployment. However, .NET applications can also be published as a self- contained application, which doesn’t require .NET to be installed at all!
When publishing applications as self-contained, you must specify the target runtime identifier (RID) in the project file or through the command-line options. The runtime identifier is used to package your application for a specific operating system. When a runtime identifier is specified, the publish process defaults to self-contained. For a full list of available runtime identifiers (RIDs), see the .NET RID Catalog at https://docs.microsoft.com/en-us/dotnet/core/rid-catalog.
Applications can be published directly from Visual Studio or by using the .NET CLI. The command for the CLI is dotnet publish. To see all the options, use dotnet publish -h. Table 16-1 explores the common options used when publishing from the command line.

Table 16-1. Some Options for Application Publishing

Option Meaning in Life
–use-current-runtime Use current runtime as the target runtime.
-o, –output The output directory to place the published artifacts in.
–self-contained Publish the .NET Runtime with your application so the runtime doesn’t need to be installed on the target machine. The default is true if a runtime identifier is specified.
–no-self-contained Publish your application as a framework-dependent application without the .NET Runtime. A supported .NET Runtime must be installed to run your application.
-r –runtime
The target runtime to publish for. This is used when creating a self-contained deployment. The default is to publish a framework-dependent application.
-c debug | release–configuration debug | release The configuration to publish for. The default for most projects is debug.
-v, –verbosity Set the MSBuild verbosity level. Allowed values are q/quiet, m/minimal, n/normal, d/detailed, and diag/diagnostic.

Publishing Framework-Dependent Applications
When a runtime identifier isn’t specified, framework-dependent deployment is the default for the dotnet publish command. To package your application and the required files, all you need to execute with the CLI is the following command:

dotnet publish

■ Note the publish command uses the default configuration for your project, which is typically debug.

This places your application and its supporting files (six files in total) into the bin\Debug\net6.0\ publish directory. Examining the files that were added to that directory, you see two *.dll files (CarLibrary.dll and CSharpCarClient.dll) that contain all the application code. As a reminder,
the CSharpCarClient.exe file is a packaged version of dotnet.exe that is configured to launch CSharpCarClient.dll. The CSharpCarClient.deps.json file lists all the dependencies for the application, and the CSharpCarClient.runtimeconfig.json file specifies the target framework (net6.0) and the framework version. The last file is the debug file for the CSharpCarClient.
To create a release version (which will be placed in the bin\release\net6.0\publish directory), enter the following command:

dotnet publish -c release

Publishing Self-Contained Applications
Like framework-dependent deploys, self-contained deployments include all your application code and referenced assemblies, but also include the .NET Runtime files required by your application. To publish

your application as a self-contained deployment, you must include a runtime identifier in the command (or in your project file). Enter the following CLI command that places the output into a folder named selfcontained. If you are on a Mac or Linux machine, select osx-x64 or linux-x64 instead of win-x64.

dotnet publish -r win-x64 -c release -o selfcontained

■ Note new in .net 6, the –self-contained true option is no longer necessary. if a runtime identifier is specified, the publish command will use the self-contained process.

This places your application and its supporting files (226 files in total) into the selfcontained directory. If you copied these files to another computer that matches the runtime identifier, you can run the application even if the .NET 6 Runtime isn’t installed.

Publishing Self-Contained Applications as a Single File
In most situations, deploying 226 files (for an application that prints a few lines of text) is probably not the most effective way to get your application out to users. Fortunately, .NET 5 greatly improved the ability to publish your application and the cross-platform runtime files into a single file. This process is improved again with .NET 6, eliminating the need to have the native libraries exist outside of the single EXE, which was necessary in .NET 5 for Windows publishing.
The following command creates a single-file, self-contained deployment package for 64-bit Windows operating systems and places the resulting files in a folder named singlefile.

dotnet publish -r win-x64 -c release -o singlefile –self-contained true -p:PublishSingleFile=true

When you examine files that were created, you will find a single executable (CSharpCarClient.exe) and a debug file (CSharpCarClient.pdb). While the previous publish process produced a large number of smaller files, the single file version of CSharpCarClient.exe clocks in at 60 MB! Creating the single file
publication packed all of the 226 files into the new single file. What was made up for in file count reduction was traded for in file size.
To include the debug symbols file in the single file (to truly make it a single file), update your command to the following:

dotnet publish -r win-x64 -c release -o singlefile -p:PublishSingleFile= true -p:DebugType=embedded

Now you have one file that contains everything, but it’s still quite large. One option that might help with file size is compression. The output can be compressed to save space, but this will most likely affect the startup time of your application even more. To enable compression, use the following command (all on one line):

dotnet publish -c release -r win-x64 -o singlefilecompressed -p:PublishSingleFile= true -p:DebugType=embedded -p:EnableCompressionInSingleFile=true

The .NET team has been working on file trimming during the publication process for the past few releases, and with .NET 6, it’s out of preview and ready to be used. The file trimming process determines what can be removed from the runtime based on what your application uses. Part of the reason this process is now live is because the team has been annotating the runtime itself to remove the false warnings that

were prevalent in .NET 5. New in .NET 6, the trimming process doesn’t just look for assemblies that can be removed; it also looks for unused members. Use the following command to trim the single file output (all on one line):

dotnet publish -c release -r win-x64 -o singlefilecompressedandtrimmed -p: PublishSingleFile=true -p:DebugType=embedded -p:EnableCompressionInSingleFile= true -p:PublishTrimmed=true

The final step in this journey is to publish your app in a ready-to-run state. This can improve startup time since some of the JIT compilation is done ahead of time (AOT), during the publish process.

dotnet publish -c release -r win-x64 -o singlefilefinal -p:PublishSingleFile=
true -p:DebugType=embedded -p:EnableCompressionInSingleFile=true -p:PublishTrimmed= true -p:PublishReadyToRun=true

The final size of our application is 11 MB, far less than the 60 MB that we started with.
As a final note, all of these settings can be configured in the project file for your application, like this:


Exe
net6.0
enable
disable
true
true
win-x64
true
embedded
true
true

With those values set in the project file, the command line becomes much shorter:

dotnet publish -c release -o singlefilefinal2

How .NET Locates Assemblies
So far in this book, all the assemblies that you have built were directly related (except for the NuGet example you just completed). You added either a project reference or a direct reference between projects. In these cases (as well as the NuGet example), the dependent assembly was copied directly into the target directory of the client application. Locating the dependent assembly isn’t an issue, since they reside on the disk right next to the application that needs them.
But what about the .NET Framework? How are those located? Previous versions of .NET installed the framework files into the Global Assembly Cache (GAC), and all .NET Framework applications knew how to locate the framework files.
However, the GAC prevents the side-by-side capabilities in .NET Core, so there isn’t a single repository of runtime and framework files. Instead, the files that make up the framework are installed together in C:\ Program Files\dotnet (on Windows), separated by version. Based on the version of the application (as specified in the .csproj file), the necessary runtime and framework files are loaded for an application from the specified version’s directory.

Specifically, when a version of the runtime is started, the runtime host provides a set of probing paths that it will use to find an application’s dependencies. There are five probing properties (each of them optional), as listed in Table 16-2.

Table 16-2. Application Probing Properties

Option Meaning in Life
TRUSTED_PLATFORM_ASSEMBLIES List of platform and application assembly file paths
PLATFORM_RESOURCE_ROOTS List of directory paths to search for satellite resource assemblies
NATIVE_DLL_SEARCH_DIRECTORIES List of directory paths to search for unmanaged (native) libraries
APP_PATHS List of directory paths to search for managed assemblies
APP_NI_PATHS List of directory paths to search for native images of managed assemblies

To see the default paths for these, create a new .NET Console application named FunWithProbingPaths.
Update the Program.cs file to the following top-level statements:

Console.WriteLine(“*** Fun with Probing Paths ***”); Console.WriteLine($”TRUSTED_PLATFORM_ASSEMBLIES: “);
//Use ‘:’ on non-Windows platforms
var list = AppContext.GetData(“TRUSTED_PLATFORM_ASSEMBLIES”)
.ToString().Split(‘;’); foreach (var dir in list)
{
Console.WriteLine(dir);
}
Console.WriteLine();
Console.WriteLine($”PLATFORM_RESOURCE_ROOTS: {AppContext.GetData (“PLATFORM_RESOURCE_ROOTS”)}”); Console.WriteLine();
Console.WriteLine($”NATIVE_DLL_SEARCH_DIRECTORIES: {AppContext.GetData (“NATIVE_DLL_SEARCH_ DIRECTORIES”)}”);
Console.WriteLine();
Console.WriteLine($”APP_PATHS: {AppContext.GetData(“APP_PATHS”)}”); Console.WriteLine();
Console.WriteLine($”APP_NI_PATHS: {AppContext.GetData(“APP_NI_PATHS”)}”); Console.WriteLine();
Console.ReadLine();

When you run this app, you will see most of the values come from the TRUSTED_PLATFORM_ASSEMBLIES variable. In addition to the assembly that is created for this project in the target directory, you will see a list of base class libraries from the current runtime directory, C:\Program Files\dotnet\shared\Microsoft. NETCore.App\6.0.0 (your version number might be different).
Each of the files directly referenced by your application is added to the list as well as any runtime files that are required for your application. The list of runtime libraries is populated by one or more *.deps.json files that are loaded with the .NET Runtime. There are several in the installation directory for the SDK (used for building the software) and the runtime (used for running the software). With our simple example, the only file used is Microsoft.NETCore.App.deps.json.
As your application grows in complexity, so will the list of files in TRUSTED_PLATFORM_ASSEMBLIES. For example, if you add a reference to the Microsoft.EntityFrameworkCore package, the list of required

assemblies grows. To demonstrate this, enter the following command in Package Manager Console (in the same directory as the *.csproj file):

dotnet add package Microsoft.EntityFrameworkCore

Once you have added the package, rerun the application, and notice how many more files are listed. Even though you added only one new reference, the Microsoft.EntityFrameworkCore package has its dependencies, which get added into the trusted file list.

Summary
This chapter examined the role of .NET class libraries (aka .NET *.dlls). As you have seen, class libraries are
.NET binaries that contain logic intended to be reused across a variety of projects.
You learned the details of partitioning types into .NET namespaces and the difference between a .NET and .NET Standard, got started with application configuration, and dove deep into the composition of class libraries. Next you learned how to publish .NET console applications. Finally, you learned how to package your applications using NuGet.

发表评论