Guest David Pine Posted June 3, 2024 Posted June 3, 2024 This post is the third in a series of four posts, exploring various C# 12 features. In this post, we’ll dive into the “alias any type” feature, which allows you to create an alias for any type with the [iCODE]using[/iCODE] directive. This series is really starting to shape up nicely: Refactor your C# code with primary constructors Refactor your C# code with collection expressions Refactor your C# code by aliasing any type (this post) Refactor your C# code to use default lambda parameters All of these features continue our journey to make our code more readable and maintainable, and these are considered “Everyday C#” features that developers should know. Let’s dive in! [HEADING=1]Alias Any Type *⃣[/HEADING] C# 12 introduced the ability to alias any type with the [iCODE]using[/iCODE] directive. This feature allows you to specify aliases that map to other types. This includes tuple types, pointer types, array types, and even non-open generic types, all of which are then available in your code. This feature is especially useful: When working with long or complex type names. When you need to disambiguate types and resolve potential naming conflicts. When defining value tuple types that you intend to share in an assembly. When you want to add clarity to your code by using more descriptive names. The official C# documentation is great at providing many examples of how to use this feature, but rather than repeating those samples here, I decided to write a demo app that exemplifies various aspects of the feature. Nullable reference types This feature supports most types, with the single exception of nullable reference types. That is, you cannot alias a nullable reference type, and the C# compiler reports an error of CS9132: Using alias cannot be a nullable reference type. The following bits were lifted from the feature specification to help clarify this point: // This is not legal. // Error CS9132: Using alias cannot be a nullable reference type using X = string?; // This is legal. // The alias is to `List<...>` which is itself not a nullable // reference type itself, even though it contains one as a type argument. using Y = System.Collections.Generic.List<string?>; // This is legal. // This is a nullable *value* type, not a nullable *reference* type. using Z = int?; [HEADING=1]Sample App: UFO Sightings [/HEADING] The demo app is available on GitHub at [iCODE]IEvangelist/alias-any-type[/iCODE]. It’s a simple console app that emulates unidentified flying object (UFO) sightings. If you want to follow along locally, you can do so with any of the following methods in a working directory of your choice: Using the Git CLI: [iCODE]git clone https://github.com/IEvangelist/alias-any-type.git[/iCODE] Using the GitHub CLI: [iCODE]gh repo clone IEvangelist/alias-any-type[/iCODE] Download the zip file: If you’d rather download the source code, a zip file is available at the following URL: IEvangelist/alias-any-type source zip To run the app, from the root directory execute the following .NET CLI command: [iCODE]dotnet run --project ./src/Alias.AnyType.csproj[/iCODE] When the app starts, it prints an introduction to the console—and waits for user input before continuing. After pressing any key, for example the Enter key, the app randomly generates valid coordinates (latitude and longitude), then using said coordinates retrieves geo-code metadata related to the coordinates. The coordinates are represented in degrees-minutes-seconds format (including cardinality). And when the app is running, distances between the generated coordinates are calculated and reported as UFO sightings. To stop the app, press the Ctrl + C keys. While this app is simple, it does include other bits of C# that aren’t necessarily relevant to our focus for this post. I’ll be sure to keep peripheral topics light, but touch on them when I think they’re important. [HEADING=1]Code Walkthrough [/HEADING] We’ll use this section to walk the codebase together. There are several interesting aspects of the code that I’d like to highlight, including the project file, GlobalUsings.cs, some extensions, and the Program.cs file. Of the code available, there’s a few things that we’re not going to cover, for example the response models and several of the utilitarian methods. └───📂 src ├───📂 Extensions │ └─── CoordinateExtensions.cs ├───📂 ResponseModels │ ├─── GeoCode.cs │ ├─── Informative.cs │ └─── LocalityInfo.cs ├─── Alias.AnyType.csproj ├─── CoordinateGeoCodePair.cs ├─── GlobalUsings.cs ├─── Program.cs ├─── Program.Http.cs └─── Program.Utils.cs Let’s start by looking at the project file: <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <Using Include="System.Console" Static="true" /> <Using Include="System.Diagnostics" /> <Using Include="System.Net.Http.Json" /> <Using Alias="AsyncCancelable" Include="System.Runtime.CompilerServices.EnumeratorCancellationAttribute" /> <Using Include="System.Text" /> <Using Include="System.Text.Json.Serialization" /> <Using Include="System.Text.Json" /> </ItemGroup> </Project> The first thing to note here, is that the [iCODE]ImplicitUsings[/iCODE] property is set to [iCODE]enable[/iCODE]. This feature has been around since C# 10 and it enables the target SDK (in this case the [iCODE]Microsoft.NET.Sdk[/iCODE]) to implicitly include a set of namespaces by default. Different SDKs include different default namespaces, for more information, see the Implicit using directives documentation. [HEADING=2]Implicit Using Directives [/HEADING] The [iCODE]ImplicitUsing[/iCODE] element is a feature of MS Build, whereas the [iCODE]global[/iCODE] keyword is a feature of C# the language. Since we’ve opted into global using functionality, we can also take advantage of this feature by adding our own directives. One way to add these directives is by adding [iCODE]Using[/iCODE] elements within an [iCODE]ItemGroup[/iCODE]. Some using directives are added with the [iCODE]Static[/iCODE] attribute set to [iCODE]true[/iCODE], which means all of their [iCODE]static[/iCODE] members are available without qualification—more on this later. The [iCODE]Alias[/iCODE] attribute is used to create an alias for a type, in this example we’ve specified an alias of [iCODE]AsyncCancelable[/iCODE] for the [iCODE]System.Runtime.CompilerServices.EnumeratorCancellationAttribute[/iCODE] type. In our code, we can now use [iCODE]AsyncCancelable[/iCODE] as a type alias for [iCODE]EnumeratorCancellation[/iCODE] attribute. The other [iCODE]Using[/iCODE] elements create non-static and non-aliased [iCODE]global using[/iCODE] directives for their corresponding namespaces. [HEADING=2]An Emerging Pattern [/HEADING] We’re starting to see a common pattern emerge in modern .NET codebases where developers define a GlobalUsings.cs file to encapsulate all (or most) using directives into a single file. This demo app follows this pattern, let’s take a look at the file next: // Ensures that all types within these namespaces are globally available. global using Alias.AnyType; global using Alias.AnyType.Extensions; global using Alias.AnyType.ResponseModels; // Expose all static members of math. global using static System.Math; // Alias a coordinates object. global using Coordinates = (double Latitude, double Longitude); // Alias representation of degrees-minutes-second (DMS). global using DMS = (int Degree, int Minute, double Second); // Alias representation of various distances in different units of measure. global using Distance = (double Meters, double Kilometers, double Miles); // Alias a stream of coordinates represented as an async enumerable. global using CoordinateStream = System.Collections.Generic.IAsyncEnumerable< Alias.AnyType.CoordinateGeoCodePair>; // Alias the CTS, making it simply "Signal". global using Signal = System.Threading.CancellationTokenSource; Everything in this file is a [iCODE]global using[/iCODE] directive, making the alias types, static members, or namespaces available throughout the entire project. The first three directives are for common namespaces, which are used in multiple places throughout the app. The next directive is a [iCODE]global using static[/iCODE] directive for the [iCODE]System.Math[/iCODE] namespace, which makes all of static members of [iCODE]Math[/iCODE] available without qualification. The remaining directives are [iCODE]global using[/iCODE] directives that create aliases for various types, including several tuples, a stream of coordinates, and a [iCODE]CancellationTokenSource[/iCODE] that’s now simply referable via [iCODE]Signal[/iCODE]. One thing to consider is that when you define a tuple alias type, you can easily pivot later to a [iCODE]record[/iCODE] type if you need to add behavior or additional properties. For example, later on you might determine that you’d really like to add some functionality to the [iCODE]Coordinates[/iCODE] type, you could easily change it to a [iCODE]record[/iCODE] type: namespace Alias.AnyType; public readonly record struct Coordinates( double Latitude, double Longitude); When you define an alias, you’re not actually creating a type, but rather a name that refers to an existing type. In the case of defining tuples, you’re defining the shape of a value tuple. When you alias an array type, you’re not creating a new array type, but rather aliases the type with perhaps a more descriptive name. For example, when I define an API that returns an [iCODE]IAsyncEnumerable<CoordinateGeoCodePair>[/iCODE], that’s a lot to write. Instead, I can now refer to it the return type as [iCODE]CoordinateStream[/iCODE] throughout my codebase. [HEADING=2]Referencing Aliases [/HEADING] There were several aliases defined, some in the project file and others in the GlobalUsings.cs file. Let’s look at how these aliases are used in the codebase. The start by looking at the top-level Program.cs file: using Signal signal = GetCancellationSignal(); WriteIntroduction(); try { Coordinates? lastObservedCoordinates = null; await foreach (var coordinate in GetCoordinateStreamAsync(signal.Token)) { (Coordinates coordinates, GeoCode geoCode) = coordinate; // Use extension method, that extends the aliased type. var cardinalizedCoordinates = coordinates.ToCardinalizedString(); // Write UFO coordinate details to the console. WriteUfoCoordinateDetails(coordinates, cardinalizedCoordinates, geoCode); // Write travel alert, including distance traveled. WriteUfoTravelAlertDetails(coordinates, lastObservedCoordinates); await Task.Delay(UfoSightingInterval, signal.Token); lastObservedCoordinates = coordinates; } } catch (Exception ex) when (Debugger.IsAttached) { // https://x.com/davidpine7/status/1415877304383950848 _ = ex; Debugger.Break(); } The preceding code snippet shows how the [iCODE]Signal[/iCODE] alias is used to create a [iCODE]CancellationTokenSource[/iCODE] instance. As you may know, the [iCODE]CancellationTokenSource[/iCODE] class is an implementation of [iCODE]IDisposable[/iCODE], that’s why we can use the [iCODE]using[/iCODE] statement to ensure that the [iCODE]Signal[/iCODE] instance is properly disposed of when it goes out of scope. Your IDE understands these aliases, and when you hover over them, you’ll see the actual type that they represent. Consider the following screen capture: The introduction is written to the console from the [iCODE]WriteIntroduction[/iCODE] call, just before entering a [iCODE]try / catch[/iCODE]. The [iCODE]try[/iCODE] block contains an [iCODE]await foreach[/iCODE] loop that iterates over an [iCODE]IAsyncEnumerable<CoordinateGeoCodePair>[/iCODE]. The [iCODE]GetCoordinateStreamAsync[/iCODE] method is defined in a separate file. I find myself leveraging [iCODE]partial class[/iCODE] functionality more often when I write top-level programs, as it helps to isolate concerns. All of the HTTP-based functionality is defined in the Program.Http.cs file, let’s focus on the [iCODE]GetCoordinateStreamAsync[/iCODE] method: static async CoordinateStream GetCoordinateStreamAsync( [AsyncCancelable] CancellationToken token) { token.ThrowIfCancellationRequested(); do { var coordinates = GetRandomCoordinates(); if (await GetGeocodeAsync(coordinates, token) is not { } geoCode) { break; } token.ThrowIfCancellationRequested(); yield return new CoordinateGeoCodePair( Coordinates: coordinates, GeoCode: geoCode); } while (!token.IsCancellationRequested); } You’ll notice that it returns the [iCODE]CoordinateStream[/iCODE] alias, which is an [iCODE]IAsyncEnumerable<CoordinateGeoCodePair>[/iCODE]. It accepts an [iCODE]AsyncCancelable[/iCODE] attribute, which is an alias for the [iCODE]EnumeratorCancellationAttribute[/iCODE] type. This attribute is used to decorate the cancellation token such that it’s used in conjunction with [iCODE]IAsyncEnumerable[/iCODE] to support cancellation. While cancellation isn’t requested, the method generates random coordinates, retrieves geo-code metadata, and yields a new [iCODE]CoordinateGeoCodePair[/iCODE] instance. The [iCODE]GetGeocodeAsync[/iCODE] method requests the geo-code metadata for the given coordinates, and if successful, it returns the [iCODE]GeoCode[/iCODE] response model. For example, Microsoft Campus has the following coordinates: GET /data/reverse-geocode-client?latitude=47.637&longitude=-122.124 HTTP/1.1 Host: api.bigdatacloud.net Scheme: https To see the JSON, open this link in your browser. The [iCODE]CoordinateGeoCodePair[/iCODE] type is not aliased, but it’s a [iCODE]readonly record struct[/iCODE] that contains a [iCODE]Coordinates[/iCODE] and a [iCODE]GeoCode[/iCODE]: namespace Alias.AnyType; internal readonly record struct CoordinateGeoCodePair( Coordinates Coordinates, GeoCode GeoCode); Going back to the [iCODE]Program[/iCODE] class, when we’re iterating each coordinate geo-code pair, we deconstruct the tuple into [iCODE]Coordinates[/iCODE] and [iCODE]GeoCode[/iCODE] instances. The [iCODE]Coordinates[/iCODE] type is an alias for a tuple of two [iCODE]double[/iCODE] values representing the latitude and longitude. Again, hover over this type in your IDE to quickly see the type, consider the following screen capture: The [iCODE]GeoCode[/iCODE] type is a response model that contains information about the geo-code metadata. We then use an extension method to convert the [iCODE]Coordinates[/iCODE] to a cardinalized string, which is a string representation of the coordinates in degrees-minutes-seconds format. I personally, love how easy it is to use aliases throughout your codebase. Let’s look at some of the extension methods that extend or return aliased types: internal static string ToCardinalizedString(this Coordinates coordinates) { var (latCardinalized, lonCardinalized) = ( FormatCardinal(coordinates.Latitude, true), FormatCardinal(coordinates.Longitude, false) ); return $"{latCardinalized},{lonCardinalized}"; static string FormatCardinal(double degrees, bool isLat) { (int degree, int minute, double second) = degrees.ToDMS(); var cardinal = degrees.ToCardinal(isLat); return $"{degree}°{minute}'{second % 60:F4}\"{cardinal}"; } } This extension method, extends the [iCODE]Coordinates[/iCODE] alias type and return a string representation of the coordinates. It uses the [iCODE]ToDMS[/iCODE] extension method to convert the latitude and longitude to degrees-minutes-seconds format. The [iCODE]ToDMS[/iCODE] extension method is defined as follows: internal static DMS ToDMS(this double coordinate) { var ts = TimeSpan.FromHours(Abs(coordinate)); int degrees = (int)(Sign(coordinate) * Floor(ts.TotalHours)); int minutes = ts.Minutes; double seconds = ts.TotalSeconds; return new DMS(degrees, minutes, seconds); } If you recall, the [iCODE]DMS[/iCODE] alias is a tuple of three values representing degrees, minutes, and seconds. The [iCODE]ToDMS[/iCODE] extension method takes a [iCODE]double[/iCODE] value and returns a [iCODE]DMS[/iCODE] tuple. The [iCODE]ToCardinal[/iCODE] extension method is used to determine the cardinal direction of the coordinate, returning either [iCODE]N[/iCODE], [iCODE]S[/iCODE], [iCODE]E[/iCODE], or [iCODE]W[/iCODE]. The [iCODE]Abs[/iCODE], [iCODE]Sign[/iCODE], and [iCODE]Floor[/iCODE] methods are all static members of the [iCODE]System.Math[/iCODE] namespace, which is aliased in the GlobalUsings.cs file. Beyond that, the app displays the UFO sighting details to the console, including the coordinates, geo-code metadata, and the distance traveled between sightings. This occurs on repeat, until the user stops the app with a Ctrl + C key combination. [HEADING=1]Next steps [/HEADING] Be sure to try this out in your own code! Check back soon for the last post in the series, where we’ll explore default lambda parameters. To continue learning more about this feature, checkout the following resources: C# using directive: using alias Allow using alias directive to reference any kind of Type Tuple types (C# reference) MSBuild reference for .NET SDK projects: Enable [iCODE]ImplicitUsings[/iCODE] The post Refactor your code using alias any type appeared first on .NET Blog. Continue reading... Quote
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.