@@ -39,6 +39,32 @@ public class DurableTaskSourceGenerator : IIncrementalGenerator
3939 * }
4040 */
4141
42+ /// <summary>
43+ /// Diagnostic ID for invalid task names.
44+ /// </summary>
45+ const string InvalidTaskNameDiagnosticId = "DURABLE1001" ;
46+
47+ /// <summary>
48+ /// Diagnostic ID for invalid event names.
49+ /// </summary>
50+ const string InvalidEventNameDiagnosticId = "DURABLE1002" ;
51+
52+ static readonly DiagnosticDescriptor InvalidTaskNameRule = new (
53+ InvalidTaskNameDiagnosticId ,
54+ title : "Invalid task name" ,
55+ messageFormat : "The task name '{0}' is not a valid C# identifier. Task names must start with a letter or underscore and contain only letters, digits, and underscores." ,
56+ category : "DurableTask.Design" ,
57+ DiagnosticSeverity . Error ,
58+ isEnabledByDefault : true ) ;
59+
60+ static readonly DiagnosticDescriptor InvalidEventNameRule = new (
61+ InvalidEventNameDiagnosticId ,
62+ title : "Invalid event name" ,
63+ messageFormat : "The event name '{0}' is not a valid C# identifier. Event names must start with a letter or underscore and contain only letters, digits, and underscores." ,
64+ category : "DurableTask.Design" ,
65+ DiagnosticSeverity . Error ,
66+ isEnabledByDefault : true ) ;
67+
4268 /// <inheritdoc/>
4369 public void Initialize ( IncrementalGeneratorInitializationContext context )
4470 {
@@ -166,13 +192,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
166192 ITypeSymbol ? outputType = kind == DurableTaskKind . Entity ? null : taskType . TypeArguments . Last ( ) ;
167193
168194 string taskName = classType . Name ;
195+ Location ? taskNameLocation = null ;
169196 if ( attribute . ArgumentList ? . Arguments . Count > 0 )
170197 {
171198 ExpressionSyntax expression = attribute . ArgumentList . Arguments [ 0 ] . Expression ;
172199 taskName = context . SemanticModel . GetConstantValue ( expression ) . ToString ( ) ;
200+ taskNameLocation = expression . GetLocation ( ) ;
173201 }
174202
175- return new DurableTaskTypeInfo ( className , taskName , inputType , outputType , kind ) ;
203+ return new DurableTaskTypeInfo ( className , taskName , inputType , outputType , kind , taskNameLocation ) ;
176204 }
177205
178206 static DurableEventTypeInfo ? GetDurableEventTypeInfo ( GeneratorSyntaxContext context )
@@ -204,6 +232,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
204232 }
205233
206234 string eventName = eventType . Name ;
235+ Location ? eventNameLocation = null ;
207236
208237 if ( attribute . ArgumentList ? . Arguments . Count > 0 )
209238 {
@@ -212,10 +241,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
212241 if ( constantValue . HasValue && constantValue . Value is string value )
213242 {
214243 eventName = value ;
244+ eventNameLocation = expression . GetLocation ( ) ;
215245 }
216246 }
217247
218- return new DurableEventTypeInfo ( eventName , eventType ) ;
248+ return new DurableEventTypeInfo ( eventName , eventType , eventNameLocation ) ;
219249 }
220250
221251 static DurableFunction ? GetDurableFunction ( GeneratorSyntaxContext context )
@@ -230,6 +260,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
230260 return null ;
231261 }
232262
263+ /// <summary>
264+ /// Checks if a name is a valid C# identifier.
265+ /// </summary>
266+ /// <param name="name">The name to validate.</param>
267+ /// <returns>True if the name is a valid C# identifier, false otherwise.</returns>
268+ static bool IsValidCSharpIdentifier ( string name )
269+ {
270+ if ( string . IsNullOrEmpty ( name ) )
271+ {
272+ return false ;
273+ }
274+
275+ // Use Roslyn's built-in identifier validation
276+ return SyntaxFacts . IsValidIdentifier ( name ) ;
277+ }
278+
233279 static void Execute (
234280 SourceProductionContext context ,
235281 Compilation compilation ,
@@ -242,18 +288,47 @@ static void Execute(
242288 return ;
243289 }
244290
291+ // Validate task names and report diagnostics for invalid identifiers
292+ foreach ( DurableTaskTypeInfo task in allTasks )
293+ {
294+ if ( ! IsValidCSharpIdentifier ( task . TaskName ) )
295+ {
296+ Location location = task . TaskNameLocation ?? Location . None ;
297+ Diagnostic diagnostic = Diagnostic . Create ( InvalidTaskNameRule , location , task . TaskName ) ;
298+ context . ReportDiagnostic ( diagnostic ) ;
299+ }
300+ }
301+
302+ // Validate event names and report diagnostics for invalid identifiers
303+ foreach ( DurableEventTypeInfo eventInfo in allEvents )
304+ {
305+ if ( ! IsValidCSharpIdentifier ( eventInfo . EventName ) )
306+ {
307+ Location location = eventInfo . EventNameLocation ?? Location . None ;
308+ Diagnostic diagnostic = Diagnostic . Create ( InvalidEventNameRule , location , eventInfo . EventName ) ;
309+ context . ReportDiagnostic ( diagnostic ) ;
310+ }
311+ }
312+
245313 // This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
246314 // code if we find the Durable Functions extension listed in the set of referenced assembly names.
247315 bool isDurableFunctions = compilation . ReferencedAssemblyNames . Any (
248316 assembly => assembly . Name . Equals ( "Microsoft.Azure.Functions.Worker.Extensions.DurableTask" , StringComparison . OrdinalIgnoreCase ) ) ;
249317
250318 // Separate tasks into orchestrators, activities, and entities
319+ // Skip tasks with invalid names to avoid generating invalid code
251320 List < DurableTaskTypeInfo > orchestrators = new ( ) ;
252321 List < DurableTaskTypeInfo > activities = new ( ) ;
253322 List < DurableTaskTypeInfo > entities = new ( ) ;
254323
255324 foreach ( DurableTaskTypeInfo task in allTasks )
256325 {
326+ // Skip tasks with invalid names
327+ if ( ! IsValidCSharpIdentifier ( task . TaskName ) )
328+ {
329+ continue ;
330+ }
331+
257332 if ( task . IsActivity )
258333 {
259334 activities . Add ( task ) ;
@@ -268,7 +343,17 @@ static void Execute(
268343 }
269344 }
270345
271- int found = activities . Count + orchestrators . Count + entities . Count + allEvents . Length + allFunctions . Length ;
346+ // Filter out events with invalid names
347+ List < DurableEventTypeInfo > validEvents = new ( ) ;
348+ foreach ( DurableEventTypeInfo eventInfo in allEvents )
349+ {
350+ if ( IsValidCSharpIdentifier ( eventInfo . EventName ) )
351+ {
352+ validEvents . Add ( eventInfo ) ;
353+ }
354+ }
355+
356+ int found = activities . Count + orchestrators . Count + entities . Count + validEvents . Count + allFunctions . Length ;
272357 if ( found == 0 )
273358 {
274359 return ;
@@ -347,7 +432,7 @@ public static class GeneratedDurableTaskExtensions
347432 }
348433
349434 // Generate WaitFor{EventName}Async methods for each event type
350- foreach ( DurableEventTypeInfo eventInfo in allEvents )
435+ foreach ( DurableEventTypeInfo eventInfo in validEvents )
351436 {
352437 AddEventWaitMethod ( sourceBuilder , eventInfo ) ;
353438 AddEventSendMethod ( sourceBuilder , eventInfo ) ;
@@ -573,11 +658,13 @@ public DurableTaskTypeInfo(
573658 string taskName ,
574659 ITypeSymbol ? inputType ,
575660 ITypeSymbol ? outputType ,
576- DurableTaskKind kind )
661+ DurableTaskKind kind ,
662+ Location ? taskNameLocation = null )
577663 {
578664 this . TypeName = taskType ;
579665 this . TaskName = taskName ;
580666 this . Kind = kind ;
667+ this . TaskNameLocation = taskNameLocation ;
581668
582669 // Entities only have a state type parameter, not input/output
583670 if ( kind == DurableTaskKind . Entity )
@@ -605,6 +692,7 @@ public DurableTaskTypeInfo(
605692 public string InputParameter { get ; }
606693 public string OutputType { get ; }
607694 public DurableTaskKind Kind { get ; }
695+ public Location ? TaskNameLocation { get ; }
608696
609697 public bool IsActivity => this . Kind == DurableTaskKind . Activity ;
610698
@@ -632,14 +720,16 @@ static string GetRenderedTypeExpression(ITypeSymbol? symbol)
632720
633721 class DurableEventTypeInfo
634722 {
635- public DurableEventTypeInfo ( string eventName , ITypeSymbol eventType )
723+ public DurableEventTypeInfo ( string eventName , ITypeSymbol eventType , Location ? eventNameLocation = null )
636724 {
637725 this . TypeName = GetRenderedTypeExpression ( eventType ) ;
638726 this . EventName = eventName ;
727+ this . EventNameLocation = eventNameLocation ;
639728 }
640729
641730 public string TypeName { get ; }
642731 public string EventName { get ; }
732+ public Location ? EventNameLocation { get ; }
643733
644734 static string GetRenderedTypeExpression ( ITypeSymbol ? symbol )
645735 {
0 commit comments