@@ -13,11 +13,20 @@ public static class Extensions
1313
1414 public static WebApplication AddHubDocs ( this WebApplication app , params Assembly [ ] additionalAssemblies )
1515 {
16+ return AddHubDocs ( app , _ => { } , additionalAssemblies ) ;
17+ }
18+
19+ public static WebApplication AddHubDocs ( this WebApplication app , Action < HubDocsDocumentOptions > configureDocument , params Assembly [ ] additionalAssemblies )
20+ {
21+ var documentOptions = new HubDocsDocumentOptions ( ) ;
22+ configureDocument ( documentOptions ) ;
23+
1624 app . MapGet ( "/hubdocs/hubdocs.json" , ( ) =>
1725 {
1826 var hubRoutes = GetHubRoutesFromEndpoints ( app ) ;
19- var metadata = DiscoverSignalRHubs ( hubRoutes , additionalAssemblies ) ;
20- return Results . Ok ( metadata ) ;
27+ var metadata = DiscoverSignalRHubs ( hubRoutes , additionalAssemblies ) . ToList ( ) ;
28+ var hubDocsDocument = BuildHubDocsDocument ( metadata , documentOptions ) ;
29+ return Results . Ok ( hubDocsDocument ) ;
2130 } )
2231 . ExcludeFromDescription ( ) ;
2332
@@ -439,6 +448,279 @@ private static string BuildReturnExample(MethodInfo method)
439448 return CreateExampleLiteral ( unwrapped , false ) ;
440449 }
441450
451+ private static Dictionary < string , object ? > BuildHubDocsDocument ( IReadOnlyList < HubMetadata > hubs )
452+ {
453+ return BuildHubDocsDocument ( hubs , new HubDocsDocumentOptions ( ) ) ;
454+ }
455+
456+ private static Dictionary < string , object ? > BuildHubDocsDocument ( IReadOnlyList < HubMetadata > hubs , HubDocsDocumentOptions options )
457+ {
458+ var channels = new Dictionary < string , object ? > ( ) ;
459+ var messages = new Dictionary < string , object ? > ( ) ;
460+ var schemas = new Dictionary < string , object ? > ( ) ;
461+
462+ foreach ( var hub in hubs )
463+ {
464+ if ( string . IsNullOrWhiteSpace ( hub . Path ) )
465+ continue ;
466+
467+ foreach ( var schema in hub . Schemas )
468+ {
469+ if ( schemas . ContainsKey ( schema . Name ) )
470+ continue ;
471+
472+ schemas [ schema . Name ] = ConvertHubSchemaToProtocolSchema ( schema ) ;
473+ }
474+
475+ var publishMessageRefs = new List < object > ( ) ;
476+ foreach ( var method in hub . Methods )
477+ {
478+ var messageName = $ "{ hub . HubName } .{ method . MethodName } .Request";
479+ messages [ messageName ] = BuildMethodMessage ( messageName , method , schemas . Keys ) ;
480+ publishMessageRefs . Add ( new Dictionary < string , object ? >
481+ {
482+ [ "$ref" ] = $ "#/components/messages/{ messageName } "
483+ } ) ;
484+ }
485+
486+ var subscribeMessageRefs = new List < object > ( ) ;
487+ foreach ( var method in hub . ClientMethods ?? [ ] )
488+ {
489+ var messageName = $ "{ hub . HubName } .{ method . MethodName } .Event";
490+ messages [ messageName ] = BuildMethodMessage ( messageName , method , schemas . Keys ) ;
491+ subscribeMessageRefs . Add ( new Dictionary < string , object ? >
492+ {
493+ [ "$ref" ] = $ "#/components/messages/{ messageName } "
494+ } ) ;
495+ }
496+
497+ channels [ hub . Path ] = new Dictionary < string , object ? >
498+ {
499+ [ "publish" ] = new Dictionary < string , object ? >
500+ {
501+ [ "operationId" ] = $ "{ hub . HubName } .publish",
502+ [ "summary" ] = $ "Client-to-server methods for { hub . HubName } ",
503+ [ "message" ] = new Dictionary < string , object ? >
504+ {
505+ [ "oneOf" ] = publishMessageRefs
506+ }
507+ } ,
508+ [ "subscribe" ] = new Dictionary < string , object ? >
509+ {
510+ [ "operationId" ] = $ "{ hub . HubName } .subscribe",
511+ [ "summary" ] = $ "Server-to-client methods for { hub . HubName } ",
512+ [ "message" ] = new Dictionary < string , object ? >
513+ {
514+ [ "oneOf" ] = subscribeMessageRefs
515+ }
516+ }
517+ } ;
518+ }
519+
520+ return new Dictionary < string , object ? >
521+ {
522+ [ "hubdocs" ] = new Dictionary < string , object ? >
523+ {
524+ [ "format" ] = "hubdocs-1.0" ,
525+ [ "version" ] = options . Version ,
526+ [ "title" ] = options . Title ,
527+ [ "description" ] = options . Description ,
528+ [ "termsOfService" ] = options . TermsOfService ,
529+ [ "projectUrl" ] = options . ProjectUrl ,
530+ [ "contact" ] = new Dictionary < string , object ? >
531+ {
532+ [ "name" ] = options . Contact . Name ,
533+ [ "email" ] = options . Contact . Email ,
534+ [ "url" ] = options . Contact . Url
535+ } ,
536+ [ "license" ] = new Dictionary < string , object ? >
537+ {
538+ [ "name" ] = options . License . Name ,
539+ [ "url" ] = options . License . Url
540+ } ,
541+ [ "generatedAtUtc" ] = DateTime . UtcNow . ToString ( "O" )
542+ } ,
543+ [ "hubs" ] = hubs ,
544+ [ "channels" ] = channels ,
545+ [ "components" ] = new Dictionary < string , object ? >
546+ {
547+ [ "messages" ] = messages ,
548+ [ "schemas" ] = schemas
549+ }
550+ } ;
551+ }
552+
553+ private static Dictionary < string , object ? > BuildMethodMessage ( string messageName , HubMethodMetadata method , IEnumerable < string > knownSchemas )
554+ {
555+ var argumentProperties = new Dictionary < string , object ? > ( ) ;
556+
557+ foreach ( var parameter in method . Parameters )
558+ {
559+ argumentProperties [ parameter . Name ] = BuildJsonSchemaForType ( parameter . Type , knownSchemas , parameter . Example , parameter . IsNullable ) ;
560+ }
561+
562+ var payloadProperties = new Dictionary < string , object ? >
563+ {
564+ [ "method" ] = new Dictionary < string , object ? >
565+ {
566+ [ "type" ] = "string" ,
567+ [ "example" ] = method . MethodName
568+ } ,
569+ [ "arguments" ] = new Dictionary < string , object ? >
570+ {
571+ [ "type" ] = "object" ,
572+ [ "properties" ] = argumentProperties
573+ } ,
574+ [ "returns" ] = BuildJsonSchemaForType ( method . ReturnType , knownSchemas , method . ReturnExample , false )
575+ } ;
576+
577+ return new Dictionary < string , object ? >
578+ {
579+ [ "name" ] = messageName ,
580+ [ "title" ] = method . Signature ,
581+ [ "payload" ] = new Dictionary < string , object ? >
582+ {
583+ [ "type" ] = "object" ,
584+ [ "properties" ] = payloadProperties ,
585+ [ "required" ] = new [ ] { "method" , "arguments" }
586+ } ,
587+ [ "examples" ] = new [ ]
588+ {
589+ new Dictionary < string , object ? >
590+ {
591+ [ "name" ] = $ "{ method . MethodName } Example",
592+ [ "summary" ] = method . Signature ,
593+ [ "payload" ] = new Dictionary < string , object ? >
594+ {
595+ [ "method" ] = method . MethodName ,
596+ [ "arguments" ] = method . Parameters . ToDictionary ( p => p . Name , p => ( object ? ) p . Example ) ,
597+ [ "returns" ] = method . ReturnExample
598+ }
599+ }
600+ }
601+ } ;
602+ }
603+
604+ private static Dictionary < string , object ? > BuildJsonSchemaForType ( string csharpType , IEnumerable < string > knownSchemas , string ? example , bool nullable )
605+ {
606+ var cleaned = csharpType . Trim ( ) ;
607+ if ( cleaned . EndsWith ( "?" , StringComparison . Ordinal ) )
608+ {
609+ cleaned = cleaned [ ..^ 1 ] ;
610+ nullable = true ;
611+ }
612+
613+ if ( cleaned . EndsWith ( "[]" , StringComparison . Ordinal ) )
614+ {
615+ var itemType = cleaned [ ..^ 2 ] ;
616+ var itemSchema = BuildJsonSchemaForType ( itemType , knownSchemas , null , false ) ;
617+ var arraySchema = new Dictionary < string , object ? >
618+ {
619+ [ "type" ] = "array" ,
620+ [ "items" ] = itemSchema
621+ } ;
622+
623+ if ( example != null )
624+ arraySchema [ "example" ] = example ;
625+
626+ if ( nullable )
627+ arraySchema [ "nullable" ] = true ;
628+
629+ return arraySchema ;
630+ }
631+
632+ if ( cleaned . StartsWith ( "List<" , StringComparison . Ordinal ) && cleaned . EndsWith ( ">" , StringComparison . Ordinal ) )
633+ {
634+ var inner = cleaned [ 5 ..^ 1 ] ;
635+ var itemSchema = BuildJsonSchemaForType ( inner , knownSchemas , null , false ) ;
636+ var listSchema = new Dictionary < string , object ? >
637+ {
638+ [ "type" ] = "array" ,
639+ [ "items" ] = itemSchema
640+ } ;
641+
642+ if ( example != null )
643+ listSchema [ "example" ] = example ;
644+
645+ if ( nullable )
646+ listSchema [ "nullable" ] = true ;
647+
648+ return listSchema ;
649+ }
650+
651+ var schemaName = cleaned . Contains ( '<' )
652+ ? cleaned [ ..cleaned . IndexOf ( '<' ) ]
653+ : cleaned ;
654+
655+ if ( knownSchemas . Contains ( schemaName , StringComparer . Ordinal ) )
656+ {
657+ var refSchema = new Dictionary < string , object ? >
658+ {
659+ [ "$ref" ] = $ "#/components/schemas/{ schemaName } "
660+ } ;
661+
662+ if ( nullable )
663+ refSchema [ "nullable" ] = true ;
664+
665+ return refSchema ;
666+ }
667+
668+ var primitive = MapPrimitiveJsonSchema ( cleaned ) ;
669+ if ( example != null )
670+ primitive [ "example" ] = example ;
671+ if ( nullable )
672+ primitive [ "nullable" ] = true ;
673+ return primitive ;
674+ }
675+
676+ private static Dictionary < string , object ? > MapPrimitiveJsonSchema ( string csharpType )
677+ {
678+ return csharpType switch
679+ {
680+ "bool" => new Dictionary < string , object ? > { [ "type" ] = "boolean" } ,
681+ "byte" or "sbyte" or "short" or "ushort" or "int" or "uint" or "long" or "ulong" =>
682+ new Dictionary < string , object ? > { [ "type" ] = "integer" , [ "format" ] = "int64" } ,
683+ "float" => new Dictionary < string , object ? > { [ "type" ] = "number" , [ "format" ] = "float" } ,
684+ "double" or "decimal" => new Dictionary < string , object ? > { [ "type" ] = "number" , [ "format" ] = "double" } ,
685+ "Guid" => new Dictionary < string , object ? > { [ "type" ] = "string" , [ "format" ] = "uuid" } ,
686+ "DateTime" or "DateTimeOffset" => new Dictionary < string , object ? > { [ "type" ] = "string" , [ "format" ] = "date-time" } ,
687+ "TimeSpan" => new Dictionary < string , object ? > { [ "type" ] = "string" } ,
688+ "Task" or "void" => new Dictionary < string , object ? > { [ "type" ] = "null" } ,
689+ _ => new Dictionary < string , object ? > { [ "type" ] = "string" }
690+ } ;
691+ }
692+
693+ private static Dictionary < string , object ? > ConvertHubSchemaToProtocolSchema ( HubTypeSchemaMetadata schema )
694+ {
695+ if ( schema . Kind == "enum" )
696+ {
697+ var enumNames = ( schema . EnumValues ?? [ ] )
698+ . Select ( v => v . Split ( '=' ) [ 0 ] . Trim ( ) )
699+ . Where ( v => ! string . IsNullOrWhiteSpace ( v ) )
700+ . ToList ( ) ;
701+
702+ return new Dictionary < string , object ? >
703+ {
704+ [ "type" ] = "string" ,
705+ [ "enum" ] = enumNames ,
706+ [ "example" ] = enumNames . FirstOrDefault ( )
707+ } ;
708+ }
709+
710+ var properties = new Dictionary < string , object ? > ( ) ;
711+ foreach ( var property in schema . Properties ?? [ ] )
712+ {
713+ properties [ property . Name ] = BuildJsonSchemaForType ( property . Type , [ ] , property . Example , property . IsNullable ) ;
714+ }
715+
716+ return new Dictionary < string , object ? >
717+ {
718+ [ "type" ] = "object" ,
719+ [ "properties" ] = properties ,
720+ [ "example" ] = schema . Example
721+ } ;
722+ }
723+
442724 private static string CreateExampleLiteral ( Type type , bool nullable )
443725 {
444726 if ( nullable )
0 commit comments