Flow FileKind from project items -> code document

This is a bit of a rework of how we initially set this up, but with more
forethought to how this will work in the project system. I have not yet
surfaced this through VS.

My immediate next step is to light up the component integration tests
and something like this is on the critical path to get that work since
we need a way to specify in tests that a document should be treated as
a component.
\n\nCommit migrated from 6b81da3f02
This commit is contained in:
Ryan Nowak 2018-12-18 17:58:02 -08:00
parent ceb1189b2e
commit c37dada0dc
10 changed files with 95 additions and 63 deletions

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{ {
return codeDocument.GetInputDocumentKind() == InputDocumentKind.Component; return string.Equals(codeDocument.GetFileKind(), FileKinds.Component);
} }
protected override void OnDocumentStructureCreated(RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class, MethodDeclarationIntermediateNode method) protected override void OnDocumentStructureCreated(RazorCodeDocument codeDocument, NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class, MethodDeclarationIntermediateNode method)

View File

@ -68,10 +68,10 @@ namespace Microsoft.AspNetCore.Razor.Language
var importItems = importFeature.GetImports(projectItem); var importItems = importFeature.GetImports(projectItem);
var importSourceDocuments = GetImportSourceDocuments(importItems); var importSourceDocuments = GetImportSourceDocuments(importItems);
return CreateCodeDocumentCore(sourceDocument, importSourceDocuments, tagHelpers: null); return CreateCodeDocumentCore(sourceDocument, projectItem.FileKind, importSourceDocuments, tagHelpers: null);
} }
internal override RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument sourceDocument, IReadOnlyList<RazorSourceDocument> importSourceDocuments, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal override RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument sourceDocument, string fileKind, IReadOnlyList<RazorSourceDocument> importSourceDocuments, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
if (sourceDocument == null) if (sourceDocument == null)
{ {
@ -84,6 +84,11 @@ namespace Microsoft.AspNetCore.Razor.Language
var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions);
codeDocument.SetTagHelpers(tagHelpers); codeDocument.SetTagHelpers(tagHelpers);
if (fileKind != null)
{
codeDocument.SetFileKind(fileKind);
}
return codeDocument; return codeDocument;
} }
@ -100,10 +105,10 @@ namespace Microsoft.AspNetCore.Razor.Language
var importItems = importFeature.GetImports(projectItem); var importItems = importFeature.GetImports(projectItem);
var importSourceDocuments = GetImportSourceDocuments(importItems, suppressExceptions: true); var importSourceDocuments = GetImportSourceDocuments(importItems, suppressExceptions: true);
return CreateCodeDocumentDesignTimeCore(sourceDocument, importSourceDocuments, tagHelpers: null); return CreateCodeDocumentDesignTimeCore(sourceDocument, projectItem.FileKind, importSourceDocuments, tagHelpers: null);
} }
internal override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument sourceDocument, IReadOnlyList<RazorSourceDocument> importSourceDocuments, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument sourceDocument, string fileKind, IReadOnlyList<RazorSourceDocument> importSourceDocuments, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
if (sourceDocument == null) if (sourceDocument == null)
{ {
@ -116,6 +121,11 @@ namespace Microsoft.AspNetCore.Razor.Language
var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions);
codeDocument.SetTagHelpers(tagHelpers); codeDocument.SetTagHelpers(tagHelpers);
if (fileKind != null)
{
codeDocument.SetFileKind(fileKind);
}
return codeDocument; return codeDocument;
} }

View File

