Skip to content

Commit

Permalink
Add AspNetCoreKernelExtension
Browse files Browse the repository at this point in the history
  • Loading branch information
halter73 authored and jonsequitur committed Mar 24, 2022
1 parent 1a9ace7 commit 6a45d0c
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 3 deletions.
15 changes: 15 additions & 0 deletions dotnet-interactive.sln
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Interactiv
EndProject
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}"
EndProject
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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -554,6 +556,18 @@ Global
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x64.Build.0 = Release|Any CPU
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x86.ActiveCfg = Release|Any CPU
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1}.Release|x86.Build.0 = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x64.ActiveCfg = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x64.Build.0 = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x86.ActiveCfg = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Debug|x86.Build.0 = Debug|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|Any CPU.Build.0 = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x64.ActiveCfg = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x64.Build.0 = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x86.ActiveCfg = Release|Any CPU
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -596,6 +610,7 @@ Global
{CA55B4D7-ABE1-4474-9D4F-ACE235358FD6} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
{25A1C91A-0B0F-4023-B95D-2C718327DFF1} = {B95A8485-8C53-4F56-B0CE-19C0726B5805}
{BF76C062-25C2-4E90-B979-A6D4B8AE04D1} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
{5934AEAA-EB91-4CD7-8E40-DCA8289A6EB7} = {11BA3480-4584-435C-BA9A-8C554DB60E9F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6D05A9AF-CFFB-4187-8599-574387B76727}
Expand Down
239 changes: 239 additions & 0 deletions src/Microsoft.DotNet.Interactive.AspNetCore.Tests/AspNetCoreTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.DotNet.Interactive.AspNetCore;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.CSharp;
using Microsoft.DotNet.Interactive.Events;
using Microsoft.DotNet.Interactive.Tests.Utility;
using Xunit;

namespace Microsoft.DotNet.Interactive.App.Tests
{
public class AspNetCoreTests : IDisposable
{
private readonly CompositeKernel _kernel;

public AspNetCoreTests()
{
_kernel = new CompositeKernel
{
new CSharpKernel(),
};

var loadTask = new AspNetCoreKernelExtension().OnLoadAsync(_kernel);
Assert.Same(Task.CompletedTask, loadTask);
}

public void Dispose()
{
_kernel.Dispose();
}

[Fact]
public async Task can_define_aspnet_endpoint_with_MapGet()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
Endpoints.MapGet(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapGet!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapGet!");
}

[Fact]
public async Task can_redefine_aspnet_endpoint_with_MapInteractive()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
Endpoints.MapGet(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapGet!"");
});
Endpoints.MapInteractive(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapInteractive!"");
});
Endpoints.MapInteractive(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapInteractive 2!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapInteractive 2!");
}

