Skip to content

Commit 6a45d0c

Browse files
halter73jonsequitur
authored andcommitted
Add AspNetCoreKernelExtension
1 parent 1a9ace7 commit 6a45d0c

File tree

7 files changed

+338
-3
lines changed

7 files changed

+338
-3
lines changed

dotnet-interactive.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactiv
100100
EndProject
101101
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactive.CSharpProject.Tests", "src\Microsoft.DotNet.Interactive.CSharpProject.Tests\Microsoft.DotNet.Interactive.CSharpProject.Tests.csproj", "{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}"
102102
EndProject
103+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactive.AspNetCore.Tests", "src\Microsoft.DotNet.Interactive.AspNetCore.Tests\Microsoft.DotNet.Interactive.AspNetCore.Tests.csproj", "{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}"
104+
EndProject
103105
Global
104106
GlobalSection(SolutionConfigurationPlatforms) = preSolution
105107
Debug|Any CPU = Debug|Any CPU
@@ -554,6 +556,18 @@ Global
554556
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x64.Build.0 = Release|Any CPU
555557
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x86.ActiveCfg = Release|Any CPU
556558
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x86.Build.0 = Release|Any CPU
559+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
560+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
561+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x64.ActiveCfg = Debug|Any CPU
562+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x64.Build.0 = Debug|Any CPU
563+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x86.ActiveCfg = Debug|Any CPU
564+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x86.Build.0 = Debug|Any CPU
565+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
566+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|Any CPU.Build.0 = Release|Any CPU
567+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x64.ActiveCfg = Release|Any CPU
568+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x64.Build.0 = Release|Any CPU
569+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x86.ActiveCfg = Release|Any CPU
570+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x86.Build.0 = Release|Any CPU
557571
EndGlobalSection
558572
GlobalSection(SolutionProperties) = preSolution
559573
HideSolutionNode = FALSE
@@ -596,6 +610,7 @@ Global
596610
{CA55B4D7-ABE1-4474-9D4F-ACE235358FD6} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
597611
{25A1C91A-0B0F-4023-B95D-2C718327DFF1} = {B95A8485-8C53-4F56-B0CE-19C0726B5805}
598612
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
613+
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
599614
EndGlobalSection
600615
GlobalSection(ExtensibilityGlobals) = postSolution
601616
SolutionGuid = {6D05A9AF-CFFB-4187-8599-574387B76727}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using FluentAssertions;
7+
using Microsoft.DotNet.Interactive.AspNetCore;
8+
using Microsoft.DotNet.Interactive.Commands;
9+
using Microsoft.DotNet.Interactive.CSharp;
10+
using Microsoft.DotNet.Interactive.Events;
11+
using Microsoft.DotNet.Interactive.Tests.Utility;
12+
using Xunit;
13+
14+
namespace Microsoft.DotNet.Interactive.App.Tests
15+
{
16+
public class AspNetCoreTests : IDisposable
17+
{
18+
private readonly CompositeKernel _kernel;
19+
20+
public AspNetCoreTests()
21+
{
22+
_kernel = new CompositeKernel
23+
{
24+
new CSharpKernel(),
25+
};
26+
27+
var loadTask = new AspNetCoreKernelExtension().OnLoadAsync(_kernel);
28+
Assert.Same(Task.CompletedTask, loadTask);
29+
}
30+
31+
public void Dispose()
32+
{
33+
_kernel.Dispose();
34+
}
35+
36+
[Fact]
37+
public async Task can_define_aspnet_endpoint_with_MapGet()
38+
{
39+
var result = await _kernel.SendAsync(new SubmitCode(@"
40+
#!aspnet
41+
42+
Endpoints.MapGet(""/"", async context =>
43+
{
44+
await context.Response.WriteAsync($""Hello from MapGet!"");
45+
});
46+
47+
await HttpClient.GetAsync(""/"")"));
48+
49+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
50+
.And.ContainSingle<ReturnValueProduced>()
51+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
52+
.Which.Value.Should().Contain("Hello from MapGet!");
53+
}
54+
55+
[Fact]
56+
public async Task can_redefine_aspnet_endpoint_with_MapInteractive()
57+
{
58+
var result = await _kernel.SendAsync(new SubmitCode(@"
59+
#!aspnet
60+
61+
Endpoints.MapGet(""/"", async context =>
62+
{
63+
await context.Response.WriteAsync($""Hello from MapGet!"");
64+
});
65+
66+
Endpoints.MapInteractive(""/"", async context =>
67+
{
68+
await context.Response.WriteAsync($""Hello from MapInteractive!"");
69+
});
70+
71+
Endpoints.MapInteractive(""/"", async context =>
72+
{
73+
await context.Response.WriteAsync($""Hello from MapInteractive 2!"");
74+
});
75+
76+
await HttpClient.GetAsync(""/"")"));
77+
78+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
79+
.And.ContainSingle<ReturnValueProduced>()
80+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
81+
.Which.Value.Should().Contain("Hello from MapInteractive 2!");
82+
}
83+
84+
[Fact]
85+
public async Task can_define_aspnet_middleware_with_Use()
86+
{
87+
var result = await _kernel.SendAsync(new SubmitCode(@"
88+
#!aspnet
89+
90+
App.Use(next =>
91+
{
92+
return async httpContext =>
93+
{
94+
await httpContext.Response.WriteAsync(""Hello from middleware!"");
95+
};
96+
});
97+
98+
await HttpClient.GetAsync(""/"")"));
99+
100+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
101+
.And.ContainSingle<ReturnValueProduced>()
102+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
103+
.Which.Value.Should().Contain("Hello from middleware!");
104+
}
105+
106+
[Fact]
107+
public async Task endpoints_take_precedence_over_new_middleware()
108+
{
109+
var result = await _kernel.SendAsync(new SubmitCode(@"
110+
#!aspnet
111+
112+
App.Use(next =>
113+
{
114+
return async httpContext =>
115+
{
116+
await httpContext.Response.WriteAsync(""Hello from middleware!"");
117+
};
118+
});
119+
120+
Endpoints.MapGet(""/"", async context =>
121+
{
122+
await context.Response.WriteAsync($""Hello from MapGet!"");
123+
});
124+
125+
await HttpClient.GetAsync(""/"")"));
126+
127+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
128+
.And.ContainSingle<ReturnValueProduced>()
129+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
130+
.Which.Value.Should().Contain("Hello from MapGet!");
131+
132+
// Re-adding the middleware makes no difference since it's added to the end of the pipeline.
133+
var result2 = await _kernel.SendAsync(new SubmitCode(@"
134+
#!aspnet
135+
136+
App.Use(next =>
137+
{
138+
return async httpContext =>
139+
{
140+
await httpContext.Response.WriteAsync(""Hello from middleware!"");
141+
};
142+
});
143+
144+
await HttpClient.GetAsync(""/"")"));
145+
146+
result2.KernelEvents.ToSubscribedList().Should().NotContainErrors()
147+
.And.ContainSingle<ReturnValueProduced>()
148+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
149+
.Which.Value.Should().Contain("Hello from MapGet!");
150+
}
151+
152+
[Fact]
153+
public async Task repeatedly_invoking_aspnet_command_noops()
154+
{
155+
var result = await _kernel.SendAsync(new SubmitCode(@"
156+
#!aspnet
157+
#!aspnet
158+
159+
Endpoints.MapGet(""/"", async context =>
160+
{
161+
await context.Response.WriteAsync($""Hello from MapGet!"");
162+
});
163+
164+
await HttpClient.GetAsync(""/"")"));
165+
166+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
167+
.And.ContainSingle<ReturnValueProduced>()
168+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
169+
.Which.Value.Should().Contain("Hello from MapGet!");
170+
}
171+
172+
[Fact]
173+
public async Task aspnet_command_is_only_necessary_in_first_submission()
174+
{
175+
var commandResult = await _kernel.SendAsync(new SubmitCode("#!aspnet"));
176+
177+
commandResult.KernelEvents.ToSubscribedList().Should().NotContainErrors();
178+
179+
var result = await _kernel.SendAsync(new SubmitCode(@"
180+
Endpoints.MapGet(""/"", async context =>
181+
{
182+
await context.Response.WriteAsync($""Hello from MapGet!"");
183+
});
184+
185+
await HttpClient.GetAsync(""/"")"));
186+
187+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
188+
.And.ContainSingle<ReturnValueProduced>()
189+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
190+
.Which.Value.Should().Contain("Hello from MapGet!");
191+
}
192+
193+
[Fact]
194+
public async Task result_includes_trace_level_logs()
195+
{
196+
var commandResult = await _kernel.SendAsync(new SubmitCode("#!aspnet"));
197+
198+
commandResult.KernelEvents.ToSubscribedList().Should().NotContainErrors();
199+
200+
var result = await _kernel.SendAsync(new SubmitCode(@"
201+
#!aspnet
202+
203+
using Microsoft.Extensions.DependencyInjection;
204+
using Microsoft.Extensions.Logging;
205+
206+
Endpoints.MapGet(""/"", async httpContext =>
207+
{
208+
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
209+
var logger = loggerFactory.CreateLogger(""interactive"");
210+
logger.LogTrace(""Log from MapGet!"");
211+
212+
await httpContext.Response.WriteAsync(""Hello from MapGet!"");
213+
});
214+
215+
await HttpClient.GetAsync(""/"")"));
216+
217+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
218+
.And.ContainSingle<ReturnValueProduced>()
219+
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
220+
.Which.Value.Should().Contain("Log from MapGet!");
221+
}
222+
223+
[Fact]
224+
public async Task server_listens_on_ephemeral_port()
225+
{
226+
var result = await _kernel.SendAsync(new SubmitCode(@"
227+
#!aspnet
228+
229+
HttpClient.BaseAddress"));
230+
231+
// Assume any port higher than 1000 is ephemeral. In practice, the start of the ephemeral port range is
232+
// usually even higher (Windows XP and older Windows releases notwithstanding).
233+
// https://en.wikipedia.org/wiki/Ephemeral_port
234+
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
235+
.And.ContainSingle<ReturnValueProduced>()
236+
.Which.Value.Should().Match(uri => uri.As<Uri>().Port > 1_000);
237+
}
238+
}
239+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project>
2+
3+
<PropertyGroup>
4+
<UseBetaVersion>true</UseBetaVersion>
5+
</PropertyGroup>
6+
7+
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
8+
9+
</Project>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<NoWarn>$(NoWarn);VSTHRD200</NoWarn><!-- Ignore: Use "Async" suffix for async methods -->
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Remove="TestResults\**" />
10+
<EmbeddedResource Remove="TestResults\**" />
11+
<None Remove="TestResults\**" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="HtmlAgilityPack" Version="1.11.30" />
16+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
17+
<PackageReference Include="pocketlogger.subscribe" Version="0.7.0">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
<PackageReference Include="Assent" Version="1.7.0" />
22+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\dotnet-interactive\dotnet-interactive.csproj" />
27+
<ProjectReference Include="..\interface-generator\interface-generator.csproj" />
28+
<ProjectReference Include="..\Microsoft.DotNet.Interactive.AspNetCore\Microsoft.DotNet.Interactive.AspNetCore.csproj" />
29+
<ProjectReference Include="..\Microsoft.DotNet.Interactive.Tests\Microsoft.DotNet.Interactive.Tests.csproj" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.3">
34+
<PrivateAssets>all</PrivateAssets>
35+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
36+
</PackageReference>
37+
</ItemGroup>
38+
39+
</Project>

src/Microsoft.DotNet.Interactive.AspNetCore/AspNetCoreCSharpKernelExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.CommandLine;
6-
using System.CommandLine.Invocation;
76
using System.CommandLine.NamingConventionBinder;
87
using System.Linq;
98
using System.Net.Http;
@@ -92,7 +91,6 @@ public static CSharpKernel UseAspNetCore(this CSharpKernel kernel)
9291
};
9392

9493
kernel.AddDirective(directive);
95-
9694
kernel.AddDirective(new Command("#!aspnet-stop", "Stop ASP.NET Core host")
9795
{
9896
Handler = CommandHandler.Create(async () =>
@@ -108,6 +106,12 @@ public static CSharpKernel UseAspNetCore(this CSharpKernel kernel)
108106
})
109107
});
110108

109+
kernel.RegisterForDisposal(() =>
110+
{
111+
interactiveHost?.Dispose();
112+
interactiveHost = null;
113+
});
114+
111115
Formatter.Register<HttpResponseMessage>((responseMessage, context) =>
112116
{
113117
// Formatter.Register() doesn't support async formatters yet.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.DotNet.Interactive.CSharp;
6+
7+
namespace Microsoft.DotNet.Interactive.AspNetCore
8+
{
9+
public class AspNetCoreKernelExtension : IKernelExtension
10+
{
11+
public Task OnLoadAsync(Kernel kernel)
12+
{
13+
kernel.VisitSubkernelsAndSelf(kernel =>
14+
{
15+
if (kernel is CSharpKernel cSharpKernel)
16+
{
17+
cSharpKernel.UseAspNetCore();
18+
}
19+
});
20+
21+
return Task.CompletedTask;
22+
}
23+
}
24+
}

src/Microsoft.DotNet.Interactive.AspNetCore/InteractiveHost.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace Microsoft.DotNet.Interactive.AspNetCore
1818
{
19-
internal class InteractiveHost : IAsyncDisposable
19+
internal class InteractiveHost : IAsyncDisposable, IDisposable
2020
{
2121
private readonly IHost _host;
2222
private readonly Startup _startup;
@@ -63,5 +63,10 @@ public ValueTask DisposeAsync()
6363
_host.Dispose();
6464
return default;
6565
}
66+
67+
public void Dispose()
68+
{
69+
_host.Dispose();
70+
}
6671
}
6772
}

0 commit comments

Comments
 (0)