diff --git a/ReadMe.md b/ReadMe.md
index 56daeda..a938596 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -40,6 +40,7 @@ Add the following nuget package to you project: https://www.nuget.org/packages/E
| [EPC30](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/EPC30.md) | Method calls itself recursively |
| [ERP041](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/ERP041.md) | EventSource class should be sealed |
| [ERP042](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/ERP042.md) | EventSource implementation is not correct |
+| [ERP043](https://github.com/SergeyTeplyakov/ErrorProne.NET/tree/master/docs/Rules/ERP043.md) | EventSource event has an empty Message |
### Concurrency
diff --git a/docs/Rules/ERP043.md b/docs/Rules/ERP043.md
new file mode 100644
index 0000000..276d92d
--- /dev/null
+++ b/docs/Rules/ERP043.md
@@ -0,0 +1,63 @@
+# ERP043 - EventSource event has an empty Message
+
+This analyzer warns when an `[Event(...)]` attribute on an `EventSource`-derived class sets its
+`Message` property to the **empty string**.
+
+## Code that triggers the analyzer
+
+```csharp
+using System.Diagnostics.Tracing;
+
+[EventSource(Name = "MyEventSource")]
+public sealed class MyEventSource : EventSource
+{
+ // ❌ ERP043 - empty Message
+ [Event(1, Message = "")]
+ public void AppStarted(string name) => WriteEvent(1, name);
+}
+```
+
+## Why an empty message is a bug
+
+`EventAttribute.Message` is documented as an *optional* format string (it uses `{0}`, `{1}` payload
+substitution). Setting it to `""` makes the runtime emit an empty `` entry in the ETW
+manifest string table.
+
+On **Windows Server 2025** the `TdhLoadManifest()` API no longer tolerates such entries. Older Windows
+silently appended a space to empty strings so they passed manifest validation; Windows Server 2025 removed
+that behavior, so loading the manifest now fails. A **single** bad entry drops the **entire** manifest — the
+provider can no longer be subscribed and **all** of its events stop flowing, with no error surfaced to the
+process. Windows Server 2019/2022 are not affected.
+
+`EventAttribute.Message` has no documented rule forbidding the empty string, and the runtime does not throw,
+so this undocumented-but-real constraint is exactly the kind of thing an analyzer should encode. Because
+.NET Framework will not fix this in the runtime, a compile-time analyzer is the most reliable guard.
+
+## When it triggers
+
+- **Only** on a compile-time constant empty string (`Message = ""`, or a `const string` equal to `""`).
+- `Message = " "` (single space), any non-empty message, `null`, and a missing `Message` are all fine.
+- Only for types deriving directly or indirectly from `System.Diagnostics.Tracing.EventSource`.
+
+## How to fix
+
+Two code fixes are offered:
+
+Both fixes also work for empty constant expressions and support Fix All. They change only the
+attribute argument, not the declaration of a referenced constant.
+
+1. **Remove Message argument** (preferred):
+ ```csharp
+ [Event(1, Level = EventLevel.Error, Message = "")] // before
+ [Event(1, Level = EventLevel.Error)] // after
+ ```
+2. **Use a single space**:
+ ```csharp
+ [Event(1, Message = "")] // before
+ [Event(1, Message = " ")] // after
+ ```
+
+## See also
+
+- [ERP042 - EventSource implementation is not correct](ERP042.md)
+- [`EventAttribute.Message`](https://learn.microsoft.com/dotnet/api/system.diagnostics.tracing.eventattribute.message)
diff --git a/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/EventSourceAnalysis/EmptyEventMessageCodeFixProvider.cs b/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/EventSourceAnalysis/EmptyEventMessageCodeFixProvider.cs
new file mode 100644
index 0000000..e58c039
--- /dev/null
+++ b/src/ErrorProne.NET.CoreAnalyzers.CodeFixes/EventSourceAnalysis/EmptyEventMessageCodeFixProvider.cs
@@ -0,0 +1,123 @@
+using System.Collections.Immutable;
+using System.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace ErrorProne.NET.EventSourceAnalysis
+{
+ ///
+ /// A fixer for an empty Message on an [Event] attribute reported by .
+ ///
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EmptyEventMessageCodeFixProvider)), Shared]
+ public class EmptyEventMessageCodeFixProvider : CodeFixProvider
+ {
+ public const string RemoveMessageTitle = "Remove Message argument";
+ public const string UseSingleSpaceTitle = "Use a single space";
+
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(EmptyEventMessageAnalyzer.Rule.Id);
+
+ ///
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
+ if (semanticModel is null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var messageArg = root.FindNode(diagnostic.Location.SourceSpan)
+ .FirstAncestorOrSelf();
+
+ if (!IsEmptyMessageArgument(messageArg, semanticModel, context.CancellationToken))
+ {
+ continue;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: RemoveMessageTitle,
+ createChangedDocument: c => RemoveMessageArgumentAsync(context.Document, diagnostic.Location, c),
+ equivalenceKey: RemoveMessageTitle),
+ diagnostic);
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: UseSingleSpaceTitle,
+ createChangedDocument: c => UseSingleSpaceAsync(context.Document, diagnostic.Location, c),
+ equivalenceKey: UseSingleSpaceTitle),
+ diagnostic);
+ }
+ }
+
+ private static bool IsEmptyMessageArgument(
+ AttributeArgumentSyntax? argument, SemanticModel semanticModel, CancellationToken cancellationToken)
+ {
+ if (argument?.NameEquals?.Name.Identifier.ValueText != "Message")
+ {
+ return false;
+ }
+
+ var constant = semanticModel.GetConstantValue(argument.Expression, cancellationToken);
+ return constant.HasValue && constant.Value is string message && message.Length == 0;
+ }
+
+ private static async Task RemoveMessageArgumentAsync(Document document, Location location, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return document;
+ }
+
+ var messageArg = root.FindNode(location.SourceSpan).FirstAncestorOrSelf();
+ if (messageArg is null)
+ {
+ return document;
+ }
+
+ // RemoveNode handles the associated comma separator so we don't leave a dangling/leading comma.
+ var newRoot = root.RemoveNode(messageArg, SyntaxRemoveOptions.KeepNoTrivia);
+ return newRoot is null ? document : document.WithSyntaxRoot(newRoot);
+ }
+
+ private static async Task UseSingleSpaceAsync(Document document, Location location, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return document;
+ }
+
+ var messageArg = root.FindNode(location.SourceSpan).FirstAncestorOrSelf();
+ if (messageArg is null)
+ {
+ return document;
+ }
+
+ var newLiteral = SyntaxFactory.LiteralExpression(
+ SyntaxKind.StringLiteralExpression,
+ SyntaxFactory.Literal(" "))
+ .WithTriviaFrom(messageArg.Expression);
+
+ var newRoot = root.ReplaceNode(messageArg.Expression, newLiteral);
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageAnalyzerTests.cs
new file mode 100644
index 0000000..f3e2ff9
--- /dev/null
+++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageAnalyzerTests.cs
@@ -0,0 +1,175 @@
+using NUnit.Framework;
+using System.Threading.Tasks;
+using VerifyCS = ErrorProne.NET.TestHelpers.CSharpCodeFixVerifier<
+ ErrorProne.NET.EventSourceAnalysis.EmptyEventMessageAnalyzer,
+ Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
+
+namespace ErrorProne.NET.CoreAnalyzers.Tests.AsyncAnalyzers
+{
+ [TestFixture]
+ public class EmptyEventMessageAnalyzerTests
+ {
+ [Test]
+ public async Task Warn_On_Indirect_EventSource_Subclass()
+ {
+ string code = @"
+public abstract class BaseEventSource : System.Diagnostics.Tracing.EventSource { }
+
+public sealed class DemoEventSource : BaseEventSource
+{
+ [System.Diagnostics.Tracing.Event(1, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Null_Message()
+ {
+ string code = @"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = null)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Unrelated_Event_Attribute()
+ {
+ string code = @"
+public class EventAttribute : System.Attribute
+{
+ public EventAttribute(int id) { }
+ public string Message { get; set; }
+}
+
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [Event(1, Message = """")]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task Warn_On_Empty_Message()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task Warn_On_Empty_Message_With_Other_Named_Args()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Level = System.Diagnostics.Tracing.EventLevel.Error, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task Warn_On_Empty_Message_From_Const()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ private const string EmptyConst = """";
+
+ [System.Diagnostics.Tracing.Event(1, [|Message = EmptyConst|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Missing_Message()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Single_Space_Message()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = "" "")]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Non_Empty_Message()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = ""Started {0}"")]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Non_Empty_Const_Message()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ private const string Msg = ""Started"";
+
+ [System.Diagnostics.Tracing.Event(1, Message = Msg)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
+ [Test]
+ public async Task No_Warn_On_Empty_Message_In_Non_EventSource_Class()
+ {
+ // The diagnostic must only fire for types deriving from EventSource.
+ string code = @"
+public sealed class NotAnEventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = """")]
+ public void AppStarted(string m) {}
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+ }
+}
diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageCodeFixProviderTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageCodeFixProviderTests.cs
new file mode 100644
index 0000000..be59d7b
--- /dev/null
+++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EmptyEventMessageCodeFixProviderTests.cs
@@ -0,0 +1,182 @@
+using NUnit.Framework;
+using System.Threading.Tasks;
+using ErrorProne.NET.EventSourceAnalysis;
+using ErrorProne.NET.TestHelpers;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+using VerifyCS = ErrorProne.NET.TestHelpers.CSharpCodeFixVerifier<
+ ErrorProne.NET.EventSourceAnalysis.EmptyEventMessageAnalyzer,
+ ErrorProne.NET.EventSourceAnalysis.EmptyEventMessageCodeFixProvider>;
+
+namespace ErrorProne.NET.CoreAnalyzers.Tests.AsyncAnalyzers
+{
+ [TestFixture]
+ public class EmptyEventMessageCodeFixProviderTests
+ {
+ private static async Task VerifyFixAsync(string code, string fixedCode, string equivalenceKey)
+ {
+ var test = new VerifyCS.Test
+ {
+ TestCode = code,
+ FixedCode = fixedCode,
+ LanguageVersion = LanguageVersion.Latest,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ CodeActionEquivalenceKey = equivalenceKey,
+ };
+
+ await test.WithoutGeneratedCodeVerification().RunAsync();
+ }
+
+ [Test]
+ public async Task Fix_Empty_Constant_Expressions(
+ [Values("EmptyConst", "(EmptyConst)", "EmptyConst + \"\"", "$\"{EmptyConst}\"")] string expression,
+ [Values("Message", "@Message", "\\u004dessage")] string argumentName,
+ [Values(EmptyEventMessageCodeFixProvider.RemoveMessageTitle, EmptyEventMessageCodeFixProvider.UseSingleSpaceTitle)] string equivalenceKey)
+ {
+ string code = $@"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{{
+ private const string EmptyConst = """";
+
+ [System.Diagnostics.Tracing.Event(1, [|{argumentName} = {expression}|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}}";
+
+ var fixedArgument = equivalenceKey == EmptyEventMessageCodeFixProvider.RemoveMessageTitle
+ ? ""
+ : $", {argumentName} = \" \"";
+ string expected = $@"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{{
+ private const string EmptyConst = """";
+
+ [System.Diagnostics.Tracing.Event(1{fixedArgument})]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}}";
+
+ await VerifyFixAsync(code, expected, equivalenceKey);
+ }
+
+ [Test]
+ public async Task Fix_Multiple_Events(
+ [Values(EmptyEventMessageCodeFixProvider.RemoveMessageTitle, EmptyEventMessageCodeFixProvider.UseSingleSpaceTitle)] string equivalenceKey)
+ {
+ string code = @"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ private const string EmptyConst = """";
+
+ [System.Diagnostics.Tracing.Event(1, [|Message = EmptyConst|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+
+ [System.Diagnostics.Tracing.Event(2, [|Message = """"|])]
+ public void AppStopped(string m) => WriteEvent(2, m);
+}";
+
+ var fixedArgument = equivalenceKey == EmptyEventMessageCodeFixProvider.RemoveMessageTitle
+ ? ""
+ : ", Message = \" \"";
+ string expected = $@"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{{
+ private const string EmptyConst = """";
+
+ [System.Diagnostics.Tracing.Event(1{fixedArgument})]
+ public void AppStarted(string m) => WriteEvent(1, m);
+
+ [System.Diagnostics.Tracing.Event(2{fixedArgument})]
+ public void AppStopped(string m) => WriteEvent(2, m);
+}}";
+
+ await VerifyFixAsync(code, expected, equivalenceKey);
+ }
+
+ [Test]
+ public async Task Remove_Message_Argument_When_Last()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Level = System.Diagnostics.Tracing.EventLevel.Error, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ string expected = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Level = System.Diagnostics.Tracing.EventLevel.Error)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyFixAsync(code, expected, EmptyEventMessageCodeFixProvider.RemoveMessageTitle);
+ }
+
+ [Test]
+ public async Task Remove_Message_Argument_When_Only_Named_Arg()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ string expected = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyFixAsync(code, expected, EmptyEventMessageCodeFixProvider.RemoveMessageTitle);
+ }
+
+ [Test]
+ public async Task Remove_Message_Argument_When_Middle()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, [|Message = """"|], Level = System.Diagnostics.Tracing.EventLevel.Error)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ string expected = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Level = System.Diagnostics.Tracing.EventLevel.Error)]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyFixAsync(code, expected, EmptyEventMessageCodeFixProvider.RemoveMessageTitle);
+ }
+
+ [Test]
+ public async Task Use_Single_Space()
+ {
+ string code = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, [|Message = """"|])]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ string expected = @"
+[System.Diagnostics.Tracing.EventSource(Name = ""Demo"")]
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = "" "")]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyFixAsync(code, expected, EmptyEventMessageCodeFixProvider.UseSingleSpaceTitle);
+ }
+ }
+}
diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EventSourceAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EventSourceAnalyzerTests.cs
index 1141b2d..0262fa0 100644
--- a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EventSourceAnalyzerTests.cs
+++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/EventSourceAnalyzerTests.cs
@@ -9,6 +9,19 @@ namespace ErrorProne.NET.CoreAnalyzers.Tests.AsyncAnalyzers
[TestFixture]
public class EventSourceAnalyzerTests
{
+ [Test]
+ public async Task Empty_Message_Is_Not_Reported_As_ERP042()
+ {
+ string code = @"
+public sealed class DemoEventSource : System.Diagnostics.Tracing.EventSource
+{
+ [System.Diagnostics.Tracing.Event(1, Message = """")]
+ public void AppStarted(string m) => WriteEvent(1, m);
+}";
+
+ await VerifyCS.VerifyAsync(code);
+ }
+
[Test]
public async Task Warn_On_Id_Mismatch()
{
diff --git a/src/ErrorProne.NET.CoreAnalyzers/AnalyzerReleases.Unshipped.md b/src/ErrorProne.NET.CoreAnalyzers/AnalyzerReleases.Unshipped.md
index 6826c2b..36e5b56 100644
--- a/src/ErrorProne.NET.CoreAnalyzers/AnalyzerReleases.Unshipped.md
+++ b/src/ErrorProne.NET.CoreAnalyzers/AnalyzerReleases.Unshipped.md
@@ -39,3 +39,4 @@ ERP022 | ErrorHandling | Warning | SwallowAllExceptionsAnalyzer
ERP031 | Concurrency | Warning | ConcurrentCollectionAnalyzer
ERP041 | CodeSmell | Info | EventSourceSealedAnalyzer
ERP042 | CodeSmell | Warning | EventSourceAnalyzer
+ERP043 | CodeSmell | Warning | EmptyEventMessageAnalyzer
diff --git a/src/ErrorProne.NET.CoreAnalyzers/DiagnosticDescriptors.cs b/src/ErrorProne.NET.CoreAnalyzers/DiagnosticDescriptors.cs
index cf56702..56e755f 100644
--- a/src/ErrorProne.NET.CoreAnalyzers/DiagnosticDescriptors.cs
+++ b/src/ErrorProne.NET.CoreAnalyzers/DiagnosticDescriptors.cs
@@ -279,6 +279,16 @@ internal static class DiagnosticDescriptors
description: "The event source implementation must follow special rules to avoid hitting runtime errors.",
helpLinkUri: GetHelpUri(nameof(ERP042)));
+ ///
+ public static readonly DiagnosticDescriptor ERP043 = new DiagnosticDescriptor(
+ nameof(ERP043),
+ title: "EventSource event has an empty Message",
+ messageFormat: "{0}: {1}",
+ CodeSmellCategory, DiagnosticSeverity.Warning, isEnabledByDefault: true,
+ description: "An empty [Event(Message = \"\")] produces an invalid ETW manifest that fails to load " +
+ "on Windows Server 2025, silently dropping the whole provider.",
+ helpLinkUri: GetHelpUri(nameof(ERP043)));
+
///
public static readonly DiagnosticDescriptor EPC35 = new DiagnosticDescriptor(
"EPC35",
diff --git a/src/ErrorProne.NET.CoreAnalyzers/EventSourceAnalysis/EmptyEventMessageAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/EventSourceAnalysis/EmptyEventMessageAnalyzer.cs
new file mode 100644
index 0000000..af30a74
--- /dev/null
+++ b/src/ErrorProne.NET.CoreAnalyzers/EventSourceAnalysis/EmptyEventMessageAnalyzer.cs
@@ -0,0 +1,83 @@
+using System.Diagnostics.Tracing;
+using System.Linq;
+using ErrorProne.NET.Core;
+using ErrorProne.NET.CoreAnalyzers;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace ErrorProne.NET.EventSourceAnalysis
+{
+ ///
+ /// An analyzer that warns when an [Event(...)] attribute sets Message to an empty string.
+ ///
+ ///
+ /// An empty Message makes the runtime emit an empty ETW manifest string-table entry
+ /// (<string value=""/>). On Windows Server 2025 the TdhLoadManifest() API no longer
+ /// tolerates such entries, so loading the manifest fails and the entire provider is silently dropped.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public sealed class EmptyEventMessageAnalyzer : DiagnosticAnalyzerBase
+ {
+ ///
+ public static DiagnosticDescriptor Rule => DiagnosticDescriptors.ERP043;
+
+ ///
+ public EmptyEventMessageAnalyzer()
+ : base(Rule)
+ {
+ }
+
+ ///
+ protected override void InitializeCore(AnalysisContext context)
+ {
+ context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+ }
+
+ private void AnalyzeMethod(SymbolAnalysisContext context)
+ {
+ var method = (IMethodSymbol)context.Symbol;
+ if (!method.ContainingType.BaseType.EnumerateBaseTypesAndSelf()
+ .Any(type => type.IsClrType(context.Compilation, typeof(EventSource))))
+ {
+ return;
+ }
+
+ var eventAttribute = method.GetAttributes()
+ .FirstOrDefault(a => a.AttributeClass?.IsClrType(context.Compilation, typeof(EventAttribute)) == true);
+ if (eventAttribute is null)
+ {
+ return;
+ }
+
+ var messageArg = eventAttribute.NamedArguments.FirstOrDefault(kvp => kvp.Key == "Message");
+
+ // Only trigger on a compile-time constant empty string. A missing 'Message', a single space,
+ // or any non-empty message is fine.
+ if (messageArg.Key != "Message" ||
+ messageArg.Value.Value is not string message ||
+ message.Length != 0)
+ {
+ return;
+ }
+
+ // Prefer reporting on the 'Message = ""' argument syntax so the fixer can target just that argument.
+ var attributeSyntax = eventAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) as AttributeSyntax;
+ var messageArgSyntax = attributeSyntax?.ArgumentList?.Arguments
+ .FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.ValueText == "Message");
+
+ var location = messageArgSyntax?.GetLocation()
+ ?? method.TryGetDeclarationSyntax()?.Identifier.GetLocation();
+ if (location is null)
+ {
+ return;
+ }
+
+ var error =
+ $"Event {method.Name} has an empty Message. An empty Message emits an empty ETW manifest " +
+ "string-table entry that fails to load on Windows Server 2025 and drops the entire provider. " +
+ "Remove the Message argument or set it to a single space.";
+ context.ReportDiagnostic(Diagnostic.Create(Rule, location, method.ContainingType.Name, error));
+ }
+ }
+}