Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion docs/mediator-framework/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ request type. Each transport is opt-in and declared independently:
code-first gRPC method; `[ServiceGroup("Orders")]` groups the service.
- `[RebusMessage(OwnerQueue = "orders")]` — expose as a Rebus message and,
when an owner is declared, contribute a type-based outbound route.
- `[ApiTag("Orders")]` — override the namespace-derived OpenAPI tag and the
gRPC service-group fallback for the contract.
Comment thread
AndreaCuneo marked this conversation as resolved.
Resolved

Generated HTTP endpoints require authorization by default. Set `Policy` on
`[HttpEndpoint]` to select a named policy, or set `AllowAnonymous = true` for
Expand All @@ -80,7 +82,8 @@ Generator contract errors are reported at the transport attribute location:
`ARKMF010` (unknown HTTP verb), `ARKMF011` (unsupported handler kind),
`ARKMF012` (missing route property), `ARKMF013` (invalid HTTP contract shape),
`ARKMF014` (duplicate Rebus registration), and `ARKMF015` (conflicting Rebus
owner queue). Invalid contracts are not emitted.
owner queue), and `ARKMF016` (duplicate OpenAPI operation name within an API
version). Invalid contracts are not emitted.

HTTP binding treats the request as an **envelope** whose members may combine
route, query and body sources (see *HTTP binding* below). These attributes are
Expand Down
2 changes: 1 addition & 1 deletion docs/mediator-framework/progress/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ Each item has a self-contained task document under
[`tasks/`](tasks/README.md); the acceptance criteria live there and are the
authoritative ones.

- [ ] **T12.1** OpenAPI tags and operation names from the contract
- [x] **T12.1** OpenAPI tags and operation names from the contract
([NET-06](tasks/aspnetcore/NET-06-openapi-tags-operation-names.md)).
- *Accept:* every generated operation carries a namespace-derived tag
(overridable with `[ApiTag]`) and a unique, contract-derived `operationId`;
Expand Down
2 changes: 1 addition & 1 deletion docs/mediator-framework/progress/tasks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ recent security commits `8502585`, `fd4d600`, `938567d`, and `c0fc361`.
[ ] [SMP-06](sample-parity/SMP-06-misc-parity.md)
[ ] [NET-01](aspnetcore/NET-01-openapi-xml-docs.md)
7. Scope extension (D8) — wire-shape items first, documentation last:
[ ] [NET-06](aspnetcore/NET-06-openapi-tags-operation-names.md)
[x] [NET-06](aspnetcore/NET-06-openapi-tags-operation-names.md)
[ ] [FW-05](framework/FW-05-standard-problem-responses.md)
[ ] [FW-06](framework/FW-06-async-enumerable-streaming.md)
[ ] [FW-07](framework/FW-07-multifile-uploads.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ See `docs/mediator-framework/design.md` → *OpenAPI operation grouping, naming

## Acceptance

- [ ] `ApiTagAttribute` exists in `Ark.Tools.MediatorFramework` with XML docs.
- [ ] Generated endpoints emit `.WithTags(...)` and `.WithName(...)`; snapshots updated.
- [ ] `operationId` is unique within each versioned document (tested).
- [ ] Duplicate operation names produce a documented generator diagnostic (tested).
- [ ] gRPC service grouping falls back to `[ApiTag]`; explicit `[ServiceGroup]` still wins (tested).
- [ ] `design.md` documents the defaults, the override and the new diagnostic id.
- [ ] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings.
- [ ] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes.
- [x] `ApiTagAttribute` exists in `Ark.Tools.MediatorFramework` with XML docs.
- [x] Generated endpoints emit `.WithTags(...)` and `.WithName(...)`; snapshots updated.
- [x] `operationId` is unique within each versioned document (tested).
- [x] Duplicate operation names produce a documented generator diagnostic (tested).
- [x] gRPC service grouping falls back to `[ApiTag]`; explicit `[ServiceGroup]` still wins (tested).
- [x] `design.md` documents the defaults, the override and the new diagnostic id.
- [x] `dotnet build Ark.Tools.slnx --configuration Debug` succeeds with zero warnings.
- [x] `dotnet test Ark.Tools.slnx --no-build --configuration Debug --minimum-expected-tests 1` passes.
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public sealed record CreateGreetingRequest : IRequest<GreetingResponse>
}

