Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/core/compatibility/9.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ If you're migrating an app to .NET 9, the breaking changes listed here might aff
| [BigInteger maximum length](core-libraries/9.0/biginteger-limit.md) | Behavioral change | Preview 6 |
| [Creating type of array of System.Void not allowed](core-libraries/9.0/type-instance.md) | Behavioral change | Preview 1 |
| [Default `Equals()` and `GetHashCode()` throw for types marked with `InlineArrayAttribute`](core-libraries/9.0/inlinearrayattribute.md) | Behavioral change | Preview 6 |
| [FromKeyedServicesAttribute no longer injects non-keyed parameter](core-libraries/9.0/non-keyed-params.md) | Behavioral change | RC 1 |
| [IncrementingPollingCounter initial callback is asynchronous](core-libraries/9.0/async-callback.md) | Behavioral change | RC 1 |
| [Inline array struct size limit is enforced](core-libraries/9.0/inlinearray-size.md) | Behavioral change | Preview 1 |
| [InMemoryDirectoryInfo prepends rootDir to files](core-libraries/9.0/inmemorydirinfo-prepends-rootdir.md) | Behavioral change | Preview 1 |
| [RuntimeHelpers.GetSubArray returns different type](core-libraries/9.0/getsubarray-return.md) | Behavioral change | Preview 1 |
Expand Down
88 changes: 88 additions & 0 deletions docs/core/compatibility/core-libraries/9.0/async-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: "IncrementingPollingCounter initial callback is asynchronous"
description: Learn about the .NET 9 breaking change in core .NET libraries where the IncrementingPollingCounter initial callback is no longer synchronous.
ms.date: 09/05/2024
---
# IncrementingPollingCounter initial callback is asynchronous

<xref:System.Diagnostics.Tracing.IncrementingPollingCounter> uses a callback to retrieve current values of a metric and reports it via <xref:System.Diagnostics.Tracing.EventSource> events. In the past, the first invocation of the callback might have occurred synchronously on whatever thread was enabling the `EventSource`; future invocations occurred on a dedicated timer thread. Starting in .NET 9, the first callback always occurs asynchronously on the timer thread. This might result in counter changes that occurred just after the counter was enabled going unobserved because the first callback happens later.

This change is most likely to impact tests that use <xref:System.Diagnostics.Tracing.EventListener> to validate an `IncrementingPollingCounter`. If tests enable the counter and then immediately modify the state that's being polled by the counter, that modification might now occur prior to the first time the callback is invoked (and go unnoticed).

## Previous behavior

Previously, when an `IncrementingPollingCounter` was enabled, the first invocation of the callback might have occurred synchronously on the thread that performed the enable operation.

This sample app calls the delegate `() => SomeInterestingValue` on the `Main` thread within the call to `EnableEvents()`. That callback will observe `log.SomeInterestingValue` is 0. A later call from a dedicated timer thread will observe `log.SomeInterestingValue` changed to 1, and an event will be sent with `Increment value = 1`.

```csharp
using System.Diagnostics.Tracing;

var log = MyEventSource.Log;
using var listener = new Listener();

log.SomeInterestingValue++;

Console.ReadKey();

class MyEventSource : EventSource
{
public static MyEventSource Log { get; } = new();
private IncrementingPollingCounter? _counter;
public int SomeInterestingValue;

private MyEventSource() : base(nameof(MyEventSource))
{
_counter = new IncrementingPollingCounter("counter", this, () => SomeInterestingValue);
}
}

class Listener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == nameof(MyEventSource))
{
EnableEvents(eventSource, EventLevel.Informational, EventKeywords.None,
new Dictionary<string, string?> { { "EventCounterIntervalSec", "1.0" } });
}
}

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (eventData.EventSource.Name == "EventCounters")
{
var counters = (IDictionary<string, object>)eventData.Payload![0]!;
Console.WriteLine($"Increment: {counters["Increment"]}");
}
}
}
```

## New behavior

Using the same code snippet as the [Previous behavior](#previous-behavior) section, the first invocation of the callback occurs asynchronously on the timer thread. It might or might not occur prior to the `Main` thread running `log.SomeInterestingValue++` depending on how the OS schedules multiple threads.

Depending on that timing, the app either outputs "Increment=0" or "Increment=1".

## Version introduced

.NET 9 RC 1

## Type of breaking change

This change is a [behavioral change](../../categories.md#behavioral-change).

## Reason for change

The change was made to resolve a potential deadlock that can occur running callback functions while the `EventListener` lock is held.

## Recommended action

No action is required for scenarios that use `IncrementingPollingCounters` to visualize metrics in external monitoring tools. These scenarios should continue to work normally.

For scenarios that do in-process testing or other consumption of counter data via `EventListener`, check if your code expects to observe a specific modification to the counter value made on the same thread that called `EnableEvents()`. If it does, we recommend waiting to observe at least one counter event from the `EventListener`, then modifying the counter value. For example, to ensure that the [example code snippet](#previous-behavior) prints "Increment=1", you could add a `ManualResetEvent` to the `EventListener`, signal it when the first counter event is received, and wait for it prior to calling `log.SomeInterestingValue++`.

## Affected APIs

- <xref:System.Diagnostics.Tracing.IncrementingPollingCounter?displayProperty=fullName>
42 changes: 42 additions & 0 deletions docs/core/compatibility/core-libraries/9.0/non-keyed-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: FromKeyedServicesAttribute no longer injects non-keyed parameter
description: Learn about the .NET 9 breaking change in core .NET libraries (dependency injection) where FromKeyedServicesAttribute no longer injects a non-keyed parameter.
ms.date: 09/05/2024
---
# FromKeyedServicesAttribute no longer injects non-keyed parameter

When you use <xref:Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute> to specify a keyed service to be injected, an incorrect service might be passed.

## Previous behavior

Previously, when a keyed service was intended to be injected as a parameter in a service constructor by using <xref:Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute> and the corresponding keyed service (`service1` in the following example) wasn't registered as a keyed service but *was* registered as a non-keyed service type (`IService` in the following example), the non-keyed service was injected instead of throwing an exception.

```csharp
public MyService([FromKeyedServices("service1")] IService service1, ...
```

## New behavior

Starting in .NET 9, an <xref:System.InvalidOperationException> is thrown when <xref:Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute> is used and the specified keyed service isn't found. This behavior is consistent with other cases when the requested service can't be found due to lack of registration.

## Version introduced

.NET 9 RC 1 and 8.0.9 servicing

## Type of breaking change

This change is a [behavioral change](../../categories.md#behavioral-change).

## Reason for change

This change adds missing validation logic to detect service misconfiguration bugs. This issue existed when the keyed service feature was added in v8.0.

## Recommended action

If `FromKeyedServicesAttribute` is used, ensure that the corresponding service is registered as a keyed service, such as by using `IServiceCollection.AddKeyedScoped()`, `IServiceCollection.AddKeyedSingleton()`, or `IServiceCollection.AddKeyedTransient()`.

The fix has also been backported to .NET 8.0.9, so both .NET 8 and .NET 9 have the same behavior. If your application depends on the old behavior, a feature switch was added for .NET 8.0.9 (but not .NET 9) named `Microsoft.Extensions.DependencyInjection.AllowNonKeyedServiceInject`. Set the switch to `true` to keep the old behavior.

## Affected APIs

- <xref:Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute.%23ctor(System.Object)>
Loading