Skip to content

Commit ee5072f

Browse files
authored
Use minimal APIs (#8013)
1 parent dd449d5 commit ee5072f

18 files changed

+991
-796
lines changed

src/Microsoft.Diagnostics.Monitoring.WebApi/ActionContextExtensions.cs

Lines changed: 0 additions & 132 deletions
This file was deleted.

src/Microsoft.Diagnostics.Monitoring.WebApi/ContentTypes.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,7 @@ public static class ContentTypes
1313
public const string ApplicationSpeedscopeJson = "application/speedscope+json";
1414
public const string TextPlain = "text/plain";
1515
public const string TextPlain_v0_0_4 = TextPlain + "; version=0.0.4";
16+
public const string TextJson = "text/json";
17+
public const string ApplicationAnyJson = "application/*+json";
1618
}
1719
}

src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/ControllerExtensions.cs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Http.HttpResults;
56
using Microsoft.AspNetCore.Mvc;
67
using Microsoft.Diagnostics.NETCore.Client;
78
using Microsoft.Extensions.Logging;
@@ -15,45 +16,44 @@
1516

1617
namespace Microsoft.Diagnostics.Monitoring.WebApi.Controllers
1718
{
19+
public class MinimalControllerBase
20+
{
21+
internal HttpContext HttpContext { get; }
22+
23+
internal HttpRequest Request => HttpContext.Request;
24+
25+
public MinimalControllerBase(HttpContext httpContext)
26+
{
27+
HttpContext = httpContext;
28+
}
29+
}
30+
1831
internal static class ControllerExtensions
1932
{
20-
public static ActionResult FeatureNotEnabled(this ControllerBase controller, string featureName)
33+
public static ProblemHttpResult FeatureNotEnabled(this MinimalControllerBase controller, string featureName)
2134
{
22-
return new BadRequestObjectResult(new ProblemDetails()
35+
return TypedResults.Problem(new ProblemDetails()
2336
{
2437
Detail = string.Format(Strings.Message_FeatureNotEnabled, featureName),
2538
Status = StatusCodes.Status400BadRequest
2639
});
2740
}
2841

29-
public static ActionResult NotAcceptable(this ControllerBase controller)
42+
public static IResult NotAcceptable(this MinimalControllerBase controller)
3043
{
31-
return new StatusCodeResult((int)HttpStatusCode.NotAcceptable);
44+
return TypedResults.StatusCode((int)HttpStatusCode.NotAcceptable);
3245
}
3346

34-
public static ActionResult InvokeService(this ControllerBase controller, Func<ActionResult> serviceCall, ILogger logger)
47+
public static IResult InvokeService<T>(this MinimalControllerBase controller, Func<T> serviceCall, ILogger logger)
48+
where T : IResult
3549
{
36-
//We can convert ActionResult to ActionResult<T>
37-
//and then safely convert back.
38-
return controller.InvokeService<object>(() => serviceCall(), logger).Result!;
39-
}
40-
41-
public static ActionResult<T> InvokeService<T>(this ControllerBase controller, Func<ActionResult<T>> serviceCall, ILogger logger)
42-
{
43-
//Convert from ActionResult<T> to Task<ActionResult<T>>
50+
//Convert from IResult to Task<IResult>
4451
//and safely convert back.
4552
return controller.InvokeService(() => Task.FromResult(serviceCall()), logger).Result;
4653
}
4754

48-
public static async Task<ActionResult> InvokeService(this ControllerBase controller, Func<Task<ActionResult>> serviceCall, ILogger logger)
49-
{
50-
//Task<ActionResult> -> Task<ActionResult<T>>
51-
//Then unwrap the result back to ActionResult
52-
ActionResult<object> result = await controller.InvokeService<object>(async () => await serviceCall(), logger);
53-
return result.Result!;
54-
}
55-
56-
public static async Task<ActionResult<T>> InvokeService<T>(this ControllerBase controller, Func<Task<ActionResult<T>>> serviceCall, ILogger logger)
55+
public static async Task<IResult> InvokeService<T>(this MinimalControllerBase controller, Func<Task<T>> serviceCall, ILogger logger)
56+
where T : IResult
5757
{
5858
CancellationToken token = controller.HttpContext.RequestAborted;
5959
// Exceptions are logged in the "when" clause in order to preview the exception
@@ -98,9 +98,9 @@ public static async Task<ActionResult<T>> InvokeService<T>(this ControllerBase c
9898
}
9999
}
100100

101-
public static ObjectResult Problem(this ControllerBase controller, Exception ex, int statusCode = StatusCodes.Status400BadRequest)
101+
public static ProblemHttpResult Problem(this MinimalControllerBase controller, Exception ex, int statusCode = StatusCodes.Status400BadRequest)
102102
{
103-
return new BadRequestObjectResult(ex.ToProblemDetails(statusCode)) { StatusCode = statusCode };
103+
return TypedResults.Problem(ex.ToProblemDetails(statusCode));
104104
}
105105

106106
private static bool LogError(ILogger logger, Exception ex)

src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.AspNetCore.Builder;
45
using Microsoft.AspNetCore.Http;
56
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Routing;
68
using Microsoft.Diagnostics.Monitoring.EventPipe;
9+
using Microsoft.Extensions.Logging;
710
using System;
811
using System.ComponentModel;
912
using System.ComponentModel.DataAnnotations;
@@ -13,32 +16,73 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi.Controllers
1316
{
1417
partial class DiagController
1518
{
16-
[EndpointSummary("Capture metrics for a process.")]
17-
[HttpGet("livemetrics", Name = nameof(CaptureMetrics))]
18-
[ProducesWithProblemDetails]
19-
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)]
20-
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, ContentTypes.ApplicationJsonSequence)]
21-
[ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)]
22-
[EgressValidation]
23-
public Task<ActionResult> CaptureMetrics(
24-
[FromQuery]
25-
[Description("Process ID used to identify the target process.")]
26-
int? pid = null,
27-
[FromQuery]
28-
[Description("The Runtime instance cookie used to identify the target process.")]
29-
Guid? uid = null,
30-
[FromQuery]
31-
[Description("Process name used to identify the target process.")]
32-
string? name = null,
33-
[FromQuery][Range(-1, int.MaxValue)]
34-
[Description("The duration of the metrics session (in seconds).")]
35-
int durationSeconds = 30,
36-
[FromQuery]
37-
[Description("The egress provider to which the metrics are saved.")]
38-
string? egressProvider = null,
39-
[FromQuery]
40-
[Description("An optional set of comma-separated identifiers users can include to make an operation easier to identify.")]
41-
string? tags = null)
19+
public static void MapMetricsActionMethods(IEndpointRouteBuilder builder)
20+
{
21+
// CaptureMetrics
22+
builder.MapGet("livemetrics",
23+
[EndpointSummary("Capture metrics for a process.")] (
24+
HttpContext context,
25+
ILogger<DiagController> logger,
26+
[Description("Process ID used to identify the target process.")]
27+
int? pid,
28+
[Description("The Runtime instance cookie used to identify the target process.")]
29+
Guid? uid,
30+
[Description("Process name used to identify the target process.")]
31+
string? name,
32+
[Range(-1, int.MaxValue)]
33+
[Description("The duration of the metrics session (in seconds).")]
34+
int durationSeconds = 30,
35+
[Description("The egress provider to which the metrics are saved.")]
36+
string? egressProvider = null,
37+
[Description("An optional set of comma-separated identifiers users can include to make an operation easier to identify.")]
38+
string? tags = null) =>
39+
new DiagController(context, logger).CaptureMetrics(pid, uid, name, durationSeconds, egressProvider, tags))
40+
.WithName(nameof(CaptureMetrics))
41+
.RequireDiagControllerCommon()
42+
.Produces<ProblemDetails>(StatusCodes.Status429TooManyRequests)
43+
.Produces<string>(StatusCodes.Status200OK, ContentTypes.ApplicationJsonSequence)
44+
.Produces(StatusCodes.Status202Accepted)
45+
.RequireEgressValidation();
46+
47+
// CaptureMetricsCustom
48+
builder.MapPost("livemetrics",
49+
[EndpointSummary("Capture metrics for a process.")] (
50+
HttpContext context,
51+
ILogger<DiagController> logger,
52+
[FromBody]
53+
[Required]
54+
[Description("The metrics configuration describing which metrics to capture.")]
55+
Models.EventMetricsConfiguration configuration,
56+
[Description("Process ID used to identify the target process.")]
57+
int? pid,
58+
[Description("The Runtime instance cookie used to identify the target process.")]
59+
Guid? uid,
60+
[Description("Process name used to identify the target process.")]
61+
string? name,
62+
[Range(-1, int.MaxValue)]
63+
[Description("The duration of the metrics session (in seconds).")]
64+
int durationSeconds = 30,
65+
[Description("The egress provider to which the metrics are saved.")]
66+
string? egressProvider = null,
67+
[Description("An optional set of comma-separated identifiers users can include to make an operation easier to identify.")]
68+
string? tags = null) =>
69+
new DiagController(context, logger).CaptureMetricsCustom(configuration, pid, uid, name, durationSeconds, egressProvider, tags))
70+
.WithName(nameof(CaptureMetricsCustom))
71+
.RequireDiagControllerCommon()
72+
.Produces<ProblemDetails>(StatusCodes.Status429TooManyRequests)
73+
.Produces<string>(StatusCodes.Status200OK, ContentTypes.ApplicationJsonSequence)
74+
.Produces(StatusCodes.Status202Accepted)
75+
.Accepts<Models.EventMetricsConfiguration>(ContentTypes.ApplicationJson, ContentTypes.TextJson, ContentTypes.ApplicationAnyJson)
76+
.RequireEgressValidation();
77+
}
78+
79+
public Task<IResult> CaptureMetrics(
80+
int? pid,
81+
Guid? uid,
82+
string? name,
83+
int durationSeconds,
84+
string? egressProvider,
85+
string? tags)
4286
{
4387
ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name);
4488