@ -34,12 +34,13 @@ namespace Microsoft.AspNetCore.Razor.Language
return directory return directory
.EnumerateFiles("*.cshtml", SearchOption.AllDirectories) .EnumerateFiles("*.cshtml", SearchOption.AllDirectories)
.Concat(directory.EnumerateFiles("*.razor", SearchOption.AllDirectories))
.Select(file => .Select(file =>
{ {
var relativePhysicalPath = file.FullName.Substring(absoluteBasePath.Length + 1); // Include leading separator var relativePhysicalPath = file.FullName.Substring(absoluteBasePath.Length + 1); // Include leading separator
var filePath = "/" + relativePhysicalPath.Replace(Path.DirectorySeparatorChar, '/'); var filePath = "/" + relativePhysicalPath.Replace(Path.DirectorySeparatorChar, '/');
return new DefaultRazorProjectItem(basePath, filePath, relativePhysicalPath, file); return new DefaultRazorProjectItem(basePath, filePath, relativePhysicalPath, fileKind: null, file);
}); });
} }
@ -57,7 +58,7 @@ namespace Microsoft.AspNetCore.Razor.Language
var relativePhysicalPath = file.FullName.Substring(absoluteBasePath.Length + 1); // Include leading separator var relativePhysicalPath = file.FullName.Substring(absoluteBasePath.Length + 1); // Include leading separator
var filePath = "/" + relativePhysicalPath.Replace(Path.DirectorySeparatorChar, '/'); var filePath = "/" + relativePhysicalPath.Replace(Path.DirectorySeparatorChar, '/');
return new DefaultRazorProjectItem("/", filePath, relativePhysicalPath, new FileInfo(absolutePath)); return new DefaultRazorProjectItem("/", filePath, relativePhysicalPath, fileKind: null, new FileInfo(absolutePath));
} }
protected override string NormalizeAndEnsureValidPath(string path) protected override string NormalizeAndEnsureValidPath(string path)

View File

@ -7,18 +7,22 @@ namespace Microsoft.AspNetCore.Razor.Language
{ {
internal class DefaultRazorProjectItem : RazorProjectItem internal class DefaultRazorProjectItem : RazorProjectItem
{ {
private readonly string _fileKind;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="DefaultRazorProjectItem"/>. /// Initializes a new instance of <see cref="DefaultRazorProjectItem"/>.
/// </summary> /// </summary>
/// <param name="basePath">The base path.</param> /// <param name="basePath">The base path.</param>
/// <param name="relativePhysicalPath">The physical path of the base path.</param> /// <param name="relativePhysicalPath">The physical path of the base path.</param>
/// <param name="filePath">The path.</param> /// <param name="filePath">The path.</param>
/// <param name="fileKind">The file kind. If null, the document kind will be inferred from the file extension.</param>
/// <param name="file">The <see cref="FileInfo"/>.</param> /// <param name="file">The <see cref="FileInfo"/>.</param>
public DefaultRazorProjectItem(string basePath, string filePath, string relativePhysicalPath, FileInfo file) public DefaultRazorProjectItem(string basePath, string filePath, string relativePhysicalPath, string fileKind, FileInfo file)
{ {
BasePath = basePath; BasePath = basePath;
FilePath = filePath; FilePath = filePath;
RelativePhysicalPath = relativePhysicalPath; RelativePhysicalPath = relativePhysicalPath;
_fileKind = fileKind;
File = file; File = file;
} }
@ -34,6 +38,8 @@ namespace Microsoft.AspNetCore.Razor.Language
public override string RelativePhysicalPath { get; } public override string RelativePhysicalPath { get; }
public override string FileKind => _fileKind ?? base.FileKind;
public override Stream Read() => new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); public override Stream Read() => new FileStream(PhysicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
} }
} }

View File

@ -1,12 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Razor.Language.Intermediate namespace Microsoft.AspNetCore.Razor.Language
{ {
internal static class InputDocumentKind internal static class FileKinds
{ {
public static readonly string Component = "component"; public static readonly string Component = "component";
public static readonly string MvcFile = "mvc"; public static readonly string Legacy = "mvc";
} }
} }

View File