/// <summary>HTTP-only request that publishes work to Rebus and returns immediately.</summary>
[ApiTag("Greetings")]
[HttpEndpoint("POST", "/api/v{version}/greetings/compose")]
public sealed record ComposeGreetingRequest : IRequest<ComposeGreetingResponse>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public sealed class ArkGrpcEndpointGenerator : IIncrementalGenerator
{
private const string GrpcMethodAttribute = "Ark.MediatorFramework.GrpcMethodAttribute";
private const string ServiceGroupAttribute = "Ark.MediatorFramework.ServiceGroupAttribute";
private const string ApiTagAttribute = "Ark.MediatorFramework.ApiTagAttribute";
private const string ServerSetAttribute = "Ark.MediatorFramework.ServerSetAttribute";
private const string ArkAttachment = "Ark.MediatorFramework.IArkAttachment";

Expand Down Expand Up @@ -62,12 +63,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var type = (INamedTypeSymbol)context.TargetSymbol;
var grpc = context.Attributes[0];
var serviceGroupAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName(ServiceGroupAttribute);
var apiTagAttribute = context.SemanticModel.Compilation.GetTypeByMetadataName(ApiTagAttribute);
var attachmentType = context.SemanticModel.Compilation.GetTypeByMetadataName(ArkAttachment);
var serviceGroup = serviceGroupAttribute is null
? null
: type.GetAttributes().FirstOrDefault(
attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, serviceGroupAttribute));
return Extract(type, grpc, serviceGroup, attachmentType);
return Extract(type, grpc, serviceGroup, apiTagAttribute, attachmentType);
}

private static string? GetAssemblyName(GeneratorSyntaxContext context, string methodName)
Expand All @@ -88,6 +90,7 @@ private static ImmutableArray<EndpointModel> GetReferencedEndpoints(
{
var grpcAttr = compilation.GetTypeByMetadataName(GrpcMethodAttribute);
var serviceGroupAttr = compilation.GetTypeByMetadataName(ServiceGroupAttribute);
var apiTagAttr = compilation.GetTypeByMetadataName(ApiTagAttribute);
var attachmentType = compilation.GetTypeByMetadataName(ArkAttachment);
if (grpcAttr is null)
return ImmutableArray<EndpointModel>.Empty;
Expand All @@ -106,7 +109,7 @@ private static ImmutableArray<EndpointModel> GetReferencedEndpoints(
continue;

var serviceGroup = attrs.FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, serviceGroupAttr));
var model = Extract(type, grpc, serviceGroup, attachmentType);
var model = Extract(type, grpc, serviceGroup, apiTagAttr, attachmentType);
if (model is not null)
builder.Add(model.Value);
}
Expand Down Expand Up @@ -157,6 +160,7 @@ private static IEnumerable<INamedTypeSymbol> _allTypes(INamespaceSymbol ns)
INamedTypeSymbol type,
AttributeData grpc,
AttributeData? serviceGroup,
INamedTypeSymbol? apiTagAttribute,
INamedTypeSymbol? attachmentType)
{
string? response = null;
Expand Down Expand Up @@ -199,7 +203,18 @@ private static IEnumerable<INamedTypeSymbol> _allTypes(INamespaceSymbol ns)
var grpcMethod = grpc.ConstructorArguments.FirstOrDefault().Value as string ?? type.Name;
var grpcIntroducedIn = NamedInt(grpc, "IntroducedIn", 1);
var grpcRetiredIn = NamedInt(grpc, "RetiredIn", 0);
var group = serviceGroup?.ConstructorArguments.FirstOrDefault().Value as string ?? "Ark";
var apiTag = apiTagAttribute is null
? null
: type.GetAttributes()
.Where(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, apiTagAttribute))
.Select(attribute => attribute.ConstructorArguments.FirstOrDefault().Value as string)
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
var defaultGroup = type.ContainingNamespace is { IsGlobalNamespace: false } ns
? ns.ToDisplayString().Split('.').Last()
: "Ark";
var group = serviceGroup?.ConstructorArguments.FirstOrDefault().Value as string
?? apiTag
?? defaultGroup;

return new EndpointModel(
type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Expand Down
Loading
Loading