@@ -58,35 +102,14 @@ public Task<ActionResult> CaptureMetrics(
58102
Utilities.ArtifactType_Metrics);
59103
}
60104

61-
[EndpointSummary("Capture metrics for a process.")]
62-
[HttpPost("livemetrics", Name = nameof(CaptureMetricsCustom))]
63-
[ProducesWithProblemDetails]
64-
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)]
65-
[ProducesResponseType(typeof(string), StatusCodes.Status200OK, ContentTypes.ApplicationJsonSequence)]
66-
[ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)]
67-
[EgressValidation]
68-
public Task<ActionResult> CaptureMetricsCustom(
69-
[FromBody][Required]
70-
[Description("The metrics configuration describing which metrics to capture.")]
105+
public Task<IResult> CaptureMetricsCustom(
71106
Models.EventMetricsConfiguration configuration,
72-
[FromQuery]
73-
[Description("Process ID used to identify the target process.")]
74-
int? pid = null,
75-
[FromQuery]
76-
[Description("The Runtime instance cookie used to identify the target process.")]
77-
Guid? uid = null,
78-
[FromQuery]
79-
[Description("Process name used to identify the target process.")]
80-
string? name = null,
81-
[FromQuery][Range(-1, int.MaxValue)]
82-
[Description("The duration of the metrics session (in seconds).")]
83-
int durationSeconds = 30,
84-
[FromQuery]
85-
[Description("The egress provider to which the metrics are saved.")]
86-
string? egressProvider = null,
87-
[FromQuery]
88-
[Description("An optional set of comma-separated identifiers users can include to make an operation easier to identify.")]
89-
string? tags = null)
107+
int? pid,
108+
Guid? uid,
109+
string? name,
110+
int durationSeconds,
111+
string? egressProvider,
112+
string? tags)
90113
{
91114
ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name);
92115

0 commit comments

Comments
 (0)