Skip to content

Commit 877e21a

Browse files
committed
Add minimal option to webapi template (#36068)
* Add minimal option to webapi template - Add "minimal" option to webapi project template - Factor Program.cs into multiple files and update template manifest to exclude/rename dependent on selected options - Updated controller and minimal versions to set endpoint/route name when EnableOpenAPI is true - Configure webapi template minimal option for VS display as "Use controllers" * Update template baselines & fix casing of option description * Fix template baseline tests issue * Update template baseline test to be more resilient Made the template baseline test more resilient by ensuring that all template arg options without values are added to the project key rather than a specific few. Args that have a value are still not added to the key. Keys are all tracked now to ensure uniqueness & an exception is thrown if they aren't. Renamed a few things for better clarity and easy of debugging too. * Make template baseline test project key disregard ordering * Update based on feedback - Change WeatherForecast to a record - Simplify method in test
1 parent d24a140 commit 877e21a

11 files changed

+380
-38
lines changed

src/ProjectTemplates/Shared/TemplatePackageInstaller.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ private static async Task InstallTemplatePackages(ITestOutputHelper output)
111111

112112
await VerifyCannotFindTemplateAsync(output, "web");
113113
await VerifyCannotFindTemplateAsync(output, "webapp");
114+
await VerifyCannotFindTemplateAsync(output, "webapi");
114115
await VerifyCannotFindTemplateAsync(output, "mvc");
115116
await VerifyCannotFindTemplateAsync(output, "react");
116117
await VerifyCannotFindTemplateAsync(output, "reactredux");
@@ -125,6 +126,7 @@ private static async Task InstallTemplatePackages(ITestOutputHelper output)
125126

126127
await VerifyCanFindTemplate(output, "webapp");
127128
await VerifyCanFindTemplate(output, "web");
129+
await VerifyCanFindTemplate(output, "webapi");
128130
await VerifyCanFindTemplate(output, "react");
129131
}
130132

src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/dotnetcli.host.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"UseLocalDB": {
55
"longName": "use-local-db"
66
},
7+
"UseMinimalAPIs": {
8+
"longName": "use-minimal-apis",
9+
"shortName": "minimal"
10+
},
711
"AADInstance": {
812
"longName": "aad-instance",
913
"shortName": ""

src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/ide.host.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
"invertBoolean": true,
4444
"isVisible": true,
4545
"defaultValue": true
46+
},
47+
{
48+
"id": "UseMinimalAPIs",
49+
"name": {
50+
"text": "Use controllers (uncheck to use minimal APIs)"
51+
},
52+
"invertBoolean": true,
53+
"isVisible": true,
54+
"defaultValue": true
4655
}
4756
],
4857
"disableHttpsSymbol": "NoHttps"

