Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Support single IDocumentProvider method signature
Browse files Browse the repository at this point in the history
- #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
  • Loading branch information
dougbu committed Oct 27, 2018
1 parent 6bb292c commit 37e5629
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 27 deletions.
4 changes: 2 additions & 2 deletions src/GetDocumentInsider/Commands/GetDocumentCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down
92 changes: 71 additions & 21 deletions src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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.
Expand Down
84 changes: 84 additions & 0 deletions src/GetDocumentInsider/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/GetDocumentInsider/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,22 @@
<data name="MissingEntryPoint" xml:space="preserve">
<value>Assembly '{0}' does not contain an entry point.</value>
</data>
<data name="ServiceTypeNotFound" xml:space="preserve">
<value>Unable to find service type '{0}' in loaded assemblies.</value>
</data>
<data name="MethodNotFound" xml:space="preserve">
<value>Unable to find method named '{0}' in '{1}' implementation.</value>
</data>
<data name="ServiceNotFound" xml:space="preserve">
<value>Unable to find service of type '{0}' in dependency injection container.</value>
</data>
<data name="MethodReturnedNull" xml:space="preserve">
<value>Method '{0}' of service '{1}' returned null. Must return a non-null '{2}'.</value>
</data>
<data name="MethodReturnTypeUnsupported" xml:space="preserve">
<value>Method '{0}' of service '{1}' has unsupported return type '{2}'. Must return a '{3}'.</value>
</data>
<data name="MethodTimedOut" xml:space="preserve">
<value>Method '{0}' of service '{1}' timed out. Must complete execution within {2} minute.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
<DocumentPath />
<!--
Method Default document generator should invoke on the %(Service) to generate document.
Default is set in server project, falling back to "Generate".
Default is set in server project, falling back to "GenerateAsync".
-->
<Method />
<!--
Expand Down
4 changes: 2 additions & 2 deletions src/dotnet-getdocument/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 37e5629

Please sign in to comment.