diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/CSharpIdentifier.cs b/src/Mvc/Extensions.ApiDescription.Design/src/CSharpIdentifier.cs
new file mode 100644
index 0000000000..33d0b0e9eb
--- /dev/null
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/CSharpIdentifier.cs
@@ -0,0 +1,55 @@
+// 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.Globalization;
+using System.Text;
+
+// Copied from
+// https://github.com/aspnet/AspNetCore-Tooling/blob/master/src/Razor/src/Microsoft.AspNetCore.Razor.Language/CSharpIdentifier.cs
+namespace Microsoft.Extensions.ApiDescription.Tasks
+{
+ internal static class CSharpIdentifier
+ {
+ // CSharp Spec §2.4.2
+ private static bool IsIdentifierStart(char character)
+ {
+ return char.IsLetter(character) ||
+ character == '_' ||
+ CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber;
+ }
+
+ public static bool IsIdentifierPart(char character)
+ {
+ return char.IsDigit(character) ||
+ IsIdentifierStart(character) ||
+ IsIdentifierPartByUnicodeCategory(character);
+ }
+
+ private static bool IsIdentifierPartByUnicodeCategory(char character)
+ {
+ var category = CharUnicodeInfo.GetUnicodeCategory(character);
+
+ return category == UnicodeCategory.NonSpacingMark || // Mn
+ category == UnicodeCategory.SpacingCombiningMark || // Mc
+ category == UnicodeCategory.ConnectorPunctuation || // Pc
+ category == UnicodeCategory.Format; // Cf
+ }
+
+ public static string SanitizeIdentifier(string inputName)
+ {
+ if (!IsIdentifierStart(inputName[0]) && IsIdentifierPart(inputName[0]))
+ {
+ inputName = "_" + inputName;
+ }
+
+ var builder = new StringBuilder(inputName.Length);
+ for (var i = 0; i < inputName.Length; i++)
+ {
+ var ch = inputName[i];
+ builder.Append(IsIdentifierPart(ch) ? ch : '_');
+ }
+
+ return builder.ToString();
+ }
+ }
+}
diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/GetFileReferenceMetadata.cs b/src/Mvc/Extensions.ApiDescription.Design/src/GetFileReferenceMetadata.cs
index 70ac4d847f..5547232bc0 100644
--- a/src/Mvc/Extensions.ApiDescription.Design/src/GetFileReferenceMetadata.cs
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/GetFileReferenceMetadata.cs
@@ -78,30 +78,13 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
Log.LogError(Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", type, item.ItemSpec));
}
- var className = item.GetMetadata("ClassName");
- if (string.IsNullOrEmpty(className))
- {
- var filename = item.GetMetadata("Filename");
- className = $"{filename}Client";
- if (char.IsLower(className[0]))
- {
- className = char.ToUpper(className[0]) + className.Substring(startIndex: 1);
- }
-
- MetadataSerializer.SetMetadata(newItem, "ClassName", className);
- }
-
- var @namespace = item.GetMetadata("Namespace");
- if (string.IsNullOrEmpty(@namespace))
- {
- MetadataSerializer.SetMetadata(newItem, "Namespace", Namespace);
- }
-
var outputPath = item.GetMetadata("OutputPath");
if (string.IsNullOrEmpty(outputPath))
{
+ // No need to further sanitize this path.
+ var filename = item.GetMetadata("Filename");
var isTypeScript = codeGenerator.EndsWith(TypeScriptLanguageName, StringComparison.OrdinalIgnoreCase);
- outputPath = $"{className}{(isTypeScript ? ".ts" : Extension)}";
+ outputPath = $"{filename}Client{(isTypeScript ? ".ts" : Extension)}";
}
// Place output file in correct directory (relative to project directory).
@@ -119,6 +102,21 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
MetadataSerializer.SetMetadata(newItem, "OutputPath", outputPath);
+ var className = item.GetMetadata("ClassName");
+ if (string.IsNullOrEmpty(className))
+ {
+ var outputFilename = Path.GetFileNameWithoutExtension(outputPath);
+
+ className = CSharpIdentifier.SanitizeIdentifier(outputFilename);
+ MetadataSerializer.SetMetadata(newItem, "ClassName", className);
+ }
+
+ var @namespace = item.GetMetadata("Namespace");
+ if (string.IsNullOrEmpty(@namespace))
+ {
+ MetadataSerializer.SetMetadata(newItem, "Namespace", Namespace);
+ }
+
// Add metadata which may be used as a property and passed to an inner build.
newItem.RemoveMetadata("SerializedMetadata");
newItem.SetMetadata("SerializedMetadata", MetadataSerializer.SerializeMetadata(newItem));
diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/GetProjectReferenceMetadata.cs b/src/Mvc/Extensions.ApiDescription.Design/src/GetProjectReferenceMetadata.cs
index a4ad42abe7..ff9ffcd780 100644
--- a/src/Mvc/Extensions.ApiDescription.Design/src/GetProjectReferenceMetadata.cs
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/GetProjectReferenceMetadata.cs
@@ -1,6 +1,7 @@
// 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 Microsoft.Build.Framework;
@@ -14,6 +15,9 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
///
public class GetProjectReferenceMetadata : Task
{
+ private static readonly char[] InvalidFilenameCharacters = Path.GetInvalidFileNameChars();
+ private static readonly string[] InvalidFilenameStrings = new[] { ".." };
+
///
/// Default directory for DocumentPath values.
///
@@ -53,22 +57,60 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
item.ItemSpec));
}
+ var documentName = item.GetMetadata("DocumentName");
+ if (string.IsNullOrEmpty(documentName))
+ {
+ documentName = "v1";
+ MetadataSerializer.SetMetadata(newItem, "DocumentName", documentName);
+ }
+
var documentPath = item.GetMetadata("DocumentPath");
if (string.IsNullOrEmpty(documentPath))
{
- var filename = item.GetMetadata("Filename");
- var documentName = item.GetMetadata("DocumentName");
+ // No need to sanitize the filename since the project file exists.
+ var projectFilename = item.GetMetadata("Filename");
+
+ // Default document filename matches project filename unless given a non-default document name.
if (string.IsNullOrEmpty(documentName))
{
- documentName = "v1";
+ // This is an odd (but allowed) case that would break the sanitize one-liner below. Also,
+ // ensure chosen name does not match the "v1" case.
+ documentPath = projectFilename + "_.json";
}
+ else if (string.Equals("v1", documentName, StringComparison.Ordinal))
+ {
+ documentPath = projectFilename + ".json";
+ }
+ else
+ {
+ // Sanitize the document name because it may contain almost any character, including illegal
+ // filename characters such as '/' and '?'. (Do not treat slashes as folder separators.)
+ var sanitizedDocumentName = string.Join("_", documentName.Split(InvalidFilenameCharacters));
+ while (sanitizedDocumentName.Contains(InvalidFilenameStrings[0]))
+ {
+ sanitizedDocumentName = string.Join(
+ ".",
+ sanitizedDocumentName.Split(InvalidFilenameStrings, StringSplitOptions.None));
+ }
- documentPath = $"{filename}.{documentName}.json";
+ documentPath = $"{projectFilename}_{sanitizedDocumentName}";
+
+ // Possible the document path already ends with .json. Don't duplicate that or a final period.
+ if (!documentPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
+ {
+ if (documentPath.EndsWith(".", StringComparison.Ordinal))
+ {
+ documentPath += "json";
+ }
+ else
+ {
+ documentPath += ".json";
+ }
+ }
+ }
}
documentPath = GetFullPath(documentPath);
- MetadataSerializer.SetMetadata(newItem, "DocumentPath", documentPath);
-
if (!destinations.Add(documentPath))
{
// This case may occur when user is experimenting e.g. with multiple generators or options.
@@ -76,6 +118,8 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
Log.LogError(Resources.FormatDuplicateProjectDocumentPaths(documentPath));
}
+ MetadataSerializer.SetMetadata(newItem, "DocumentPath", documentPath);
+
// Add metadata which may be used as a property and passed to an inner build.
newItem.SetMetadata("SerializedMetadata", MetadataSerializer.SerializeMetadata(newItem));
}
diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/GetUriReferenceMetadata.cs b/src/Mvc/Extensions.ApiDescription.Design/src/GetUriReferenceMetadata.cs
index 922359cb36..42ac9033c9 100644
--- a/src/Mvc/Extensions.ApiDescription.Design/src/GetUriReferenceMetadata.cs
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/GetUriReferenceMetadata.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -14,6 +15,9 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
///
public class GetUriReferenceMetadata : Task
{
+ private static readonly char[] InvalidFilenameCharacters = Path.GetInvalidFileNameChars();
+ private static readonly string[] InvalidFilenameStrings = new[] { ".." };
+
///
/// Default directory for DocumentPath metadata values.
///
@@ -41,68 +45,74 @@ namespace Microsoft.Extensions.ApiDescription.Tasks
var newItem = new TaskItem(item);
outputs.Add(newItem);
+ var uri = item.ItemSpec;
+ var builder = new UriBuilder(uri);
+ if (!builder.Uri.IsAbsoluteUri)
+ {
+ Log.LogError($"{nameof(Inputs)} item '{uri}' is not an absolute URI.");
+ return false;
+ }
+
+ if (!string.Equals(Uri.UriSchemeHttp, builder.Scheme, StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(Uri.UriSchemeHttps, builder.Scheme, StringComparison.OrdinalIgnoreCase))
+ {
+ Log.LogError($"{nameof(Inputs)} item '{uri}' does not have scheme {Uri.UriSchemeHttp} or " +
+ $"{Uri.UriSchemeHttps}.");
+ return false;
+ }
+
+ // If not specified, base filename on the URI.
var documentPath = item.GetMetadata("DocumentPath");
if (string.IsNullOrEmpty(documentPath))
{
- var uri = item.ItemSpec;
- var builder = new UriBuilder(uri);
- if (!builder.Uri.IsAbsoluteUri)
+ // Default to a fairly long but identifiable and fairly unique filename.
+ var documentPathBuilder = new StringBuilder(builder.Host);
+ if (!string.IsNullOrEmpty(builder.Path) &&
+ !string.Equals("/", builder.Path, StringComparison.Ordinal))
{
- Log.LogError($"{nameof(Inputs)} item '{uri}' is not an absolute URI.");
- return false;
+ documentPathBuilder.Append(builder.Path);
}
- if (!string.Equals(Uri.UriSchemeHttp, builder.Scheme, StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(Uri.UriSchemeHttps, builder.Scheme, StringComparison.OrdinalIgnoreCase))
+ if (!string.IsNullOrEmpty(builder.Query) &&
+ !string.Equals("?", builder.Query, StringComparison.Ordinal))
{
- Log.LogError($"{nameof(Inputs)} item '{uri}' does not have scheme {Uri.UriSchemeHttp} or " +
- $"{Uri.UriSchemeHttps}.");
- return false;
+ documentPathBuilder.Append(builder.Query);
}
- var host = builder.Host
- .Replace("/", string.Empty)
- .Replace("[", string.Empty)
- .Replace("]", string.Empty)
- .Replace(':', '_');
- var path = builder.Path
- .Replace("!", string.Empty)
- .Replace("'", string.Empty)
- .Replace("$", string.Empty)
- .Replace("%", string.Empty)
- .Replace("&", string.Empty)
- .Replace("(", string.Empty)
- .Replace(")", string.Empty)
- .Replace("*", string.Empty)
- .Replace("@", string.Empty)
- .Replace("~", string.Empty)
- .Replace('/', '_')
- .Replace(':', '_')
- .Replace(';', '_')
- .Replace('+', '_')
- .Replace('=', '_');
-
- documentPath = host + path;
- if (char.IsLower(documentPath[0]))
+ // Sanitize the string because it likely contains illegal filename characters such as '/' and '?'.
+ // (Do not treat slashes as folder separators.)
+ documentPath = documentPathBuilder.ToString();
+ documentPath = string.Join("_", documentPath.Split(InvalidFilenameCharacters));
+ while (documentPath.Contains(InvalidFilenameStrings[0]))
{
- documentPath = char.ToUpper(documentPath[0]) + documentPath.Substring(startIndex: 1);
+ documentPath = string.Join(
+ ".",
+ documentPath.Split(InvalidFilenameStrings, StringSplitOptions.None));
}
+ // URI might end with ".json". Don't duplicate that or a final period.
if (!documentPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
- documentPath = $"{documentPath}.json";
+ if (documentPath.EndsWith(".", StringComparison.Ordinal))
+ {
+ documentPath += "json";
+ }
+ else
+ {
+ documentPath += ".json";
+ }
}
}
documentPath = GetFullPath(documentPath);
- MetadataSerializer.SetMetadata(newItem, "DocumentPath", documentPath);
-
if (!destinations.Add(documentPath))
{
// This case may occur when user is experimenting e.g. with multiple code generators or options.
// May also occur when user accidentally duplicates DocumentPath metadata.
Log.LogError(Resources.FormatDuplicateUriDocumentPaths(documentPath));
}
+
+ MetadataSerializer.SetMetadata(newItem, "DocumentPath", documentPath);
}
Outputs = outputs.ToArray();
diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.props b/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.props
index 88a40992af..a0c5c19fd7 100644
--- a/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.props
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.props
@@ -91,7 +91,7 @@
-->
@@ -116,7 +116,7 @@
-
+
diff --git a/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.targets b/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.targets
index de1655454c..5bc81fe530 100644
--- a/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.targets
+++ b/src/Mvc/Extensions.ApiDescription.Design/src/build/Microsoft.Extensions.ApiDescription.Design.targets
@@ -311,7 +311,7 @@
%(ServiceFileReference.FullPath)
-
+
%(_Directories.FullPath)
diff --git a/src/Mvc/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs b/src/Mvc/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs
index f16e5dc8ab..fa2ee803dd 100644
--- a/src/Mvc/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs
+++ b/src/Mvc/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs
@@ -117,12 +117,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
}
writer.Flush();
- stream.Position = 0L;
- using (var outStream = File.Create(context.OutputPath))
+ if (stream.Length > 0L)
{
- stream.CopyTo(outStream);
-
- outStream.Flush();
+ stream.Position = 0L;
+ using (var outStream = File.Create(context.OutputPath))
+ {
+ stream.CopyTo(outStream);
+ outStream.Flush();
+ }
}
}