src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/.template.config/template.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,39 @@
3636
"exclude": [
3737
"Properties/launchSettings.json"
3838
]
39+
},
40+
{
41+
"condition": "(UseMinimalAPIs)",
42+
"exclude": [
43+
"Controllers/WeatherForecastController.cs",
44+
"Program.cs",
45+
"WeatherForecast.cs"
46+
]
47+
},
48+
{
49+
"condition": "(UseMinimalAPIs && (NoAuth || WindowsAuth))",
50+
"rename": {
51+
"Program.MinimalAPIs.WindowsOrNoAuth.cs": "Program.cs"
52+
},
53+
"exclude": [
54+
"Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs"
55+
]
56+
},
57+
{
58+
"condition": "(UseMinimalAPIs && (IndividualAuth || OrganizationalAuth))",
59+
"rename": {
60+
"Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs": "Program.cs"
61+
},
62+
"exclude": [
63+
"Program.MinimalAPIs.WindowsOrNoAuth.cs"
64+
]
65+
},
66+
{
67+
"condition": "(UseControllers)",
68+
"exclude": [
69+
"Program.MinimalAPIs.WindowsOrNoAuth.cs",
70+
"Program.MinimalAPIs.OrgOrIndividualB2CAuth.cs"
71+
]
3972
}
4073
]
4174
}
@@ -254,6 +287,12 @@
254287
"defaultValue": "false",
255288
"description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual or --auth IndividualB2C is specified."
256289
},
290+
"UseMinimalAPIs": {
291+
"type": "parameter",
292+
"datatype": "bool",
293+
"defaultValue": "false",
294+
"description": "Whether to use mininmal APIs instead of controllers."
295+
},
257296
"Framework": {
258297
"type": "parameter",
259298
"description": "The target framework for the project.",
@@ -321,6 +360,10 @@
321360
"EnableOpenAPI": {
322361
"type": "computed",
323362
"value": "(!DisableOpenAPI)"
363+
},
364+
"UseControllers": {
365+
"type": "computed",
366+
"value": "(!UseMinimalAPIs)"
324367
}
325368
},
326369
"primaryOutputs": [

src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Controllers/WeatherForecastController.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,15 @@ public class WeatherForecastController : ControllerBase
4040
public WeatherForecastController(ILogger<WeatherForecastController> logger,
4141
IDownstreamWebApi downstreamWebApi)
4242
{
43-
_logger = logger;
43+
_logger = logger;
4444
_downstreamWebApi = downstreamWebApi;
4545
}
4646

47+
#if (EnableOpenAPI)
48+
[HttpGet(Name = "GetWeatherForecast")]
49+
#else
4750
[HttpGet]
51+
#endif
4852
public async Task<IEnumerable<WeatherForecast>> Get()
4953
{
5054
using var response = await _downstreamWebApi.CallWebApiForUserAsync("DownstreamApi").ConfigureAwait(false);
@@ -74,11 +78,15 @@ public async Task<IEnumerable<WeatherForecast>> Get()
7478
public WeatherForecastController(ILogger<WeatherForecastController> logger,
7579
GraphServiceClient graphServiceClient)
7680
{
77-
_logger = logger;
81+
_logger = logger;
7882
_graphServiceClient = graphServiceClient;
7983
}
8084

85+
#if (EnableOpenAPI)
86+
[HttpGet(Name = "GetWeatherForecast")]
87+
#else
8188
[HttpGet]
89+
#endif
8290
public async Task<IEnumerable<WeatherForecast>> Get()
8391
{
8492
var user = await _graphServiceClient.Me.Request().GetAsync();
@@ -97,7 +105,11 @@ public WeatherForecastController(ILogger<WeatherForecastController> logger)
97105
_logger = logger;
98106
}
99107

108+
#if (EnableOpenAPI)
109+
[HttpGet(Name = "GetWeatherForecast")]
110+
#else
100111
[HttpGet]
112+
#endif
101113
public IEnumerable<WeatherForecast> Get()
102114
{
103115
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#if (GenerateApi)
2+
using System.Net.Http;
3+
#endif
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.AspNetCore.Authentication.JwtBearer;
6+
#if (GenerateGraph)
7+
using Graph = Microsoft.Graph;
8+
#endif
9+
using Microsoft.Identity.Web;
10+
using Microsoft.Identity.Web.Resource;
11+
12+
var builder = WebApplication.CreateBuilder(args);
13+
14+
// Add services to the container.
15+
#if (OrganizationalAuth)
16+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
17+
#if (GenerateApiOrGraph)
18+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
19+
.EnableTokenAcquisitionToCallDownstreamApi()
20+
#if (GenerateApi)
21+
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
22+
#endif
23+
#if (GenerateGraph)
24+
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
25+
#endif
26+
.AddInMemoryTokenCaches();
27+
#else
28+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
29+
#endif
30+
#elif (IndividualB2CAuth)
31+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
32+
#if (GenerateApi)
33+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"))
34+
.EnableTokenAcquisitionToCallDownstreamApi()
35+
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
36+
.AddInMemoryTokenCaches();
37+
#else
38+
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"));
39+
#endif
40+
#endif
41+
builder.Services.AddAuthorization();
42+
43+
#if (EnableOpenAPI)
44+
builder.Services.AddEndpointsApiExplorer();
45+
builder.Services.AddSwaggerGen();
46+
#endif
47+
48+
var app = builder.Build();
49+
50+
// Configure the HTTP request pipeline.
51+
#if (EnableOpenAPI)
52+
if (app.Environment.IsDevelopment())
53+
{
54+
app.UseSwagger();
55+
app.UseSwaggerUI();
56+
}
57+
#endif
58+
#if (RequiresHttps)
59+
60+
app.UseHttpsRedirection();
61+
#endif
62+
63+
app.UseAuthentication();
64+
app.UseAuthorization();
65+
66+
var scopeRequiredByApi = app.Configuration["AzureAd:Scopes"];
67+
var summaries = new[]
68+
{
69+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
70+
};
71+
72+
#if (GenerateApi)
73+
app.MapGet("/weatherforecast", (HttpContext httpContext, IDownstreamWebApi downstreamWebApi) =>
74+
{
75+
httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
76+
77+
using var response = await downstreamWebApi.CallWebApiForUserAsync("DownstreamApi").ConfigureAwait(false);
78+
if (response.StatusCode == System.Net.HttpStatusCode.OK)
79+
{
80+
var apiResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
81+
// Do something
82+
}
83+
else
84+
{
85+
var error = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
86+
throw new HttpRequestException($"Invalid status code in the HttpResponseMessage: {response.StatusCode}: {error}");
87+
}
88+
89+
var forecast = Enumerable.Range(1, 5).Select(index =>
90+
new WeatherForecast
91+
(
92+
DateTime.Now.AddDays(index),
93+
Random.Shared.Next(-20, 55),
94+
summaries[Random.Shared.Next(summaries.Length)]
95+
))
96+
.ToArray();
97+
98+
return forecast;
99+
})
100+
#elseif (GenerateGraph)
101+
app.MapGet("/weahterforecast", (HttpContext httpContext, GraphServiceClient graphServiceClient) =>
102+
{
103+
httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
104+
105+
var user = await _graphServiceClient.Me.Request().GetAsync();
106+
107+
var forecast = Enumerable.Range(1, 5).Select(index =>
108+
new WeatherForecast
109+
(
110+
DateTime.Now.AddDays(index),
111+
Random.Shared.Next(-20, 55),
112+
summaries[Random.Shared.Next(summaries.Length)]
113+
))
114+
.ToArray();
115+
116+
return forecast;
117+
})
118+
#else
119+
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
120+
{
121+
httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
122+
123+
var forecast = Enumerable.Range(1, 5).Select(index =>
124+
new WeatherForecast
125+
(
126+
DateTime.Now.AddDays(index),
127+
Random.Shared.Next(-20, 55),
128+
summaries[Random.Shared.Next(summaries.Length)]
129+
))
130+
.ToArray();
131+
return forecast;
132+
#endif
133+
#if (EnableOpenAPI)
134+
})
135+
.WithName("GetWeatherForecast")
136+
.RequireAuthorization();
137+
#else
138+
})
139+
.RequireAuthorization();
140+
#endif
141+
142+
app.Run();
143+
144+
record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
145+
{
146+
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
147+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
var builder = WebApplication.CreateBuilder(args);
2+
3+
// Add services to the container.
4+
#if (EnableOpenAPI)
5+
builder.Services.AddEndpointsApiExplorer();
6+
builder.Services.AddSwaggerGen();
7+
#endif
8+
9+
var app = builder.Build();
10+
11+
// Configure the HTTP request pipeline.
12+
#if (EnableOpenAPI)
13+
if (app.Environment.IsDevelopment())
14+
{
15+
app.UseSwagger();
16+
app.UseSwaggerUI();
17+
}
18+
#endif
19+
#if (RequiresHttps)
20+
21+
app.UseHttpsRedirection();
22+
#endif
23+
24+
var summaries = new[]
25+
{
26+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
27+
};
28+
29+
app.MapGet("/weatherforecast", () =>
30+
{
31+
var forecast = Enumerable.Range(1, 5).Select(index =>
32+
new WeatherForecast
33+
(
34+
DateTime.Now.AddDays(index),
35+
Random.Shared.Next(-20, 55),
36+
summaries[Random.Shared.Next(summaries.Length)]
37+
))
38+
.ToArray();
39+
return forecast;
40+
#if (EnableOpenAPI)
41+
})
42+
.WithName("GetWeatherForecast");
43+
#else
44+
});
45+
#endif
46+
47+
app.Run();
48+
49+
record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
50+
{
51+
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
52+
}

src/ProjectTemplates/scripts/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ angular/
55
blazorserver/
66
blazorwasm/
77
mvc/
8+
mvcorgauth/
89
razor/
910
react/
1011
reactredux/
1112
web/
1213
webapp/
1314
webapi/
15+
webapimin/
1416
worker/
1517
grpc/
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env pwsh
2+
#requires -version 4
3+
4+
[CmdletBinding(PositionalBinding = $false)]
5+
param()
6+
7+
Set-StrictMode -Version 2
8+
$ErrorActionPreference = 'Stop'
9+
10+
. $PSScriptRoot\Test-Template.ps1
11+
12+
Test-Template "webapimin" "webapi -minimal" "Microsoft.DotNet.Web.ProjectTemplates.6.0.6.0.0-dev.nupkg" $false

0 commit comments

Comments
 (0)