[Fact]
public async Task can_define_aspnet_middleware_with_Use()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
App.Use(next =>
{
return async httpContext =>
{
await httpContext.Response.WriteAsync(""Hello from middleware!"");
};
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from middleware!");
}

[Fact]
public async Task endpoints_take_precedence_over_new_middleware()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
App.Use(next =>
{
return async httpContext =>
{
await httpContext.Response.WriteAsync(""Hello from middleware!"");
};
});
Endpoints.MapGet(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapGet!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapGet!");

// Re-adding the middleware makes no difference since it's added to the end of the pipeline.
var result2 = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
App.Use(next =>
{
return async httpContext =>
{
await httpContext.Response.WriteAsync(""Hello from middleware!"");
};
});
await HttpClient.GetAsync(""/"")"));

result2.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapGet!");
}

[Fact]
public async Task repeatedly_invoking_aspnet_command_noops()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
#!aspnet
Endpoints.MapGet(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapGet!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapGet!");
}

[Fact]
public async Task aspnet_command_is_only_necessary_in_first_submission()
{
var commandResult = await _kernel.SendAsync(new SubmitCode("#!aspnet"));

commandResult.KernelEvents.ToSubscribedList().Should().NotContainErrors();

var result = await _kernel.SendAsync(new SubmitCode(@"
Endpoints.MapGet(""/"", async context =>
{
await context.Response.WriteAsync($""Hello from MapGet!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Hello from MapGet!");
}

[Fact]
public async Task result_includes_trace_level_logs()
{
var commandResult = await _kernel.SendAsync(new SubmitCode("#!aspnet"));

commandResult.KernelEvents.ToSubscribedList().Should().NotContainErrors();

var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Endpoints.MapGet(""/"", async httpContext =>
{
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(""interactive"");
logger.LogTrace(""Log from MapGet!"");
await httpContext.Response.WriteAsync(""Hello from MapGet!"");
});
await HttpClient.GetAsync(""/"")"));

result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.FormattedValues.Should().ContainSingle(f => f.MimeType == "text/html")
.Which.Value.Should().Contain("Log from MapGet!");
}

[Fact]
public async Task server_listens_on_ephemeral_port()
{
var result = await _kernel.SendAsync(new SubmitCode(@"
#!aspnet
HttpClient.BaseAddress"));

// Assume any port higher than 1000 is ephemeral. In practice, the start of the ephemeral port range is
// usually even higher (Windows XP and older Windows releases notwithstanding).
// https://en.wikipedia.org/wiki/Ephemeral_port
result.KernelEvents.ToSubscribedList().Should().NotContainErrors()
.And.ContainSingle<ReturnValueProduced>()
.Which.Value.Should().Match(uri => uri.As<Uri>().Port > 1_000);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>

<PropertyGroup>
<UseBetaVersion>true</UseBetaVersion>
</PropertyGroup>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>$(NoWarn);VSTHRD200</NoWarn><!-- Ignore: Use "Async" suffix for async methods -->
</PropertyGroup>

<ItemGroup>
<Compile Remove="TestResults\**" />
<EmbeddedResource Remove="TestResults\**" />
<None Remove="TestResults\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.30" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
<PackageReference Include="pocketlogger.subscribe" Version="0.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Assent" Version="1.7.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\dotnet-interactive\dotnet-interactive.csproj" />
<ProjectReference Include="..\interface-generator\interface-generator.csproj" />
<ProjectReference Include="..\Microsoft.DotNet.Interactive.AspNetCore\Microsoft.DotNet.Interactive.AspNetCore.csproj" />
<ProjectReference Include="..\Microsoft.DotNet.Interactive.Tests\Microsoft.DotNet.Interactive.Tests.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.NamingConventionBinder;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -92,7 +91,6 @@ public static CSharpKernel UseAspNetCore(this CSharpKernel kernel)
};

kernel.AddDirective(directive);

kernel.AddDirective(new Command("#!aspnet-stop", "Stop ASP.NET Core host")
{
Handler = CommandHandler.Create(async () =>
Expand All @@ -108,6 +106,12 @@ public static CSharpKernel UseAspNetCore(this CSharpKernel kernel)
})
});

kernel.RegisterForDisposal(() =>
{
interactiveHost?.Dispose();
interactiveHost = null;
});

Formatter.Register<HttpResponseMessage>((responseMessage, context) =>
{
// Formatter.Register() doesn't support async formatters yet.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading.Tasks;
using Microsoft.DotNet.Interactive.CSharp;

namespace Microsoft.DotNet.Interactive.AspNetCore
{
public class AspNetCoreKernelExtension : IKernelExtension
{
public Task OnLoadAsync(Kernel kernel)
{
kernel.VisitSubkernelsAndSelf(kernel =>
{
if (kernel is CSharpKernel cSharpKernel)
{
cSharpKernel.UseAspNetCore();
}
});

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace Microsoft.DotNet.Interactive.AspNetCore
{
internal class InteractiveHost : IAsyncDisposable
internal class InteractiveHost : IAsyncDisposable, IDisposable
{
private readonly IHost _host;
private readonly Startup _startup;
Expand Down Expand Up @@ -63,5 +63,10 @@ public ValueTask DisposeAsync()
_host.Dispose();
return default;
}

public void Dispose()
{
_host.Dispose();
}
}
}

0 comments on commit 6a45d0c

Please sign in to comment.