The core and extension libraries in .NET 8 have been updated to include new features and enhancements. These updates provide developers with more tools and capabilities to build robust and feature-rich applications. The libraries have been optimized for better performance and usability, making it easier for developers to implement complex functionality with less code.
Core .net libraries
Reflection
Function pointers were introduced in .NET 5, but initial support for reflection was not included. Previously, using typeof or reflection on a function pointer—such as typeof(delegate*<void>()) or FieldInfo.FieldType—would return an IntPtr. Beginning with .NET 8, these operations now return a System.Type object. This enhancement allows access to function pointer metadata, including calling conventions, return type, and parameters. The new functionality is currently implemented only in the CoreCLR runtime and MetadataLoadContext.
New APIs have been introduced to System.Type, including IsFunctionPointer, as well as to System.Reflection.PropertyInfo, System.Reflection.FieldInfo, and System.Reflection.ParameterInfo. The following code example demonstrates how to use some of these new reflection APIs:
using System; using System.Reflection; public class Example { public delegate*<void> FunctionPointerField; public static void Main() { Type type = typeof(Example); FieldInfo fieldInfo = type.GetField("FunctionPointerField"); if (fieldInfo.FieldType.IsFunctionPointer) { Console.WriteLine("Field is a function pointer."); } // Accessing function pointer metadata var functionPointerType = fieldInfo.FieldType; Console.WriteLine($"Is function pointer: {functionPointerType.IsFunctionPointer}"); Console.WriteLine($"Calling convention: {functionPointerType.GetFunctionPointerCallingConvention()}"); Console.WriteLine($"Return type: {functionPointerType.GetFunctionPointerReturnType()}"); var parameterTypes = functionPointerType.GetFunctionPointerParameters(); Console.WriteLine("Parameter types:"); foreach (var paramType in parameterTypes) { Console.WriteLine(paramType); } } }
Serialization
Many improvements have been made to the System.Text.Json serialization and deserialization functionality in .NET 8. For instance, you can now customize the handling of members that aren’t present in the JSON payload.
The following sections describe other serialization improvements:
Built-in support for additional types
The serializer has built-in support for the following additional types.
Console.WriteLine(JsonSerializer.Serialize( [ Half.MaxValue, Int128.MaxValue, UInt128.MaxValue ])); // [65500,170141183460469231731687303715884105727,340282366920938463463374607431768211455]
-
Memory<T> and ReadOnlyMemory<T> values. byte values are serialized to Base64 strings, and other types to JSON arrays.
JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 1, 2, 3 }); // "AQID" JsonSerializer.Serialize<Memory<int>>(new int[] { 1, 2, 3 }); // [1,2,3]
Source generator
.NET 8 includes enhancements to the System.Text.Json source generator, designed to make the Native AOT (Ahead-of-Time) experience comparable to the reflection-based serializer.
For example:
- The source generator now seamlessly handles serializing types with required and init properties, aligning with the functionality already present in reflection-based serialization.
- Enhancements have been made to improve the formatting of source-generated code.
- The JsonSourceGenerationOptionsAttribute now matches the feature set of JsonSerializerOptions, offering comprehensive configuration capabilities.
- Additional diagnostics, including SYSLIB1034 and SYSLIB1039, have been included for improved debugging.
- Types of ignored or inaccessible properties are now excluded from serialization.
- Support for nesting JsonSerializerContext declarations within various type definitions has been implemented.
- In scenarios involving weakly typed source generation, the system now handles compiler-generated or unnamed types gracefully. Runtime resolution identifies the most appropriate supertype for serialization, ensuring seamless operation.
- Introducing the new converter type JsonStringEnumConverter<TEnum>. Please note that the existing JsonStringEnumConverter class isn’t compatible with Native AOT. You can annotate your enum types as follows:
[JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))] public enum MyEnum { Value1, Value2, Value3 } [JsonSerializable(typeof(MyEnum))] public partial class MyContext : JsonSerializerContext { }
-
The newly introduced JsonConverter.Type property allows you to retrieve the type of a non-generic JsonConverter instance.
Dictionary<Type, JsonConverter> CreateDictionary(IEnumerable<JsonConverter> converters) => converters.Where(converter => converter.Type != null) .ToDictionary(converter => converter.Type!);
Chain source generators
The `JsonSerializerOptions` class now features a new `TypeInfoResolverChain` property alongside the existing `TypeInfoResolver`. These properties facilitate contract customization by enabling the chaining of source generators. With the introduction of the `TypeInfoResolverChain` property, you no longer need to specify all chained components at one call site—they can be added subsequently. Moreover, `TypeInfoResolverChain` allows introspection of the chain and removal of components. For detailed guidance, refer to [Combine source generators](link).
Additionally, the `JsonSerializerOptions.AddContext<TContext>()` method is now obsolete. It has been replaced by the `TypeInfoResolver` and `TypeInfoResolverChain` properties.
Interface hierarchies
In .NET 8, support has been introduced for serializing properties from interface hierarchies. The following code demonstrates an example where properties from both the immediately implemented interface and its base interface are serialized.
public static void InterfaceHierarchies() { IDerived value = new DerivedImplement { Base = 0, Derived = 1 }; string json = JsonSerializer.Serialize(value); Console.WriteLine(json); // {"Derived":1,"Base":0} } public interface IBase { public int Base { get; set; } } public interface IDerived : IBase { public int Derived { get; set; } } public class DerivedImplement : IDerived { public int Base { get; set; } public int Derived { get; set; } }
Read-only properties
In .NET 8, you can now deserialize onto read-only fields or properties, i.e., those lacking a set accessor.
To activate this functionality globally, set a new option, PreferredObjectCreationHandling, to JsonObjectCreationHandling.Populate. For finer control, enable the feature more selectively by placing the [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] attribute on specific types or individual properties.
For instance, consider the following code snippet that deserializes into a CustomerInfo type with two read-only properties.
public static void ReadOnlyProperties() { CustomerInfo customer = JsonSerializer.Deserialize<CustomerInfo>(""" { "Names":["John Doe"], "Company":{"Name":"Contoso"} }""")!; Console.WriteLine(JsonSerializer.Serialize(customer)); } class CompanyInfo { public required string Name { get; set; } public string? PhoneNumber { get; set; } } [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] class CustomerInfo { // Both of these properties are read-only. public List<string> Names { get; } = new(); public CompanyInfo Company { get; } = new() { Name = "N/A", PhoneNumber = "N/A" }; }
Before .NET 8, input values were disregarded, and the Names and Company properties retained their default values.
Output {"Names":[],"Company":{"Name":"N/A","PhoneNumber":"N/A"}}
Now, during deserialization, the input values are utilized to populate the read-only properties.
Output {"Names":["John Doe"],"Company":{"Name":"Contoso","PhoneNumber":"N/A"}}
Disable Reflection-based default
In .NET 8, you can now opt to disable the reflection-based serializer by default. This is particularly useful to prevent unintentional inclusion of reflection components that are not in use, particularly in trimmed and Native AOT (Ahead-of-Time) applications. To achieve this, set the `JsonSerializerIsReflectionEnabledByDefault` MSBuild property to `false` in your project file, requiring a `JsonSerializerOptions` argument to be passed to the `JsonSerializer` serialization and deserialization methods.
You can utilize the new `IsReflectionEnabledByDefault` API to check the status of this feature switch. If you’re a library author building on top of `System.Text.Json`, this property enables you to configure your defaults without inadvertently including reflection components.
New JsonNode API methods
The JsonNode and System.Text.Json.Nodes.JsonArray types include the following new methods.
public partial class JsonNode { // Creates a deep clone of the current node and all its descendants. public JsonNode DeepClone(); // Returns true if the two nodes are equivalent JSON representations. public static bool DeepEquals(JsonNode? node1, JsonNode? node2); // Determines the JsonValueKind of the current node. public JsonValueKind GetValueKind(JsonSerializerOptions options = null); // If node is the value of a property in the parent object, returns its name. // Throws InvalidOperationException otherwise. public string GetPropertyName(); // If node is the element of a parent JsonArray, returns its index. // Throws InvalidOperationException otherwise. public int GetElementIndex(); // Replaces this instance with a new value, updating the parent object/array accordingly. public void ReplaceWith<T>(T value); // Asynchronously parses a stream as UTF-8 encoded data representing a single JSON value into a JsonNode. public static Task<JsonNode?> ParseAsync( Stream utf8Json, JsonNodeOptions? nodeOptions = null, JsonDocumentOptions documentOptions = default, CancellationToken cancellationToken = default); } public partial class JsonArray { // Returns an IEnumerable<T> view of the current array. public IEnumerable<T> GetValues<T>(); }
Non-public members
You can selectively include non-public members into the serialization contract for a particular type by using the `JsonIncludeAttribute` and `JsonConstructorAttribute` attribute annotations.
public static void NonPublicMembers()
{
string json = JsonSerializer.Serialize(new MyPoco(42));
Console.WriteLine(json);
// {"X":42}
JsonSerializer.Deserialize<MyPoco>(json);
}
public class MyPoco
{
[JsonConstructor]
internal MyPoco(int x) => X = x;
[JsonInclude]
internal int X { get; }
}
WithAddedModifier extension method
The newly introduced `WithAddedModifier(IJsonTypeInfoResolver, Action<JsonTypeInfo>)` extension method provides a convenient way to introduce modifications to the serialization contracts of any `IJsonTypeInfoResolver` instances.
var options = new JsonSerializerOptions
{
TypeInfoResolver = MyContext.Default.WithAddedModifier
(
static typeInfo =>
{
foreach (JsonPropertyInfo prop in typeInfo.Properties)
{
prop.Name = prop.Name.ToUpperInvariant();
}
}
)
};
New JsonContent.Create overloads
In .NET 8, you can now generate `JsonContent` instances using trim-safe or source-generated contracts. The new methods are:
- JsonContent.Create(Object, JsonTypeInfo, MediaTypeHeaderValue)
- JsonContent.Create<T>(T, JsonTypeInfo<T>, MediaTypeHeaderValue)
var book = new Book(id: 42, "Title", "Author", publishedYear: 2023);
HttpContent content = JsonContent.Create(book, MyContext.Default.Book);
public record Book(int id, string title, string author, int publishedYear);
[JsonSerializable(typeof(Book))]
public partial class MyContext : JsonSerializerContext
{}
Freeze a JsonSerializerOptions instance
The following new methods provide control over when a `JsonSerializerOptions` instance is frozen:
- JsonSerializerOptions.MakeReadOnly(): This overload is trim-safe and will throw an exception if the options instance hasn’t been configured with a resolver.
- JsonSerializerOptions.MakeReadOnly(Boolean): If you pass `true` to this overload, it populates the options instance with the default reflection resolver if one is missing. However, this method is marked `RequiresUnreferenceCode/RequiresDynamicCode` and is thus unsuitable for Native AOT applications.
Time abstraction
The introduction of the new `TimeProvider` class and `ITimer` interface brings time abstraction functionality, enabling you to mock time in test scenarios. Additionally, you can utilize time abstraction to mock `Task` operations dependent on time progression, such as `Task.Delay` and `Task.WaitAsync`. The time abstraction facilitates essential time operations, including:
- Retrieve local and UTC time
- Obtain a timestamp for measuring performance
- Create a timer
The following code snippet shows some usage examples.
// Get system time. DateTimeOffset utcNow = TimeProvider.System.GetUtcNow(); DateTimeOffset localNow = TimeProvider.System.GetLocalNow(); TimerCallback callback = s => ((State)s!).Signal(); // Create a timer using the time provider. ITimer timer = _timeProvider.CreateTimer( callback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan); // Measure a period using the system time provider. long providerTimestamp1 = TimeProvider.System.GetTimestamp(); long providerTimestamp2 = TimeProvider.System.GetTimestamp(); TimeSpan period = _timeProvider.GetElapsedTime(providerTimestamp1, providerTimestamp2); // Create a time provider that works with a time zone that's different than the local time zone. private class ZonedTimeProvider(TimeZoneInfo zoneInfo) : TimeProvider() { private readonly TimeZoneInfo _zoneInfo = zoneInfo ?? TimeZoneInfo.Local; public override TimeZoneInfo LocalTimeZone => _zoneInfo; public static TimeProvider FromLocalTimeZone(TimeZoneInfo zoneInfo) => new ZonedTimeProvider(zoneInfo); }
UTF8 improvements
To enable the writing out of a string-like representation of your type to a destination span, implement the new `IUtf8SpanFormattable` interface on your type. This interface, closely related to `ISpanFormattable`, targets UTF-8 and `Span<byte>` instead of UTF-16 and `Span<char>`.
`IUtf8SpanFormattable` has been implemented on all primitive types, among others, with shared logic targeting string, `Span<char>`, or `Span<byte>`. It fully supports all formats, including the new “B” binary specifier, and all cultures. Consequently, you can now format directly to UTF-8 from a wide range of types, including Byte, Complex, Char, DateOnly, DateTime, DateTimeOffset, Decimal, Double, Guid, Half, IPAddress, IPNetwork, Int16, Int32, Int64, Int128, IntPtr, NFloat, SByte, Single, Rune, TimeOnly, TimeSpan, UInt16, UInt32, UInt64, UInt128, UIntPtr, and Version.
The new `Utf8.TryWrite` methods offer a UTF-8-based alternative to the existing `MemoryExtensions.TryWrite` methods, which are UTF-16-based. You can utilize interpolated string syntax to directly format a complex expression into a span of UTF-8 bytes. For example:
static bool FormatHexVersion( short major, short minor, short build, short revision, Span<byte> utf8Bytes, out int bytesWritten) => Utf8.TryWrite( utf8Bytes, CultureInfo.InvariantCulture, $"{major:X4}.{minor:X4}.{build:X4}.{revision:X4}", out bytesWritten);
The implementation recognizes `IUtf8SpanFormattable` on the format values and utilizes their implementations to write their UTF-8 representations directly to the destination span.
Additionally, it leverages the new `Encoding.TryGetBytes(ReadOnlySpan<Char>, Span<Byte>, Int32)` method, along with its counterpart `Encoding.TryGetChars(ReadOnlySpan<Byte>, Span<Char>, Int32)`. These methods support encoding and decoding into a destination span. If the span isn’t long enough to hold the resulting state, the methods return `false` rather than throwing an exception.
Methods for working with randomness
The System.Random and System.Security.Cryptography.RandomNumberGenerator types introduce two new methods for working with randomness.
GetItems<T>()
The introduction of `System.Random.GetItems` and `System.Security.Cryptography.RandomNumberGenerator.GetItems` methods allows for the random selection of a specified number of items from a given set. Below is an example illustrating the usage of `System.Random.GetItems<T>()`, utilizing the instance provided by `Random.Shared`, to randomly insert 31 items into an array. This functionality could be utilized in games like “Simon,” where players need to recall a sequence of colored buttons.
private static ReadOnlySpan<Button> s_allButtons = new[] { Button.Red, Button.Green, Button.Blue, Button.Yellow, }; Button[] thisRound = Random.Shared.GetItems(s_allButtons, 31);
Shuffle<T>()
The introduction of the new `Random.Shuffle` and `RandomNumberGenerator.Shuffle<T>(Span<T>)` methods enables the randomization of the order of a span. These methods prove useful for mitigating training bias in machine learning scenarios, ensuring that the sequence of data isn’t consistently influencing training or testing outcomes.
YourType[] trainingData = LoadTrainingData(); Random.Shared.Shuffle(trainingData); IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData); DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData); model = chain.Fit(split.TrainSet); IDataView predictions = model.Transform(split.TestSet);
Performance-focused types
.NET 8 brings forth various types designed to elevate application performance.
Among these, the `System.Collections.Frozen` namespace debuts two new collection types: `FrozenDictionary<TKey, TValue>` and `FrozenSet<T>`. These types enforce immutability, prohibiting any alterations to keys and values after collection creation. This restriction leads to swifter read operations, notably `TryGetValue()`. Particularly, these types shine in scenarios where collections are initially populated and then retained throughout the lifecycle of long-running services, as exemplified below:
private static readonly FrozenDictionary<string, bool> s_configurationData =
LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true);
// ...
if (s_configurationData.TryGetValue(key, out bool setting) && setting)
{
Process();
}
- Methods like `MemoryExtensions.IndexOfAny` are designed to locate the first occurrence of any value within the passed collection. In .NET 8, the new `System.Buffers.SearchValues<T>` type is introduced for this purpose. Consequently, new overloads of methods such as `MemoryExtensions.IndexOfAny` now accept an instance of this type. When you instantiate `SearchValues<T>`, all necessary data to optimize subsequent searches is derived upfront, eliminating the need for repeated work.
- Additionally, the new `System.Text.CompositeFormat` type proves beneficial for optimizing format strings that aren’t known at compile time. This is particularly useful when the format string is loaded from a resource file. Although there’s some initial overhead, such as string parsing, this approach saves time by avoiding repetitive work on each usage.
private static readonly CompositeFormat s_rangeMessage = CompositeFormat.Parse(LoadRangeMessageResource()); // ... static string GetMessage(int min, int max) => string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max);
Data Validation
The System.ComponentModel.DataAnnotations namespace introduces new data validation attributes tailored for validation scenarios in cloud-native services. Unlike the existing DataAnnotations validators, which are primarily focused on typical UI data-entry validation, such as form fields, the new attributes are specifically crafted for validating non-user-entry data, such as configuration options. Alongside the new attributes, additional properties have been incorporated into the RangeAttribute type.
| New API | Description |
| RangeAttribute.MinimumIsExclusive RangeAttribute.MaximumIsExclusive |
Specifies whether bounds are included in the allowable range. |
| System.ComponentModel.DataAnnotations.LengthAttribute | Specifies both lower and upper bounds for strings or collections. For example, [Length(10, 20)] requires at least 10 elements and at most 20 elements in a collection. |
| System.ComponentModel.DataAnnotations.Base64StringAttribute | Validates that a string is a valid Base64 representation. |
| System.ComponentModel.DataAnnotations.AllowedValuesAttribute System.ComponentModel.DataAnnotations.DeniedValuesAttribute |
Specify allow lists and deny lists, respectively. For example, [AllowedValues(“apple”, “banana”, “mango”)]. |
Metrics
New APIs have been introduced to allow the attachment of key-value pair tags to Meter and Instrument objects during their creation. These tags provide a means for aggregators of published metric measurements to differentiate the aggregated values.
var options = new MeterOptions("name")
{
Version = "version",
// Attach these tags to the created meter.
Tags = new TagList()
{
{ "MeterKey1", "MeterValue1" },
{ "MeterKey2", "MeterValue2" }
}
};
Meter meter = meterFactory!.Create(options);
Counter<int> counterInstrument = meter.CreateCounter<int>
(
"counter", null, null, new TagList() { { "counterKey1", "counterValue1" } }
);
counterInstrument.Add(1);
Cryptography
.NET 8 introduces support for the SHA-3 hashing primitives. These primitives are currently supported by Linux with OpenSSL 1.1.1 or later and Windows 11 Build 25324 or later. APIs that offer SHA-2 functionality now include their SHA-3 counterparts. This encompasses SHA3_256, SHA3_384, and SHA3_512 for hashing; HMACSHA3_256, HMACSHA3_384, and HMACSHA3_512 for HMAC; HashAlgorithmName.SHA3_256, HashAlgorithmName.SHA3_384, and HashAlgorithmName.SHA3_512 for configurable hashing algorithms; and RSAEncryptionPadding.OaepSHA3_256, RSAEncryptionPadding.OaepSHA3_384, and RSAEncryptionPadding.OaepSHA3_512 for RSA OAEP encryption.
The following example demonstrates how to utilize these APIs, including the SHA3_256.IsSupported property for checking if the platform supports SHA-3.
// Hashing example
if (SHA3_256.IsSupported)
{
byte[] hash = SHA3_256.HashData(dataToHash);
}
else
{
// ...
}
// Signing example
if (SHA3_256.IsSupported)
{
using ECDsa ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
byte[] signature = ec.SignData(dataToBeSigned, HashAlgorithmName.SHA3_256);
}
else
{
// ...
}
Currently, SHA-3 support primarily targets cryptographic primitives. Higher-level constructions and protocols, such as X.509 certificates, SignedXml, and COSE, are not expected to have full support for SHA-3 initially.
Networking
Support for HTTPS proxy
Previously, the proxy types supported by HttpClient allowed a “man-in-the-middle” to observe the destination site the client was connecting to, even for HTTPS URIs. With the introduction of HTTPS proxy support, HttpClient now establishes an encrypted channel between the client and the proxy, ensuring that all requests are handled with full privacy.
To enable HTTPS proxy, you can either set the `all_proxy` environment variable or use the `WebProxy` class to control the proxy programmatically.
On Unix systems, you can set the `all_proxy` environment variable like this:
export all_proxy=https://x.x.x.x:3218
On Windows, you can set it using the `set` command:
set all_proxy=https://x.x.x.x:3218
Additionally, you can use the `WebProxy` class to control the proxy programmatically.
Stream-based ZipFile methods
.NET 8 introduces new overloads of `ZipFile.CreateFromDirectory` that enable you to gather all the files contained within a directory, zip them, and then store the resulting zip file into the provided stream. Similarly, new overloads of `ZipFile.ExtractToDirectory` allow you to provide a stream containing a zipped file and extract its contents into the filesystem. Below are the new overloads:
namespace System.IO.Compression;
public static partial class ZipFile
{
public static void CreateFromDirectory( string sourceDirectoryName, Stream destination);
public static void CreateFromDirectory (
string sourceDirectoryName,
Stream destination,
CompressionLevel compressionLevel,
bool includeBaseDirectory);
public static void CreateFromDirectory(
string sourceDirectoryName,
Stream destination,
CompressionLevel compressionLevel,
bool includeBaseDirectory,
Encoding? entryNameEncoding);
public static void ExtractToDirectory( Stream source, string destinationDirectoryName) { }
public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles) { }
public static void ExtractToDirectory( Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }
public static void ExtractToDirectory( Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
}
These new APIs are particularly beneficial when disk space is limited because they bypass the need to utilize disk space as an intermediate step.
Extension libraries
Keyed DI services
Keyed dependency injection (DI) services offer a mechanism for registering and retrieving DI services using keys. By employing keys, you can manage how services are registered and consumed within different scopes. Here are some of the new APIs introduced:
- The `IKeyedServiceProvider` interface.
- The `ServiceKeyAttribute` attribute, which facilitates injecting the key used for registration/resolution into constructors.
- The `FromKeyedServicesAttribute` attribute, which can be applied to constructor parameters to specify the keyed service to utilize.
- Various new extension methods for `IServiceCollection` to support keyed services, such as `ServiceCollectionServiceExtensions.AddKeyedScoped`.
- The `ServiceProvider` implementation of `IKeyedServiceProvider`.
The following example shows you how to use keyed DI services.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<BigCacheConsumer>(); builder.Services.AddSingleton<SmallCacheConsumer>(); builder.Services.AddKeyedSingleton<ICache, BigCache>("big"); builder.Services.AddKeyedSingleton<ICache, SmallCache>("small"); WebApplication app = builder.Build(); app.MapGet("/big", (BigCacheConsumer data) => data.GetData()); app.MapGet("/small", (SmallCacheConsumer data) => data.GetData()); app.MapGet("/big-cache", ([FromKeyedServices("big")] ICache cache) => cache.Get("data")); app.MapGet("/small-cache", (HttpContext httpContext) => httpContext.RequestServices.GetRequiredKeyedService<ICache>("small").Get("data")); app.Run(); class BigCacheConsumer([FromKeyedServices("big")] ICache cache) { public object? GetData() => cache.Get("data"); } class SmallCacheConsumer(IServiceProvider serviceProvider) { public object? GetData() => serviceProvider.GetRequiredKeyedService<ICache>("small").Get("data"); } public interface ICache { object Get(string key); } public class BigCache : ICache { public object Get(string key) => $"Resolving {key} from big cache."; } public class SmallCache : ICache { public object Get(string key) => $"Resolving {key} from small cache."; }
Hosted lifecycle services
Hosted services now offer increased flexibility in execution throughout the application lifecycle. While `IHostedService` previously provided `StartAsync` and `StopAsync`, the introduction of `IHostedLifecycleService` brings additional methods:
- StartingAsync(CancellationToken)
- StartedAsync(CancellationToken)
- StoppingAsync(CancellationToken)
- StoppedAsync(CancellationToken)
These methods run before and after the existing points in the lifecycle, respectively.
The following example shows how to use the new APIs.
using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; internal class HostedLifecycleServices { public async static void RunIt() { IHostBuilder hostBuilder = new HostBuilder(); hostBuilder.ConfigureServices(services => { services.AddHostedService<MyService>(); }); using (IHost host = hostBuilder.Build()) { await host.StartAsync(); } } public class MyService : IHostedLifecycleService { public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask; } }
Options validations
Source generator
To minimize startup overhead and enhance the validation feature set, we’ve introduced a source-code generator that implements the validation logic. Below is an example showcasing model and validator classes:
// Model class public class Person { public string Name { get; set; } public int Age { get; set; } } // Validator class [GeneratedValidator] public partial class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(p => p.Name).NotEmpty().MaximumLength(50); RuleFor(p => p.Age).InclusiveBetween(18, 100); } }
In this example, the `Person` class represents a model, while the `PersonValidator` class, marked with the `[GeneratedValidator]` attribute, contains the validation logic for the `Person` class. The source-code generator generates the implementation of the `PersonValidator` class based on the validation rules specified in the validator class.
LoggerMessageAttribute constructors
The `LoggerMessageAttribute` now provides additional constructor overloads, offering greater flexibility in specifying required parameters with reduced code. Previously, you had to choose between the parameterless constructor or the constructor that demanded all parameters (event ID, log level, and message). With the new overloads, you can specify the necessary parameters more conveniently. If you omit an event ID, the system automatically generates one.
public LoggerMessageAttribute(LogLevel level, string message); public LoggerMessageAttribute(LogLevel level); public LoggerMessageAttribute(string message);
Extensions metrics
IMeterFactory interface
You can register the new `IMeterFactory` interface within dependency injection (DI) containers and employ it to generate Meter objects in a segregated fashion.
Register the IMeterFactory to the DI container using the default meter factory implementation:
// 'services' is the DI IServiceCollection. services.AddMetrics();
Consumers can then obtain the meter factory and use it to create a new Meter object.
IMeterFactory meterFactory = serviceProvider.GetRequiredService<IMeterFactory>(); MeterOptions options = new MeterOptions("MeterName") { Version = "version", }; Meter meter = meterFactory.Create(options);
MetricCollector<T> class
The new `MetricCollector<T>` class enables you to record metric measurements along with timestamps. Moreover, it provides the flexibility to utilize a time provider of your choice for precise timestamp generation.
const string CounterName = "MyCounter"; DateTimeOffset now = DateTimeOffset.Now; var timeProvider = new FakeTimeProvider(now); using var meter = new Meter(Guid.NewGuid().ToString()); Counter<long> counter = meter.CreateCounter<long>(CounterName); using var collector = new MetricCollector<long>(counter, timeProvider); Assert.IsNull(collector.LastMeasurement); counter.Add(3); // Verify the update was recorded. Assert.AreEqual(counter, collector.Instrument); Assert.IsNotNull(collector.LastMeasurement); Assert.AreSame(collector.GetMeasurementSnapshot().Last(), collector.LastMeasurement); Assert.AreEqual(3, collector.LastMeasurement.Value); Assert.AreEqual(now, collector.LastMeasurement.Timestamp);
System.Numerics.Tensors.TensorPrimitives
The updated System.Numerics.Tensors NuGet package introduces APIs within the new TensorPrimitives namespace, enhancing support for tensor operations. These tensor primitives are tailored to optimize data-intensive workloads, particularly those associated with AI and machine learning tasks.
In AI workloads such as semantic search and retrieval-augmented generation (RAG), which enhance the natural language capabilities of large language models like ChatGPT by augmenting prompts with relevant data, operations on vectors—such as cosine similarity to identify the most relevant data to answer a question—are critical. The System.Numerics.Tensors.TensorPrimitives package offers APIs for vector operations, eliminating the need for external dependencies or custom implementations.