#77 Catch startup exceptions and show them in the browser.
This commit is contained in:
parent
520fc2b5fd
commit
a9e7948d72
|
|
@ -14,6 +14,7 @@ using Microsoft.AspNet.Http;
|
|||
using Microsoft.AspNet.Http.Features;
|
||||
using Microsoft.AspNet.Http.Features.Internal;
|
||||
using Microsoft.AspNet.Server.Features;
|
||||
using Microsoft.Dnx.Runtime;
|
||||
using Microsoft.Framework.Configuration;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.Logging;
|
||||
|
|
@ -24,11 +25,13 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
{
|
||||
// This is defined by IIS's HttpPlatformHandler.
|
||||
private static readonly string ServerPort = "HTTP_PLATFORM_PORT";
|
||||
private static readonly string DetailedErrors = "Hosting:DetailedErrors";
|
||||
|
||||
private readonly IServiceCollection _applicationServiceCollection;
|
||||
private readonly IStartupLoader _startupLoader;
|
||||
private readonly ApplicationLifetime _applicationLifetime;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly bool _captureStartupErrors;
|
||||
|
||||
private IServiceProvider _applicationServices;
|
||||
|
||||
|
|
@ -45,7 +48,8 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
public HostingEngine(
|
||||
IServiceCollection appServices,
|
||||
IStartupLoader startupLoader,
|
||||
IConfiguration config)
|
||||
IConfiguration config,
|
||||
bool captureStartupErrors)
|
||||
{
|
||||
if (appServices == null)
|
||||
{
|
||||
|
|
@ -65,6 +69,7 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
_config = config;
|
||||
_applicationServiceCollection = appServices;
|
||||
_startupLoader = startupLoader;
|
||||
_captureStartupErrors = captureStartupErrors;
|
||||
_applicationLifetime = new ApplicationLifetime();
|
||||
}
|
||||
|
||||
|
|
@ -79,8 +84,6 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
|
||||
public virtual IApplication Start()
|
||||
{
|
||||
EnsureApplicationServices();
|
||||
|
||||
var application = BuildApplication();
|
||||
|
||||
var logger = _applicationServices.GetRequiredService<ILogger<HostingEngine>>();
|
||||
|
|
@ -174,6 +177,67 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
}
|
||||
|
||||
private RequestDelegate BuildApplication()
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureApplicationServices();
|
||||
EnsureServer();
|
||||
|
||||
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
|
||||
var builder = builderFactory.CreateBuilder(_serverInstance);
|
||||
builder.ApplicationServices = _applicationServices;
|
||||
|
||||
var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
|
||||
var configure = Startup.ConfigureDelegate;
|
||||
foreach (var filter in startupFilters)
|
||||
{
|
||||
configure = filter.Configure(configure);
|
||||
}
|
||||
|
||||
configure(builder);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_captureStartupErrors)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
// EnsureApplicationServices may have failed due to a missing or throwing Startup class.
|
||||
if (_applicationServices == null)
|
||||
{
|
||||
_applicationServices = _applicationServiceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
EnsureServer();
|
||||
|
||||
// Write errors to standard out so they can be retrieved when not in development mode.
|
||||
Console.Out.WriteLine("Application startup exception: " + ex.ToString());
|
||||
var logger = _applicationServices.GetRequiredService<ILogger<HostingEngine>>();
|
||||
logger.LogError("Application startup exception", ex);
|
||||
|
||||
// Generate an HTML error page.
|
||||
var runtimeEnv = _applicationServices.GetRequiredService<IRuntimeEnvironment>();
|
||||
var hostingEnv = _applicationServices.GetRequiredService<IHostingEnvironment>();
|
||||
var showDetailedErrors = hostingEnv.IsDevelopment()
|
||||
|| string.Equals("true", _config[DetailedErrors], StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("1", _config[DetailedErrors], StringComparison.OrdinalIgnoreCase);
|
||||
var errorBytes = StartupExceptionPage.GenerateErrorHtml(showDetailedErrors, runtimeEnv, ex);
|
||||
|
||||
return context =>
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
context.Response.Headers["Cache-Control"] = "private, max-age=0";
|
||||
context.Response.ContentType = "text/html; charset=utf-8";
|
||||
context.Response.ContentLength = errorBytes.Length;
|
||||
return context.Response.Body.WriteAsync(errorBytes, 0, errorBytes.Length);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureServer()
|
||||
{
|
||||
if (ServerFactory == null)
|
||||
{
|
||||
|
|
@ -186,37 +250,25 @@ namespace Microsoft.AspNet.Hosting.Internal
|
|||
ServerFactory = _applicationServices.GetRequiredService<IServerLoader>().LoadServerFactory(ServerFactoryLocation);
|
||||
}
|
||||
|
||||
_serverInstance = ServerFactory.Initialize(_config);
|
||||
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
|
||||
var builder = builderFactory.CreateBuilder(_serverInstance);
|
||||
builder.ApplicationServices = _applicationServices;
|
||||
|
||||
var addresses = builder.ServerFeatures?.Get<IServerAddressesFeature>()?.Addresses;
|
||||
if (addresses != null && !addresses.IsReadOnly)
|
||||
if (_serverInstance == null)
|
||||
{
|
||||
var port = _config[ServerPort];
|
||||
if (!string.IsNullOrEmpty(port))
|
||||
_serverInstance = ServerFactory.Initialize(_config);
|
||||
var addresses = _serverInstance?.Get<IServerAddressesFeature>()?.Addresses;
|
||||
if (addresses != null && !addresses.IsReadOnly)
|
||||
{
|
||||
addresses.Add("http://localhost:" + port);
|
||||
}
|
||||
var port = _config[ServerPort];
|
||||
if (!string.IsNullOrEmpty(port))
|
||||
{
|
||||
addresses.Add("http://localhost:" + port);
|
||||
}
|
||||
|
||||
// Provide a default address if there aren't any configured.
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
addresses.Add("http://localhost:5000");
|
||||
// Provide a default address if there aren't any configured.
|
||||
if (addresses.Count == 0)
|
||||
{
|
||||
addresses.Add("http://localhost:5000");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
|
||||
var configure = Startup.ConfigureDelegate;
|
||||
foreach (var filter in startupFilters)
|
||||
{
|
||||
configure = filter.Configure(configure);
|
||||
}
|
||||
|
||||
configure(builder);
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
private string GetRequestIdentifier(HttpContext httpContext)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Hosting
|
|||
builder.AddCommandLine(args);
|
||||
var config = builder.Build();
|
||||
|
||||
var host = new WebHostBuilder(_serviceProvider, config).Build();
|
||||
var host = new WebHostBuilder(_serviceProvider, config, captureStartupErrors: true).Build();
|
||||
using (var app = host.Start())
|
||||
{
|
||||
var hostingEnv = app.Services.GetRequiredService<IHostingEnvironment>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,520 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text;
|
||||
using Microsoft.Dnx.Compilation;
|
||||
using Microsoft.Dnx.Runtime;
|
||||
using Microsoft.Framework.WebEncoders;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Startup
|
||||
{
|
||||
internal static class StartupExceptionPage
|
||||
{
|
||||
private const int MaxCompilationErrorsToShow = 20;
|
||||
|
||||
private static readonly string _errorPageFormatString = GetResourceString("GenericError.html", escapeBraces: true);
|
||||
private static readonly string _errorMessageFormatString = GetResourceString("GenericError_Message.html");
|
||||
private static readonly string _errorExceptionFormatString = GetResourceString("GenericError_Exception.html");
|
||||
private static readonly string _errorFooterFormatString = GetResourceString("GenericError_Footer.html");
|
||||
private static readonly string _compilationExceptionFormatString = GetResourceString("Compilation_Exception.html");
|
||||
|
||||
public static byte[] GenerateErrorHtml(bool showDetails, IRuntimeEnvironment runtimeEnvironment, params object[] errorDetails)
|
||||
{
|
||||
if (!showDetails)
|
||||
{
|
||||
errorDetails = new[] { "An error occurred while starting the application." };
|
||||
}
|
||||
|
||||
// Build the message for each error
|
||||
var wasSourceCodeWrittenOntoPage = false;
|
||||
var builder = new StringBuilder();
|
||||
var rawExceptionDetails = new StringBuilder();
|
||||
|
||||
foreach (object error in errorDetails ?? new object[0])
|
||||
{
|
||||
var ex = error as Exception;
|
||||
if (ex == null && error is ExceptionDispatchInfo)
|
||||
{
|
||||
ex = ((ExceptionDispatchInfo)error).SourceException;
|
||||
}
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
var flattenedExceptions = FlattenAndReverseExceptionTree(ex);
|
||||
|
||||
var compilationException = flattenedExceptions.OfType<ICompilationException>()
|
||||
.FirstOrDefault();
|
||||
if (compilationException != null)
|
||||
{
|
||||
WriteException(compilationException, builder, ref wasSourceCodeWrittenOntoPage);
|
||||
|
||||
var compilationErrorMessages = compilationException.CompilationFailures
|
||||
.SelectMany(f => f.Messages.Select(m => m.FormattedMessage))
|
||||
.Take(MaxCompilationErrorsToShow);
|
||||
|
||||
WriteRawExceptionDetails("Show raw compilation error details", compilationErrorMessages, rawExceptionDetails);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var innerEx in flattenedExceptions)
|
||||
{
|
||||
WriteException(innerEx, builder, ref wasSourceCodeWrittenOntoPage);
|
||||
}
|
||||
|
||||
WriteRawExceptionDetails("Show raw exception details", new[] { ex.ToString() }, rawExceptionDetails);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = Convert.ToString(error, CultureInfo.InvariantCulture);
|
||||
WriteMessage(message, builder);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the footer
|
||||
var footer = showDetails ? GenerateFooterEncoded(runtimeEnvironment) : null;
|
||||
|
||||
// And generate the full markup
|
||||
return Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, _errorPageFormatString, builder, rawExceptionDetails, footer));
|
||||
}
|
||||
|
||||
private static string BuildCodeSnippetDiv(StackFrame frame)
|
||||
{
|
||||
var filename = frame.GetFileName();
|
||||
if (!string.IsNullOrEmpty(filename))
|
||||
{
|
||||
int failingLineNumber = frame.GetFileLineNumber();
|
||||
if (failingLineNumber >= 1)
|
||||
{
|
||||
var lines = GetFailingCallSiteInFile(filename, failingLineNumber);
|
||||
if (lines != null)
|
||||
{
|
||||
return @"<div class=""codeSnippet"">"
|
||||
+ @"<div class=""filename""><code>" + HtmlEncodeAndReplaceLineBreaks(filename) + "</code></div>" + Environment.NewLine
|
||||
+ string.Join(Environment.NewLine, lines) + "</div>" + Environment.NewLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string BuildLineForStackFrame(StackFrame frame)
|
||||
{
|
||||
var builder = new StringBuilder("<pre>");
|
||||
var method = frame.GetMethod();
|
||||
|
||||
// Special case: no method available
|
||||
if (method == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, write the type name
|
||||
var type = method.DeclaringType;
|
||||
if (type != null)
|
||||
{
|
||||
// Special-case ExceptionDispatchInfo.Throw()
|
||||
if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw")
|
||||
{
|
||||
return @"<pre><span class=""faded"">--- exception rethrown ---</span></pre>";
|
||||
}
|
||||
|
||||
string prefix, friendlyName;
|
||||
SplitTypeIntoPrefixAndFriendlyName(type, out prefix, out friendlyName);
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, @"<span class=""faded"">at {0}</span>", HtmlEncodeAndReplaceLineBreaks(prefix));
|
||||
builder.Append(HtmlEncodeAndReplaceLineBreaks(friendlyName));
|
||||
}
|
||||
|
||||
// Next, write the method signature
|
||||
builder.Append(HtmlEncodeAndReplaceLineBreaks("." + method.Name));
|
||||
|
||||
// Is this method generic?
|
||||
if (method.IsGenericMethod)
|
||||
{
|
||||
builder.Append(HtmlEncodeAndReplaceLineBreaks(BuildMethodGenericParametersUnescaped(method)));
|
||||
}
|
||||
|
||||
// Build method parameters
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, @"<span class=""faded"">{0}</span>", HtmlEncodeAndReplaceLineBreaks(BuildMethodParametersUnescaped(method)));
|
||||
|
||||
// Do we have source information for this frame?
|
||||
if (frame.GetILOffset() != -1)
|
||||
{
|
||||
var filename = frame.GetFileName();
|
||||
if (!string.IsNullOrEmpty(filename))
|
||||
{
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, " in {0}:line {1:D}", HtmlEncodeAndReplaceLineBreaks(filename), frame.GetFileLineNumber());
|
||||
}
|
||||
}
|
||||
|
||||
// Finish
|
||||
builder.Append("</pre>");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string BuildMethodGenericParametersUnescaped(MethodBase method)
|
||||
{
|
||||
Debug.Assert(method.IsGenericMethod);
|
||||
return "<" + string.Join(", ", method.GetGenericArguments().Select(PrettyPrintTypeName)) + ">";
|
||||
}
|
||||
|
||||
private static string BuildMethodParametersUnescaped(MethodBase method)
|
||||
{
|
||||
return "(" + string.Join(", ", method.GetParameters().Select(p => {
|
||||
Type parameterType = p.ParameterType;
|
||||
return ((parameterType != null) ? PrettyPrintTypeName(parameterType) : "?") + " " + p.Name;
|
||||
})) + ")";
|
||||
}
|
||||
|
||||
private static void BuildCodeSnippetDiv(CompilationFailure failure,
|
||||
StringBuilder builder,
|
||||
ref int totalErrorsShown)
|
||||
{
|
||||
const int NumContextLines = 3;
|
||||
var fileName = failure.SourceFilePath;
|
||||
if (totalErrorsShown < MaxCompilationErrorsToShow &&
|
||||
!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
builder.Append(@"<div class=""codeSnippet"">")
|
||||
.AppendFormat(@"<div class=""filename""><code>{0}</code></div>", HtmlEncodeAndReplaceLineBreaks(fileName))
|
||||
.AppendLine();
|
||||
|
||||
IEnumerable<string> fileContent;
|
||||
if (string.IsNullOrEmpty(failure.SourceFileContent))
|
||||
{
|
||||
fileContent = File.ReadLines(fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileContent = failure.SourceFileContent.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
}
|
||||
foreach (var message in failure.Messages)
|
||||
{
|
||||
if (totalErrorsShown++ > MaxCompilationErrorsToShow)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (totalErrorsShown > 1)
|
||||
{
|
||||
builder.AppendLine("<br />");
|
||||
}
|
||||
|
||||
builder.Append(@"<div class=""error-message"">")
|
||||
.Append(HtmlEncodeAndReplaceLineBreaks(message.Message))
|
||||
.Append("</div>");
|
||||
|
||||
// StartLine and EndLine are 1-based
|
||||
var startLine = message.StartLine - 1;
|
||||
var endLine = message.EndLine - 1;
|
||||
var preContextIndex = Math.Max(startLine - NumContextLines, 0);
|
||||
var index = preContextIndex + 1;
|
||||
foreach (var line in fileContent.Skip(preContextIndex).Take(startLine - preContextIndex))
|
||||
{
|
||||
builder.Append(@"<div class=""line faded"">")
|
||||
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
|
||||
.Append(HtmlEncodeAndReplaceLineBreaks(line))
|
||||
.AppendLine("</code></div>");
|
||||
}
|
||||
|
||||
var numErrorLines = 1 + Math.Max(0, endLine - startLine);
|
||||
foreach (var line in fileContent.Skip(startLine).Take(numErrorLines))
|
||||
{
|
||||
builder.Append(@"<div class=""line error"">")
|
||||
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
|
||||
.Append(HtmlEncodeAndReplaceLineBreaks(line))
|
||||
.AppendLine("</code></div>");
|
||||
}
|
||||
foreach (var line in fileContent.Skip(message.EndLine).Take(NumContextLines))
|
||||
{
|
||||
builder.Append(@"<div class=""line faded"">")
|
||||
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
|
||||
.Append(HtmlEncodeAndReplaceLineBreaks(line))
|
||||
.AppendLine("</code></div>");
|
||||
}
|
||||
}
|
||||
|
||||
builder.AppendLine("</div>"); // Close codeSnippet div
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetResourceString(string name, bool escapeBraces = false)
|
||||
{
|
||||
// '{' and '}' are special in CSS, so we use "[[[0]]]" instead for {0} (and so on).
|
||||
var assembly = typeof(StartupExceptionPage).GetTypeInfo().Assembly;
|
||||
var resourceName = assembly.GetName().Name + ".compiler.resources." + name;
|
||||
var manifestStream = assembly.GetManifestResourceStream(resourceName);
|
||||
var formatString = new StreamReader(manifestStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false).ReadToEnd();
|
||||
if (escapeBraces)
|
||||
{
|
||||
formatString = formatString.Replace("{", "{{").Replace("}", "}}").Replace("[[[", "{").Replace("]]]", "}");
|
||||
}
|
||||
|
||||
return formatString;
|
||||
}
|
||||
|
||||
private static List<string> GetFailingCallSiteInFile(string filename, int failedLineNumber)
|
||||
{
|
||||
// We figure out the [first, last] range of lines to read from the file.
|
||||
var firstLineNumber = failedLineNumber - 2;
|
||||
firstLineNumber = Math.Max(1, firstLineNumber);
|
||||
var lastLineNumber = failedLineNumber + 2;
|
||||
lastLineNumber = Math.Max(lastLineNumber, failedLineNumber);
|
||||
|
||||
// Figure out how many characters lastLineNumber will take to print.
|
||||
var lastLineNumberCharLength = lastLineNumber.ToString("D", CultureInfo.InvariantCulture).Length;
|
||||
|
||||
var errorSubContents = new List<string>();
|
||||
var didReadFailingLine = false;
|
||||
|
||||
try
|
||||
{
|
||||
var thisLineNum = 0;
|
||||
foreach (var line in File.ReadLines(filename))
|
||||
{
|
||||
thisLineNum++;
|
||||
|
||||
// Are we within the correct range?
|
||||
if (thisLineNum < firstLineNumber)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (thisLineNum > lastLineNumber)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var encodedLine = HtmlEncodeAndReplaceLineBreaks("Line "
|
||||
+ thisLineNum.ToString("D", CultureInfo.InvariantCulture).PadLeft(lastLineNumberCharLength)
|
||||
+ ": "
|
||||
+ line);
|
||||
|
||||
if (thisLineNum == failedLineNumber)
|
||||
{
|
||||
didReadFailingLine = true;
|
||||
errorSubContents.Add(@"<div class=""line error""><code>" + encodedLine + "</code></div>");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorSubContents.Add(@"<div class=""line""><code>" + encodedLine + "</code></div>");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If there's an error for any reason, don't show source.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (didReadFailingLine) ? errorSubContents : null;
|
||||
}
|
||||
|
||||
private static string PrettyPrintTypeName(Type t)
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.EnsureSufficientExecutionStack();
|
||||
|
||||
var name = t.Name;
|
||||
|
||||
// Degenerate case
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
name = "?";
|
||||
}
|
||||
|
||||
// Handle generic types
|
||||
if (t.GetTypeInfo().IsGenericType)
|
||||
{
|
||||
// strip off the CLR generic type marker if it exists
|
||||
var indexOfGenericTypeMarker = name.LastIndexOf('`');
|
||||
if (indexOfGenericTypeMarker >= 0)
|
||||
{
|
||||
name = name.Substring(0, indexOfGenericTypeMarker);
|
||||
name += "<" + string.Join(", ", t.GetGenericArguments().Select(PrettyPrintTypeName)) + ">";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle nested types
|
||||
if (!t.IsGenericParameter)
|
||||
{
|
||||
var containerType = t.DeclaringType;
|
||||
if (containerType != null)
|
||||
{
|
||||
name = PrettyPrintTypeName(containerType) + "." + name;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If anything at all goes wrong, fall back to the full type name so that we don't crash the server.
|
||||
return t.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SplitTypeIntoPrefixAndFriendlyName(Type type, out string prefix, out string friendlyName)
|
||||
{
|
||||
prefix = type.Namespace;
|
||||
friendlyName = PrettyPrintTypeName(type);
|
||||
|
||||
if (!string.IsNullOrEmpty(friendlyName) && !string.IsNullOrEmpty(prefix))
|
||||
{
|
||||
prefix += ".";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateFooterEncoded(IRuntimeEnvironment environment)
|
||||
{
|
||||
var runtimeType = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeType);
|
||||
#if DNXCORE50
|
||||
var systemRuntimeAssembly = typeof(System.ComponentModel.DefaultValueAttribute).GetTypeInfo().Assembly;
|
||||
var assemblyVersion = new AssemblyName(systemRuntimeAssembly.FullName).Version.ToString();
|
||||
var clrVersion = HtmlEncodeAndReplaceLineBreaks(assemblyVersion);
|
||||
#else
|
||||
var clrVersion = HtmlEncodeAndReplaceLineBreaks(Environment.Version.ToString());
|
||||
#endif
|
||||
var runtimeArch = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeArchitecture);
|
||||
var dnxVersion = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeVersion);
|
||||
var currentAssembly = typeof(StartupExceptionPage).GetTypeInfo().Assembly;
|
||||
var currentAssemblyVersion = currentAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
|
||||
currentAssemblyVersion = HtmlEncodeAndReplaceLineBreaks(currentAssemblyVersion);
|
||||
|
||||
var os = HtmlEncodeAndReplaceLineBreaks(environment.OperatingSystem);
|
||||
var osVersion = HtmlEncodeAndReplaceLineBreaks(environment.OperatingSystemVersion);
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, _errorFooterFormatString, runtimeType, clrVersion,
|
||||
runtimeArch, dnxVersion, currentAssemblyVersion, os, osVersion);
|
||||
}
|
||||
|
||||
private static string HtmlEncodeAndReplaceLineBreaks(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Split on line breaks before passing it through the encoder.
|
||||
// We use the static default encoder since we can't depend on DI in the error handling logic.
|
||||
return string.Join("<br />" + Environment.NewLine,
|
||||
input.Split(new[] { "\r\n" }, StringSplitOptions.None)
|
||||
.SelectMany(s => s.Split(new[] { '\r', '\n' }, StringSplitOptions.None))
|
||||
.Select(HtmlEncoder.Default.HtmlEncode));
|
||||
}
|
||||
|
||||
private static void WriteException(Exception ex, StringBuilder builder, ref bool wasFailingCallSiteSourceWritten)
|
||||
{
|
||||
string inlineSourceDiv = null;
|
||||
|
||||
// First, build the stack trace
|
||||
var firstStackFrame = true;
|
||||
var stackTraceBuilder = new StringBuilder();
|
||||
var needFileInfo = true;
|
||||
foreach (var frame in new StackTrace(ex, needFileInfo).GetFrames() ?? Enumerable.Empty<StackFrame>())
|
||||
{
|
||||
if (!firstStackFrame)
|
||||
{
|
||||
stackTraceBuilder.Append("<br />");
|
||||
}
|
||||
firstStackFrame = false;
|
||||
var thisFrameLine = BuildLineForStackFrame(frame);
|
||||
stackTraceBuilder.AppendLine(thisFrameLine);
|
||||
|
||||
// Try to include the source code in the error page if we can.
|
||||
if (!wasFailingCallSiteSourceWritten && inlineSourceDiv == null)
|
||||
{
|
||||
inlineSourceDiv = BuildCodeSnippetDiv(frame);
|
||||
if (inlineSourceDiv != null)
|
||||
{
|
||||
wasFailingCallSiteSourceWritten = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, build the rest of the <div>
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, _errorExceptionFormatString,
|
||||
HtmlEncodeAndReplaceLineBreaks(ex.GetType().FullName),
|
||||
HtmlEncodeAndReplaceLineBreaks(ex.Message),
|
||||
inlineSourceDiv,
|
||||
stackTraceBuilder);
|
||||
}
|
||||
|
||||
private static void WriteRawExceptionDetails(string linkText, IEnumerable<string> lines, StringBuilder rawExceptionDetails)
|
||||
{
|
||||
rawExceptionDetails
|
||||
.AppendLine("<div class=\"rawExceptionBlock\">")
|
||||
.AppendFormat($" <div><a href=\"#\" onclick=\"javascript: showRawException(); return false;\">{linkText}</a></div>")
|
||||
.AppendLine()
|
||||
.AppendLine(" <div id=\"rawException\">")
|
||||
.Append(" <pre>");
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
rawExceptionDetails.AppendLine(line);
|
||||
}
|
||||
|
||||
rawExceptionDetails
|
||||
.AppendLine("</pre>")
|
||||
.AppendLine(" </div>")
|
||||
.AppendLine("</div>");
|
||||
}
|
||||
|
||||
private static void WriteException(ICompilationException compilationException,
|
||||
StringBuilder builder,
|
||||
ref bool wasSourceCodeWrittenOntoPage)
|
||||
{
|
||||
var totalErrorsShown = 0;
|
||||
var inlineSourceDiv = new StringBuilder();
|
||||
var firstStackFrame = true;
|
||||
foreach (var failure in compilationException.CompilationFailures)
|
||||
{
|
||||
if (firstStackFrame)
|
||||
{
|
||||
firstStackFrame = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
inlineSourceDiv.AppendLine("<br/>");
|
||||
}
|
||||
|
||||
BuildCodeSnippetDiv(failure, inlineSourceDiv, ref totalErrorsShown);
|
||||
}
|
||||
|
||||
wasSourceCodeWrittenOntoPage = totalErrorsShown > 0;
|
||||
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture,
|
||||
_compilationExceptionFormatString,
|
||||
inlineSourceDiv);
|
||||
}
|
||||
|
||||
private static void WriteMessage(string message, StringBuilder builder)
|
||||
{
|
||||
// Build the <div>
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, _errorMessageFormatString,
|
||||
HtmlEncodeAndReplaceLineBreaks(message));
|
||||
}
|
||||
|
||||
private static IEnumerable<Exception> FlattenAndReverseExceptionTree(Exception ex)
|
||||
{
|
||||
var list = new List<Exception>();
|
||||
for (; ex != null; ex = ex.InnerException)
|
||||
{
|
||||
list.Add(ex);
|
||||
}
|
||||
list.Reverse();
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ namespace Microsoft.AspNet.Hosting
|
|||
private StartupMethods _startup;
|
||||
private Type _startupType;
|
||||
private string _startupAssemblyName;
|
||||
private readonly bool _captureStartupErrors;
|
||||
|
||||
// Only one of these should be set
|
||||
private string _serverFactoryLocation;
|
||||
|
|
@ -50,6 +51,11 @@ namespace Microsoft.AspNet.Hosting
|
|||
}
|
||||
|
||||
public WebHostBuilder(IServiceProvider services, IConfiguration config)
|
||||
: this(services, config: config, captureStartupErrors: false)
|
||||
{
|
||||
}
|
||||
|
||||
public WebHostBuilder(IServiceProvider services, IConfiguration config, bool captureStartupErrors)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
|
|
@ -65,6 +71,7 @@ namespace Microsoft.AspNet.Hosting
|
|||
_loggerFactory = new LoggerFactory();
|
||||
_services = services;
|
||||
_config = config;
|
||||
_captureStartupErrors = captureStartupErrors;
|
||||
}
|
||||
|
||||
private IServiceCollection BuildHostingServices()
|
||||
|
|
@ -117,8 +124,7 @@ namespace Microsoft.AspNet.Hosting
|
|||
var startupLoader = hostingContainer.GetRequiredService<IStartupLoader>();
|
||||
|
||||
_hostingEnvironment.Initialize(appEnvironment.ApplicationBasePath, _config?[EnvironmentKey] ?? _config?[OldEnvironmentKey]);
|
||||
|
||||
var engine = new HostingEngine(hostingServices, startupLoader, _config);
|
||||
var engine = new HostingEngine(hostingServices, startupLoader, _config, _captureStartupErrors);
|
||||
|
||||
// Only one of these should be set, but they are used in priority
|
||||
engine.ServerFactory = _serverFactory;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<div class="message">
|
||||
<span class="light exception">One or more compilation errors occured:</span><br />
|
||||
<div class="stacktrace">
|
||||
{0}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>500 Internal Server Error</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: white;
|
||||
color: #111111;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 2em 4em;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: darkblue;
|
||||
text-decoration: none;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#header {
|
||||
margin-bottom: 2.5em;
|
||||
}
|
||||
|
||||
.stacktrace pre {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.faded {
|
||||
color: #999999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.message {
|
||||
margin-top: 2.5em;
|
||||
padding: 0.3em 1em;
|
||||
border-left: 0.25em solid red;
|
||||
}
|
||||
|
||||
.light {
|
||||
font-size: 1.3em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.heavy {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.exception {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.stacktrace {
|
||||
padding-top: 0.3em;
|
||||
padding-left: 2em;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.codeSnippet {
|
||||
margin-left: 2em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: inline-block;
|
||||
border-top: 0.2em solid #cccccc;
|
||||
border-bottom: 0.2em solid #cccccc;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.codeSnippet div:nth-of-type(2n) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.codeSnippet div:nth-of-type(2n + 1) {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.codeSnippet .error-message {
|
||||
color: red;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.codeSnippet div.filename {
|
||||
font-weight: bold;
|
||||
background-color: white;
|
||||
margin: 0.6em;
|
||||
}
|
||||
|
||||
.codeSnippet div.line {
|
||||
padding: 0.2em;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.codeSnippet div.line .line-number {
|
||||
color: #999999;
|
||||
text-align: right;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.codeSnippet div.error {
|
||||
color: red;
|
||||
font-weight: bolder;
|
||||
background-color: #ffeda7;
|
||||
}
|
||||
|
||||
.codeSnippet code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.rawExceptionBlock {
|
||||
margin-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#rawException {
|
||||
display: none;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2em;
|
||||
font-size: smaller;
|
||||
font-weight: lighter;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
function showRawException() {
|
||||
var div = document.getElementById('rawException');
|
||||
div.style.display = 'inline-block';
|
||||
div.scrollIntoView(true);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="header">
|
||||
<div style="font-size: 6em; display: inline-block;">
|
||||
:(
|
||||
</div>
|
||||
<div style="display: inline-block; padding-left: 3em;">
|
||||
<span style="font-size: 2em;">Oops.</span><br />
|
||||
<span style="font-size: 1.65em; font-weight: lighter;">500 Internal Server Error</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[[[0]]]
|
||||
|
||||
[[[1]]]
|
||||
|
||||
[[[2]]]
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<div class="message">
|
||||
<span class="light exception">{0}</span><br />
|
||||
<span class="heavy">{1}</span><br />
|
||||
{2}
|
||||
<div class="stacktrace">
|
||||
{3}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<footer>
|
||||
.NET Framework {0} version {1} | DNX {2} version {3} | Microsoft.AspNet.Hosting version {4} | {5} {6} | <a href="http://go.microsoft.com/fwlink/?LinkId=517394">Need help?</a>
|
||||
</footer>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<div class="message">
|
||||
<span class="heavy">{0}</span><br />
|
||||
</div>
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"Microsoft.Framework.Configuration.Json": "1.0.0-*",
|
||||
"Microsoft.Framework.DependencyInjection": "1.0.0-*",
|
||||
"Microsoft.Framework.Logging": "1.0.0-*",
|
||||
"Microsoft.Dnx.Compilation.Abstractions": "1.0.0-*",
|
||||
"Microsoft.Dnx.Runtime.Abstractions": "1.0.0-*",
|
||||
"Newtonsoft.Json": "6.0.6",
|
||||
"System.Diagnostics.Tracing.Telemetry": "4.0.0-beta-*"
|
||||
|
|
@ -30,7 +31,8 @@
|
|||
},
|
||||
"dnxcore50": {
|
||||
"dependencies": {
|
||||
"System.Console": "4.0.0-beta-*"
|
||||
"System.Console": "4.0.0-beta-*",
|
||||
"System.Diagnostics.StackTrace": "4.0.1-beta-*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Dnx.Runtime;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Fakes
|
||||
{
|
||||
public class RuntimeEnvironment : IRuntimeEnvironment
|
||||
{
|
||||
public string OperatingSystem { get; } = "TestOs";
|
||||
|
||||
public string OperatingSystemVersion { get; } = "TestOsVersion";
|
||||
|
||||
public string RuntimeArchitecture { get; } = "TestArch";
|
||||
|
||||
public string RuntimeType { get; } = "TestRuntime";
|
||||
|
||||
public string RuntimeVersion { get; } = "TestRuntimeVersion";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Fakes
|
||||
{
|
||||
public class StartupConfigureServicesThrows
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
throw new Exception("Exception from ConfigureServices");
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder builder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Fakes
|
||||
{
|
||||
public class StartupConfigureThrows
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder builder)
|
||||
{
|
||||
throw new Exception("Exception from Configure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Fakes
|
||||
{
|
||||
public class StartupCtorThrows
|
||||
{
|
||||
public StartupCtorThrows()
|
||||
{
|
||||
throw new Exception("Exception from constructor");
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNet.Builder;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting.Fakes
|
||||
{
|
||||
public class StartupStaticCtorThrows
|
||||
{
|
||||
static StartupStaticCtorThrows()
|
||||
{
|
||||
throw new Exception("Exception from static constructor");
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Hosting.Fakes;
|
||||
using Microsoft.AspNet.Hosting.Internal;
|
||||
using Microsoft.AspNet.Hosting.Server;
|
||||
using Microsoft.AspNet.Http.Features;
|
||||
using Microsoft.AspNet.Http.Internal;
|
||||
using Microsoft.Dnx.Runtime.Infrastructure;
|
||||
using Microsoft.Framework.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Hosting
|
||||
|
|
@ -29,6 +38,110 @@ namespace Microsoft.AspNet.Hosting
|
|||
Assert.Equal("MyStartupAssembly", engine.StartupAssemblyName);
|
||||
}
|
||||
|
||||
private WebHostBuilder CreateWebHostBuilder() => new WebHostBuilder(CallContextServiceLocator.Locator.ServiceProvider);
|
||||
[Fact]
|
||||
public async Task StartupMissing_Fallback()
|
||||
{
|
||||
var builder = CreateWebHostBuilder();
|
||||
var serverFactory = new TestServerFactory();
|
||||
var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup("MissingStartupAssembly").Build();
|
||||
using (engine.Start())
|
||||
{
|
||||
await AssertResponseContains(serverFactory.Application, "MissingStartupAssembly");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupStaticCtorThrows_Fallback()
|
||||
{
|
||||
var builder = CreateWebHostBuilder();
|
||||
var serverFactory = new TestServerFactory();
|
||||
var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup<StartupStaticCtorThrows>().Build();
|
||||
using (engine.Start())
|
||||
{
|
||||
await AssertResponseContains(serverFactory.Application, "Exception from static constructor");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupCtorThrows_Fallback()
|
||||
{
|
||||
var builder = CreateWebHostBuilder();
|
||||
var serverFactory = new TestServerFactory();
|
||||
var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup<StartupCtorThrows>().Build();
|
||||
using (engine.Start())
|
||||
{
|
||||
await AssertResponseContains(serverFactory.Application, "Exception from constructor");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupConfigureServicesThrows_Fallback()
|
||||
{
|
||||
var builder = CreateWebHostBuilder();
|
||||
var serverFactory = new TestServerFactory();
|
||||
var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup<StartupConfigureServicesThrows>().Build();
|
||||
using (engine.Start())
|
||||
{
|
||||
await AssertResponseContains(serverFactory.Application, "Exception from ConfigureServices");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartupConfigureThrows_Fallback()
|
||||
{
|
||||
var builder = CreateWebHostBuilder();
|
||||
var serverFactory = new TestServerFactory();
|
||||
var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup<StartupConfigureServicesThrows>().Build();
|
||||
using (engine.Start())
|
||||
{
|
||||
await AssertResponseContains(serverFactory.Application, "Exception from Configure");
|
||||
}
|
||||
}
|
||||
|
||||
private WebHostBuilder CreateWebHostBuilder()
|
||||
{
|
||||
var vals = new Dictionary<string, string>
|
||||
{
|
||||
{ "server", "Microsoft.AspNet.Hosting.Tests" },
|
||||
{ "Hosting:DetailedErrors", "true" },
|
||||
};
|
||||
var builder = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(vals);
|
||||
var config = builder.Build();
|
||||
return new WebHostBuilder(CallContextServiceLocator.Locator.ServiceProvider, config, captureStartupErrors: true);
|
||||
}
|
||||
|
||||
private async Task AssertResponseContains(Func<IFeatureCollection, Task> app, string expectedText)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Response.Body = new MemoryStream();
|
||||
await app(httpContext.Features);
|
||||
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
|
||||
var bodyText = new StreamReader(httpContext.Response.Body).ReadToEnd();
|
||||
Assert.Contains(expectedText, bodyText);
|
||||
}
|
||||
|
||||
private class TestServerFactory : IServerFactory
|
||||
{
|
||||
public Func<IFeatureCollection, Task> Application { get; set; }
|
||||
|
||||
public IFeatureCollection Initialize(IConfiguration configuration)
|
||||
{
|
||||
return new FeatureCollection();
|
||||
}
|
||||
|
||||
public IDisposable Start(IFeatureCollection serverFeatures, Func<IFeatureCollection, Task> application)
|
||||
{
|
||||
Application = application;
|
||||
return new Disposable();
|
||||
}
|
||||
|
||||
private class Disposable : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue