Skip to content

Commit dae7ec0

Browse files
waldekmastykarzgarrytrinder
authored andcommitted
Extends select guidance with Open API info. Closes #107
1 parent 6f173fb commit dae7ec0

File tree

5 files changed

+2223445
-10
lines changed

5 files changed

+2223445
-10
lines changed

m365-developer-proxy-abstractions/ProxyUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ private static string RemoveExtraSlashesFromUrl(string url) {
187187
return new Regex(@"([^:]\/)\/+").Replace(url, "$1");
188188
}
189189

190-
private static string GetGraphVersion(string url) {
190+
public static string GetGraphVersion(string url) {
191191
var uri = new Uri(url);
192192
return uri.Segments[1].Replace("/", "");
193193
}
Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,146 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Text.RegularExpressions;
45
using Microsoft.Extensions.Configuration;
6+
using Microsoft.OpenApi.Models;
7+
using Microsoft.OpenApi.Readers;
58
using Microsoft365.DeveloperProxy.Abstractions;
69
using Titanium.Web.Proxy.Http;
710

811
namespace Microsoft365.DeveloperProxy.Plugins.Guidance;
912

10-
public class GraphSelectGuidancePlugin : BaseProxyPlugin {
13+
public class GraphSelectGuidancePlugin : BaseProxyPlugin
14+
{
1115
public override string Name => nameof(GraphSelectGuidancePlugin);
16+
private readonly Dictionary<string, OpenApiDocument> _openApiDocuments = new();
1217

1318
public override void Register(IPluginEvents pluginEvents,
1419
IProxyContext context,
1520
ISet<UrlToWatch> urlsToWatch,
16-
IConfigurationSection? configSection = null) {
21+
IConfigurationSection? configSection = null)
22+
{
23+
var proxyFolder = Path.GetDirectoryName(context.Configuration.ConfigFile);
24+
var stopwatch = new System.Diagnostics.Stopwatch();
25+
stopwatch.Start();
26+
LoadOpenAPIFiles(proxyFolder!);
27+
stopwatch.Stop();
28+
UpdateOpenAPIGraphFilesIfNecessary(proxyFolder!);
29+
1730
base.Register(pluginEvents, context, urlsToWatch, configSection);
1831

1932
pluginEvents.AfterResponse += AfterResponse;
2033
}
2134

22-
private async Task AfterResponse(object? sender, ProxyResponseArgs e) {
35+
private async Task UpdateOpenAPIGraphFilesIfNecessary(string proxyFolder)
36+
{
37+
var modified = false;
38+
var versions = new[] { "v1.0", "beta" };
39+
foreach (var version in versions)
40+
{
41+
try
42+
{
43+
var file = new FileInfo(Path.Combine(proxyFolder, "GraphProxyPlugins", $"graph-{version.Replace(".", "_")}-openapi.yaml"));
44+
if (file.LastWriteTime.Date == DateTime.Now.Date)
45+
{
46+
// file already updated today
47+
continue;
48+
}
49+
50+
var url = $"https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/{version}/openapi.yaml";
51+
var client = new HttpClient();
52+
var response = await client.GetStringAsync(url);
53+
File.WriteAllText(file.FullName, response);
54+
modified = true;
55+
}
56+
catch (Exception ex)
57+
{
58+
_logger?.LogError(ex.Message);
59+
}
60+
}
61+
62+
if (modified)
63+
{
64+
LoadOpenAPIFiles(proxyFolder);
65+
}
66+
}
67+
68+
private async void LoadOpenAPIFiles(string proxyFolder)
69+
{
70+
var versions = new[] { "v1.0", "beta" };
71+
foreach (var version in versions)
72+
{
73+
var file = new FileInfo(Path.Combine(proxyFolder, "GraphProxyPlugins", $"graph-{version.Replace(".", "_")}-openapi.yaml"));
74+
if (!file.Exists)
75+
{
76+
continue;
77+
}
78+
79+
var openApiDocument = await new OpenApiStreamReader().ReadAsync(file.OpenRead());
80+
_openApiDocuments[version] = openApiDocument.OpenApiDocument;
81+
}
82+
}
83+
84+
private async Task AfterResponse(object? sender, ProxyResponseArgs e)
85+
{
2386
Request request = e.Session.HttpClient.Request;
2487
if (_urlsToWatch is not null && e.HasRequestUrlMatch(_urlsToWatch) && WarnNoSelect(request))
2588
_logger?.LogRequest(BuildUseSelectMessage(request), MessageType.Warning, new LoggingContext(e.Session));
2689
}
2790

28-
private static bool WarnNoSelect(Request request) =>
29-
ProxyUtils.IsGraphRequest(request) &&
30-
request.Method == "GET" &&
31-
!request.RequestUri.AbsolutePath.EndsWith("/$value", StringComparison.OrdinalIgnoreCase) &&
32-
!request.Url.Contains("$select", StringComparison.OrdinalIgnoreCase) &&
33-
!request.Url.Contains("%24select", StringComparison.OrdinalIgnoreCase);
91+
private bool WarnNoSelect(Request request)
92+
{
93+
if (!ProxyUtils.IsGraphRequest(request) ||
94+
request.Method != "GET")
95+
{
96+
return false;
97+
}
98+
99+
var graphVersion = ProxyUtils.GetGraphVersion(request.RequestUri.AbsoluteUri);
100+
var tokenizedUrl = GetTokenizedUrl(request.RequestUri.AbsoluteUri);
101+
102+
if (EndpointSupportsSelect(graphVersion, tokenizedUrl))
103+
{
104+
return !request.Url.Contains("$select", StringComparison.OrdinalIgnoreCase) &&
105+
!request.Url.Contains("%24select", StringComparison.OrdinalIgnoreCase);
106+
}
107+
else
108+
{
109+
return false;
110+
}
111+
}
112+
113+
private bool EndpointSupportsSelect(string graphVersion, string relativeUrl)
114+
{
115+
var fallback = relativeUrl.Contains("$value", StringComparison.OrdinalIgnoreCase);
116+
117+
if (!_openApiDocuments.ContainsKey(graphVersion))
118+
{
119+
return fallback;
120+
}
121+
122+
var relativeUrlPattern = Regex.Replace(relativeUrl, @"{[^}]+}", @"{[a-zA-Z-]+}");
123+
var relativeUrlRegex = new Regex($"^{relativeUrlPattern}$");
124+
125+
var openApiDocument = _openApiDocuments[graphVersion];
126+
var pathString = openApiDocument.Paths.Keys.FirstOrDefault(k => relativeUrlRegex.IsMatch(k));
127+
if (pathString is null ||
128+
!openApiDocument.Paths[pathString].Operations.ContainsKey(OperationType.Get))
129+
{
130+
return fallback;
131+
}
132+
133+
var operation = openApiDocument.Paths[pathString].Operations[OperationType.Get];
134+
var parameters = operation.Parameters;
135+
return parameters.Any(p => p.Name == "$select");
136+
}
34137

35138
private static string GetSelectParameterGuidanceUrl() => "https://aka.ms/m365/proxy/guidance/select";
36139
private static string[] BuildUseSelectMessage(Request r) => new[] { $"To improve performance of your application, use the $select parameter.", $"More info at {GetSelectParameterGuidanceUrl()}" };
140+
141+
private static string GetTokenizedUrl(string absoluteUrl)
142+
{
143+
var sanitizedUrl = ProxyUtils.SanitizeUrl(absoluteUrl);
144+
return "/" + String.Join("", new Uri(sanitizedUrl).Segments.Skip(2).Select(s => Uri.UnescapeDataString(s)));
145+
}
37146
}

0 commit comments

Comments
 (0)