@ -169,24 +169,24 @@ namespace Microsoft.AspNetCore.Razor.Language
document.Items[typeof(RazorCodeGenerationOptions)] = codeGenerationOptions; document.Items[typeof(RazorCodeGenerationOptions)] = codeGenerationOptions;
} }
public static string GetInputDocumentKind(this RazorCodeDocument document) public static string GetFileKind(this RazorCodeDocument document)
{ {
if (document == null) if (document == null)
{ {
throw new ArgumentNullException(nameof(document)); throw new ArgumentNullException(nameof(document));
} }
return (string)document.Items[typeof(InputDocumentKind)]; return (string)document.Items[typeof(FileKinds)];
} }
public static void SetInputDocumentKind(this RazorCodeDocument document, string kind) public static void SetFileKind(this RazorCodeDocument document, string fileKind)
{ {
if (document == null) if (document == null)
{ {
throw new ArgumentNullException(nameof(document)); throw new ArgumentNullException(nameof(document));
} }
document.Items[typeof(InputDocumentKind)] = kind; document.Items[typeof(FileKinds)] = fileKind;
} }
private class ImportSyntaxTreesHolder private class ImportSyntaxTreesHolder

View File

@ -35,14 +35,14 @@ namespace Microsoft.AspNetCore.Razor.Language
return codeDocument; return codeDocument;
} }
internal virtual RazorCodeDocument Process(RazorSourceDocument source, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal virtual RazorCodeDocument Process(RazorSourceDocument source, string fileKind, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
if (source == null) if (source == null)
{ {
throw new ArgumentNullException(nameof(source)); throw new ArgumentNullException(nameof(source));
} }
var codeDocument = CreateCodeDocumentCore(source, importSources, tagHelpers); var codeDocument = CreateCodeDocumentCore(source, fileKind, importSources, tagHelpers);
ProcessCore(codeDocument); ProcessCore(codeDocument);
return codeDocument; return codeDocument;
} }
@ -59,30 +59,40 @@ namespace Microsoft.AspNetCore.Razor.Language
return codeDocument; return codeDocument;
} }
internal virtual RazorCodeDocument ProcessDesignTime(RazorSourceDocument source, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal virtual RazorCodeDocument ProcessDesignTime(RazorSourceDocument source, string fileKind, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
if (source == null) if (source == null)
{ {
throw new ArgumentNullException(nameof(source)); throw new ArgumentNullException(nameof(source));
} }
var codeDocument = CreateCodeDocumentDesignTimeCore(source, importSources, tagHelpers); var codeDocument = CreateCodeDocumentDesignTimeCore(source, fileKind, importSources, tagHelpers);
ProcessCore(codeDocument); ProcessCore(codeDocument);
return codeDocument; return codeDocument;
} }
protected abstract RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem); protected abstract RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem);
internal virtual RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument source, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal virtual RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument source, string fileKind, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
return RazorCodeDocument.Create(source, importSources); var codeDocument = RazorCodeDocument.Create(source, importSources);
if (fileKind != null)
{
codeDocument.SetFileKind(fileKind);
}
return codeDocument;
} }
protected abstract RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem); protected abstract RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem);
internal virtual RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument source, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers) internal virtual RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument source, string fileKind, IReadOnlyList<RazorSourceDocument> importSources, IReadOnlyList<TagHelperDescriptor> tagHelpers)
{ {
return RazorCodeDocument.Create(source, importSources); var codeDocument = RazorCodeDocument.Create(source, importSources);
if (fileKind != null)
{
codeDocument.SetFileKind(fileKind);
}
return codeDocument;
} }
protected abstract void ProcessCore(RazorCodeDocument codeDocument); protected abstract void ProcessCore(RazorCodeDocument codeDocument);

View File

