From 37e562902f2a88c22c3aeaf4bc288ed266a92499 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Thu, 11 Oct 2018 12:55:10 -0700 Subject: [PATCH] Support single `IDocumentProvider` method signature - #8593 - also find `IDocumentProvider` using a more-laborious process - `Type.GetType(string)` requires an assembly-qualified name and we don't know the assembly - default method name now `GenerateAsync` - only supported signature is `public Task GenerateAsync(string, TextWriter)` also: - handle more error cases in the tool's inside man - avoid an empty document file if `IDocumentProvider.GenerateAsync(...)` fails - unwrap an `AggregateException` nits: - remove duplicate comments - change `GetDocumentCommandWorker.TryProcess(...)` to return `false` on failure - minor because return value is currently ignored - rename `GetDocumentCommandContext.Output` -> `OutputPath` - reflect recent change to `dotnet-getdocument`'s `Resources.resx` file in its designer file --- .../Commands/GetDocumentCommand.cs | 4 +- .../Commands/GetDocumentCommandContext.cs | 2 +- .../Commands/GetDocumentCommandWorker.cs | 92 ++++++++++++++----- .../Properties/Resources.Designer.cs | 84 +++++++++++++++++ src/GetDocumentInsider/Resources.resx | 18 ++++ ...oft.Extensions.ApiDescription.Design.props | 2 +- .../Properties/Resources.Designer.cs | 4 +- 7 files changed, 179 insertions(+), 27 deletions(-) diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommand.cs b/src/GetDocumentInsider/Commands/GetDocumentCommand.cs index bf80df802b..11c381aac6 100644 --- a/src/GetDocumentInsider/Commands/GetDocumentCommand.cs +++ b/src/GetDocumentInsider/Commands/GetDocumentCommand.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands internal class GetDocumentCommand : ProjectCommandBase { internal const string FallbackDocumentName = "v1"; - internal const string FallbackMethod = "Generate"; + internal const string FallbackMethod = "GenerateAsync"; internal const string FallbackService = "Microsoft.Extensions.ApiDescription.IDocumentProvider"; private CommandOption _documentName; @@ -139,7 +139,7 @@ protected override int Execute() AssemblyName = Path.GetFileNameWithoutExtension(assemblyPath), DocumentName = _documentName.Value(), Method = _method.Value(), - Output = _output.Value(), + OutputPath = _output.Value(), Service = _service.Value(), }; diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs b/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs index 208139c12f..0cd0bd7f57 100644 --- a/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs +++ b/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs @@ -18,7 +18,7 @@ public class GetDocumentCommandContext public string Method { get; set; } - public string Output { get; set; } + public string OutputPath { get; set; } public string Service { get; set; } } diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs b/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs index 752d65861f..af02bd7cc3 100644 --- a/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs +++ b/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs @@ -4,8 +4,8 @@ using System; using System.IO; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.ApiDescription.Tool.Commands { @@ -56,41 +56,91 @@ public static bool TryProcess(GetDocumentCommandContext context, IServiceProvide try { - var serviceType = Type.GetType(serviceName, throwOnError: true); - var method = serviceType.GetMethod(methodName, new[] { typeof(TextWriter), typeof(string) }); - var service = services.GetRequiredService(serviceType); + Type serviceType = null; + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + serviceType = assembly.GetType(serviceName, throwOnError: false); + if (serviceType != null) + { + break; + } + } + + // As part of the aspnet/Mvc#8425 fix, make all warnings in this method errors unless the file already + // exists. + if (serviceType == null) + { + Reporter.WriteWarning(Resources.FormatServiceTypeNotFound(serviceName)); + return false; + } + + var method = serviceType.GetMethod(methodName, new[] { typeof(string), typeof(TextWriter) }); + if (method == null) + { + Reporter.WriteWarning(Resources.FormatMethodNotFound(methodName, serviceName)); + return false; + } + else if (!typeof(Task).IsAssignableFrom(method.ReturnType)) + { + Reporter.WriteWarning(Resources.FormatMethodReturnTypeUnsupported( + methodName, + serviceName, + method.ReturnType, + typeof(Task))); + return false; + } - var success = true; - using (var writer = File.CreateText(context.Output)) + var service = services.GetService(serviceType); + if (service == null) { - if (method.ReturnType == typeof(bool)) + Reporter.WriteWarning(Resources.FormatServiceNotFound(serviceName)); + return false; + } + + // Create the output FileStream last to avoid corrupting an existing file or writing partial data. + var stream = new MemoryStream(); + using (var writer = new StreamWriter(stream)) + { + var resultTask = (Task)method.Invoke(service, new object[] { documentName, writer }); + if (resultTask == null) + { + Reporter.WriteWarning( + Resources.FormatMethodReturnedNull(methodName, serviceName, nameof(Task))); + return false; + } + + var finished = Task.WhenAny(resultTask, Task.Delay(TimeSpan.FromMinutes(1))); + if (!ReferenceEquals(resultTask, finished)) { - success = (bool)method.Invoke(service, new object[] { writer, documentName }); + Reporter.WriteWarning(Resources.FormatMethodTimedOut(methodName, serviceName, 1)); + return false; } - else + + writer.Flush(); + stream.Position = 0L; + using (var outStream = File.Create(context.OutputPath)) { - method.Invoke(service, new object[] { writer, documentName }); + stream.CopyTo(outStream); } } - if (!success) + return true; + } + catch (AggregateException ex) when (ex.InnerException != null) + { + foreach (var innerException in ex.Flatten().InnerExceptions) { - // As part of the aspnet/Mvc#8425 fix, make this an error unless the file already exists. - var message = Resources.FormatMethodInvocationFailed(methodName, serviceName, documentName); - Reporter.WriteWarning(message); + Reporter.WriteWarning(FormatException(innerException)); } - - return success; } catch (Exception ex) { - var message = FormatException(ex); + Reporter.WriteWarning(FormatException(ex)); + } - // As part of the aspnet/Mvc#8425 fix, make this an error unless the file already exists. - Reporter.WriteWarning(message); + File.Delete(context.OutputPath); - return false; - } + return false; } // TODO: Use Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources once we have dev feed available. diff --git a/src/GetDocumentInsider/Properties/Resources.Designer.cs b/src/GetDocumentInsider/Properties/Resources.Designer.cs index eaec18f2fe..488d4ae93c 100644 --- a/src/GetDocumentInsider/Properties/Resources.Designer.cs +++ b/src/GetDocumentInsider/Properties/Resources.Designer.cs @@ -220,6 +220,90 @@ internal static string MissingEntryPoint internal static string FormatMissingEntryPoint(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("MissingEntryPoint"), p0); + /// + /// Unable to find service type '{0}' in loaded assemblies. + /// + internal static string ServiceTypeNotFound + { + get => GetString("ServiceTypeNotFound"); + } + + /// + /// Unable to find service type '{0}' in loaded assemblies. + /// + internal static string FormatServiceTypeNotFound(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ServiceTypeNotFound"), p0); + + /// + /// Unable to find method named '{0}' in '{1}' implementation. + /// + internal static string MethodNotFound + { + get => GetString("MethodNotFound"); + } + + /// + /// Unable to find method named '{0}' in '{1}' implementation. + /// + internal static string FormatMethodNotFound(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("MethodNotFound"), p0, p1); + + /// + /// Unable to find service of type '{0}' in dependency injection container. + /// + internal static string ServiceNotFound + { + get => GetString("ServiceNotFound"); + } + + /// + /// Unable to find service of type '{0}' in dependency injection container. + /// + internal static string FormatServiceNotFound(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ServiceNotFound"), p0); + + /// + /// Method '{0}' of service '{1}' returned null. Must return a non-null '{2}'. + /// + internal static string MethodReturnedNull + { + get => GetString("MethodReturnedNull"); + } + + /// + /// Method '{0}' of service '{1}' returned null. Must return a non-null '{2}'. + /// + internal static string FormatMethodReturnedNull(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("MethodReturnedNull"), p0, p1, p2); + + /// + /// Method '{0}' of service '{1}' has unsupported return type '{2}'. Must return a '{3}'. + /// + internal static string MethodReturnTypeUnsupported + { + get => GetString("MethodReturnTypeUnsupported"); + } + + /// + /// Method '{0}' of service '{1}' has unsupported return type '{2}'. Must return a '{3}'. + /// + internal static string FormatMethodReturnTypeUnsupported(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("MethodReturnTypeUnsupported"), p0, p1, p2, p3); + + /// + /// Method '{0}' of service '{1}' timed out. Must complete execution within {2} minute. + /// + internal static string MethodTimedOut + { + get => GetString("MethodTimedOut"); + } + + /// + /// Method '{0}' of service '{1}' timed out. Must complete execution within {2} minute. + /// + internal static string FormatMethodTimedOut(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("MethodTimedOut"), p0, p1, p2); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/GetDocumentInsider/Resources.resx b/src/GetDocumentInsider/Resources.resx index fffabb44f3..facc644154 100644 --- a/src/GetDocumentInsider/Resources.resx +++ b/src/GetDocumentInsider/Resources.resx @@ -162,4 +162,22 @@ Assembly '{0}' does not contain an entry point. + + Unable to find service type '{0}' in loaded assemblies. + + + Unable to find method named '{0}' in '{1}' implementation. + + + Unable to find service of type '{0}' in dependency injection container. + + + Method '{0}' of service '{1}' returned null. Must return a non-null '{2}'. + + + Method '{0}' of service '{1}' has unsupported return type '{2}'. Must return a '{3}'. + + + Method '{0}' of service '{1}' timed out. Must complete execution within {2} minute. + \ No newline at end of file diff --git a/src/Microsoft.Extensions.ApiDescription.Design/build/Microsoft.Extensions.ApiDescription.Design.props b/src/Microsoft.Extensions.ApiDescription.Design/build/Microsoft.Extensions.ApiDescription.Design.props index 3a3177cdc9..88a40992af 100644 --- a/src/Microsoft.Extensions.ApiDescription.Design/build/Microsoft.Extensions.ApiDescription.Design.props +++ b/src/Microsoft.Extensions.ApiDescription.Design/build/Microsoft.Extensions.ApiDescription.Design.props @@ -97,7 +97,7 @@