Here's a summary of what's new in .NET Libraries in this release:
- WebSocket
Keep-Alive
Ping and Timeout APIs - Add ZLib, Brotli compression options
- Add TarEntry.DataOffset
HttpClientFactory
no longer logs header values by default- Out-of-proc Meter wildcard listening
Libraries updates in .NET 9 Release Candidate 1:
- Release notes
- What's new in .NET 9 documentation
.NET 9 Release Candidate 1:
We've added new APIs on ClientWebSocketOptions
and WebSocketCreationOptions
that let you opt-in to sending WebSocket pings, and abort the connection if the peer does not respond in time.
Until now, you could specify a KeepAliveInterval
to keep the connection from staying idle, but there was no built-in mechanism to enforce that the peer is responding.
The following example pings the server every 5 seconds and aborts the connection if it does not respond within a second.
using var cws = new ClientWebSocket();
cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
cws.Options.KeepAliveInterval = TimeSpan.FromSeconds(5);
cws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);
await cws.ConnectAsync(uri, httpClient, cancellationToken);
We added ZLibCompressionOptions
and BrotliCompressionOptions
types for setting algorithm specific compression level and compression strategy for users who would like to set more fine-tuned settings than the only existing option CompressionLevel. The new compression options are designed to allow for expanding to more options in the future.
namespace System.IO.Compression
{
+ public enum ZLibCompressionStrategy
+ {
+ Default = 0,
+ Filtered = 1,
+ HuffmanOnly = 2,
+ RunLengthEncoding = 3,
+ Fixed = 4,
+ }
+ public sealed class ZLibCompressionOptions
+ {
+ public int CompressionLevel { get; set; }
+ public ZLibCompressionStrategy CompressionStrategy { get; set; }
+ }
public sealed class ZLibStream : Stream
{
+ public ZLibStream(Stream stream, ZLibCompressionOptions compressionOptions, bool leaveOpen = false);
}
public partial class DeflateStream : Stream
{
+ public DeflateStream(Stream stream, ZLibCompressionOptions compressionOptions, bool leaveOpen = false);
}
public partial class GZipStream : Stream
{
+ public GZipStream(Stream stream, ZLibCompressionOptions compressionOptions, bool leaveOpen = false);
}
+ public sealed class BrotliCompressionOptions
+ {
+ public int Quality { get; set; }
+ }
public sealed partial class BrotliStream : System.IO.Stream
{
+ public BrotliStream(Stream stream, BrotliCompressionOptions compressionOptions, bool leaveOpen = false) { }
}
}
API Usage:
private MemoryStream CompressStream(Stream uncompressedStream)
{
var compressorOutput = new MemoryStream();
using var compressionStream = new ZLibStream(compressorOutput, new ZLibCompressionOptions() { CompressionLevel = 6, CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly });
uncompressedStream.CopyTo(compressionStream);
compressionStream.Flush();
return compressorOutput;
}
public abstract partial class TarEntry
{
+ public long DataOffset { get; }
}
TarEntry.DataOffset
returns a position in the entry's archive stream where the entry's first data byte is located. The entry's data is encapsulated in a sub-stream which users can access via TarEntry.DataStream
, which hides the real position of the data relative to the archive stream. This is enough for most users, but for those who need more flexibility and want to know the real starting position of the data in the archive stream, we added this new API, which should make it easy to support features like concurrent access with very large TAR files.
// Create stream for tar ball data in Azure Blob Storage
var blobClient = Azure.Storage.Blobs.BlobClient(....);
var blobClientStream = await blobClient.OpenReadAsync(...);
// Create TarReader for the stream and get a TarEntry
var tarReader = new System.Formats.Tar.TarReader(blobClientStream);
var tarEntry = await tarReader.GetNextEntryAsync();
// get position of TarEntry data in blob stream
var entryOffsetInBlobStream = tarEntry.DataOffset;
var entryLength = tarEntry.Length;
// create a separate stream
var newBlobClientStream = await TarBlob.OpenReadAsync(...);
newBlobClientStream.Seek(entryOffsetInBlobStream, SeekOrigin.Begin);
// read tar ball content from separate BlobClient stream
var bytes = new byte[length];
await tarBlobStream.ReadAsync(bytes, 0, (int)entryLength);
LogLevel.Trace
events logged by HttpClientFactory
no longer include header values by default. You may opt-in to logging values for specific headers via the RedactLoggedHeaders
helper method.
The following example redacts all headers, except for the user agent.
services.AddHttpClient("myClient")
.RedactLoggedHeaders(name => name != "User-Agent");
See the documentation for detailed breaking change guidance.
It is already possible to listen to meters out-of-process using the System.Diagnostics.Metrics
event source provider, but the current implementation requires specifying the full meter name. This update introduces support for listening to all meters by using the wildcard character *
, allowing you to capture metrics from every meter in a process. Additionally, it adds support for listening by meter prefix, so you can listen to all meters whose names start with a specified prefix. For example, using MyMeter*
will enable listening to all meters with names beginning with MyMeter
.
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
var meter = new Meter("MyCompany.MyMeter"); // Note the complete meter name is "MyCompany.MyMeter"
meter.CreateObservableCounter("MyCounter", () => 1); // Create a counter and allow publishing values.
// Create the listener to use the wildcard character to listen to all meters using prefix names.
MyEventListener listener = new MyEventListener();
// Press Enter to exit the program.
Console.ReadLine();
The MyEventListener class is defined as follows:
internal class MyEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
Console.WriteLine(eventSource.Name);
if (eventSource.Name == "System.Diagnostics.Metrics")
{
// Listening to all meters with names starting with "MyCompany".
// If using "*" allow listening to all meters.
EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)0x3, new Dictionary<string,string?>() { { "Metrics", "MyCompany*" } });
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (eventData.EventSource.Name != "System.Diagnostics.Metrics" || eventData.EventName == "CollectionStart" || eventData.EventName == "CollectionStop" || eventData.EventName == "InstrumentPublished")
{
// ignore all events we are not interested in
return;
}
Console.WriteLine(eventData.EventName);
if (eventData.Payload is not null)
{
for (int i = 0; i < eventData.Payload.Count; i++)
{
Console.WriteLine($"\t{eventData.PayloadNames![i]}: {eventData.Payload[i]}");
}
}
}
}
Upon executing the code, the output displaying the published counter values should appear:
CounterRateValuePublished
sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
meterName: MyCompany.MyMeter
meterVersion:
instrumentName: MyCounter
unit:
tags:
rate: 0
value: 1
instrumentId: 1
CounterRateValuePublished
sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
meterName: MyCompany.MyMeter
meterVersion:
instrumentName: MyCounter
unit:
tags:
rate: 0
value: 1
instrumentId: 1
Monitoring tools like dotnet-counters can also be used to listen to the metrics using the wildcard.