@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Razor.Language namespace Microsoft.AspNetCore.Razor.Language
{ {
@ -35,6 +38,28 @@ namespace Microsoft.AspNetCore.Razor.Language
/// </summary> /// </summary>
public virtual string RelativePhysicalPath => null; public virtual string RelativePhysicalPath => null;
/// <summary>
/// Gets the document kind that should be used for the generated document. If possible this will be inferred from the file path. May be null.
/// </summary>
public virtual string FileKind
{
get
{
if (FilePath == null)
{
return null;
}
else if (string.Equals(".razor", Path.GetExtension(FilePath), StringComparison.OrdinalIgnoreCase))
{
return FileKinds.Component;
}
else
{
return FileKinds.Legacy;
}
}
}
/// <summary> /// <summary>
/// Gets the file contents as readonly <see cref="Stream"/>. /// Gets the file contents as readonly <see cref="Stream"/>.
/// </summary> /// </summary>

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
Sources = Option("-s", ".cshtml files to compile", CommandOptionType.MultipleValue); Sources = Option("-s", ".cshtml files to compile", CommandOptionType.MultipleValue);
Outputs = Option("-o", "Generated output file path", CommandOptionType.MultipleValue); Outputs = Option("-o", "Generated output file path", CommandOptionType.MultipleValue);
RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue); RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue);
DocumentKinds = Option("-k", "Document kind", CommandOptionType.MultipleValue); FileKinds = Option("-k", "File kind", CommandOptionType.MultipleValue);
ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue);
TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue);
Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue);
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public CommandOption RelativePaths { get; } public CommandOption RelativePaths { get; }
public CommandOption DocumentKinds { get; } public CommandOption FileKinds { get; }
public CommandOption ProjectDirectory { get; } public CommandOption ProjectDirectory { get; }
@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
var version = RazorLanguageVersion.Parse(Version.Value()); var version = RazorLanguageVersion.Parse(Version.Value());
var configuration = RazorConfiguration.Create(version, Configuration.Value(), extensions); var configuration = RazorConfiguration.Create(version, Configuration.Value(), extensions);
var sourceItems = GetSourceItems(ProjectDirectory.Value(), Sources.Values, Outputs.Values, RelativePaths.Values, DocumentKinds.Values); var sourceItems = GetSourceItems(ProjectDirectory.Value(), Sources.Values, Outputs.Values, RelativePaths.Values, FileKinds.Values);
var result = ExecuteCore( var result = ExecuteCore(
configuration: configuration, configuration: configuration,
@ -105,11 +105,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
return false; return false;
} }
if (DocumentKinds.Values.Count != 0 && DocumentKinds.Values.Count != Sources.Values.Count) if (FileKinds.Values.Count != 0 && FileKinds.Values.Count != Sources.Values.Count)
{ {
// 2.x tasks do not specify DocumentKinds - in which case, no values will be present. If a kind for one document is specified, we expect as many kind entries // 2.x tasks do not specify FileKinds - in which case, no values will be present. If a kind for one file is specified, we expect as many kind entries
// as sources. // as sources.
Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {DocumentKinds.Description} has {DocumentKinds.Values.Count} values."); Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {FileKinds.Description} has {FileKinds.Values.Count} values.");
return false; return false;
} }
@ -171,7 +171,6 @@ namespace Microsoft.AspNetCore.Razor.Tools
var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b => var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b =>
{ {
b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, });
b.Features.Add(new InputDocumentKindClassifierPass(sourceItems));
if (GenerateDeclaration.HasValue()) if (GenerateDeclaration.HasValue())
{ {
@ -223,6 +222,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
basePath: "/", basePath: "/",
filePath: item.FilePath, filePath: item.FilePath,
relativePhysicalPath: item.RelativePhysicalPath, relativePhysicalPath: item.RelativePhysicalPath,
fileKind: item.FileKind,
file: new FileInfo(item.SourcePath)); file: new FileInfo(item.SourcePath));
project.Add(projectItem); project.Add(projectItem);
@ -251,15 +251,15 @@ namespace Microsoft.AspNetCore.Razor.Tools
} }
} }
private SourceItem[] GetSourceItems(string projectDirectory, List<string> sources, List<string> outputs, List<string> relativePath, List<string> documentKinds) private SourceItem[] GetSourceItems(string projectDirectory, List<string> sources, List<string> outputs, List<string> relativePath, List<string> fileKinds)
{ {
var items = new SourceItem[sources.Count]; var items = new SourceItem[sources.Count];
for (var i = 0; i < items.Length; i++) for (var i = 0; i < items.Length; i++)
{ {
var outputPath = Path.Combine(projectDirectory, outputs[i]); var outputPath = Path.Combine(projectDirectory, outputs[i]);
var documentKind = documentKinds.Count > 0 ? documentKinds[i] : "mvc"; var fileKind = fileKinds.Count > 0 ? fileKinds[i] : "mvc";
items[i] = new SourceItem(sources[i], outputs[i], relativePath[i], documentKind); items[i] = new SourceItem(sources[i], outputs[i], relativePath[i], fileKind);
} }
return items; return items;
@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
private readonly struct SourceItem private readonly struct SourceItem
{ {
public SourceItem(string sourcePath, string outputPath, string physicalRelativePath, string documentKind) public SourceItem(string sourcePath, string outputPath, string physicalRelativePath, string fileKind)
{ {
SourcePath = sourcePath; SourcePath = sourcePath;
OutputPath = outputPath; OutputPath = outputPath;
@ -305,7 +305,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
FilePath = '/' + physicalRelativePath FilePath = '/' + physicalRelativePath
.Replace(Path.DirectorySeparatorChar, '/') .Replace(Path.DirectorySeparatorChar, '/')
.Replace("//", "/"); .Replace("//", "/");
DocumentKind = documentKind; FileKind = fileKind;
} }
public string SourcePath { get; } public string SourcePath { get; }
@ -316,7 +316,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
public string FilePath { get; } public string FilePath { get; }
public string DocumentKind { get; } public string FileKind { get; }
} }
private class StaticTagHelperFeature : ITagHelperFeature private class StaticTagHelperFeature : ITagHelperFeature
@ -342,31 +342,5 @@ namespace Microsoft.AspNetCore.Razor.Tools
options.SuppressPrimaryMethodBody = true; options.SuppressPrimaryMethodBody = true;
} }
} }
private class InputDocumentKindClassifierPass : RazorEngineFeatureBase, IRazorDocumentClassifierPass
{
public InputDocumentKindClassifierPass(SourceItem[] sourceItems)
{
DocumentKinds = new Dictionary<string, string>(sourceItems.Length, StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < sourceItems.Length; i++)
{
var item = sourceItems[i];
DocumentKinds[item.SourcePath] = item.DocumentKind;
}
}
// Run before other document classifiers
public int Order => -1000;
public Dictionary<string, string> DocumentKinds { get; }
public void Execute(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
if (DocumentKinds.TryGetValue(codeDocument.Source.FilePath, out var kind))
{
codeDocument.SetInputDocumentKind(kind);
}
}
}
} }
} }

View File

@ -8,20 +8,26 @@ namespace Microsoft.AspNetCore.Razor.Language
{ {
public class TestRazorProjectItem : RazorProjectItem public class TestRazorProjectItem : RazorProjectItem
{ {
private readonly string _fileKind;
public TestRazorProjectItem( public TestRazorProjectItem(
string filePath, string filePath,
string physicalPath = null, string physicalPath = null,
string relativePhysicalPath = null, string relativePhysicalPath = null,
string basePath = "/") string basePath = "/",
string fileKind = null)
{ {
FilePath = filePath; FilePath = filePath;
PhysicalPath = physicalPath; PhysicalPath = physicalPath;
RelativePhysicalPath = relativePhysicalPath; RelativePhysicalPath = relativePhysicalPath;
BasePath = basePath; BasePath = basePath;
_fileKind = fileKind;
} }
public override string BasePath { get; } public override string BasePath { get; }
public override string FileKind => _fileKind ?? base.FileKind;
public override string FilePath { get; } public override string FilePath { get; }
public override string PhysicalPath { get; } public override string PhysicalPath { get; }