Implements versions for generated code

This change implements version tracking the inputs and outputs of
generated code.

Version tracking is still best-effort - meaning that in some cases a
perfect system could avoid doing more work. However, since we base the
versions off of all of the inputs, we now that the guarantee that code
generation operations that happen 'out of order' will always result in
the newer inputs generating the newer outputs.

Fixes: https://github.com/aspnet/Razor/issues/2650
This commit is contained in:
Ryan Nowak 2018-10-20 21:18:38 -07:00
parent 029304ae69
commit 357657fc45
18 changed files with 1019 additions and 721 deletions

View File

@ -37,9 +37,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override ProjectSnapshot Project => ProjectInternal;
public override bool SupportsOutput => true;
public override IReadOnlyList<DocumentSnapshot> GetImports()
{
return State.Imports.GetImports(Project, this);
return State.GetImports(ProjectInternal);
}
public override Task<SourceText> GetTextAsync()
@ -52,10 +54,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return State.GetTextVersionAsync();
}
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
public override async Task<RazorCodeDocument> GetGeneratedOutputAsync()
{
// IMPORTANT: Don't put more code here. We want this to return a cached task.
return State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this);
var (output, _, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false);
return output;
}
public override bool TryGetText(out SourceText result)
@ -70,9 +72,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
{
if (State.GeneratedOutput.IsResultAvailable)
if (State.IsGeneratedOutputResultAvailable)
{
result = State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this).Result;
result = State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).Result.output;
return true;
}

View File

@ -0,0 +1,85 @@
// 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.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DefaultImportDocumentSnapshot : DocumentSnapshot
{
private ProjectSnapshot _project;
private RazorProjectItem _importItem;
private SourceText _sourceText;
private VersionStamp _version;
public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item)
{
_project = project;
_importItem = item;
_version = VersionStamp.Default;
}
public override string FilePath => null;
public override string TargetPath => null;
public override bool SupportsOutput => false;
public override ProjectSnapshot Project => _project;
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
{
throw new NotSupportedException();
}
public override IReadOnlyList<DocumentSnapshot> GetImports()
{
return Array.Empty<DocumentSnapshot>();
}
public async override Task<SourceText> GetTextAsync()
{
using (var stream = _importItem.Read())
using (var reader = new StreamReader(stream))
{
var content = await reader.ReadToEndAsync();
_sourceText = SourceText.From(content);
}
return _sourceText;
}
public override Task<VersionStamp> GetTextVersionAsync()
{
return Task.FromResult(_version);
}
public override bool TryGetText(out SourceText result)
{
if (_sourceText != null)
{
result = _sourceText;
return true;
}
result = null;
return false;
}
public override bool TryGetTextVersion(out VersionStamp result)
{
result = _version;
return true;
}
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
{
throw new NotSupportedException();
}
}
}

View File

@ -89,20 +89,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override RazorProjectEngine GetProjectEngine()
{
return State.ProjectEngine.GetProjectEngine(this.State);
return State.ProjectEngine;
}
public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
{
// IMPORTANT: Don't put more code here. We want this to return a cached task.
return State.TagHelpers.GetTagHelperInitializationTask(this);
return State.GetTagHelpersAsync(this);
}
public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> result)
{
if (State.TagHelpers.IsResultAvailable)
if (State.IsTagHelperResultAvailable)
{
result = State.TagHelpers.GetTagHelperInitializationTask(this).Result;
result = State.GetTagHelpersAsync(this).Result;
return true;
}

View File

@ -1,179 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Internal;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DocumentGeneratedOutputTracker
{
private readonly object _lock;
private DocumentGeneratedOutputTracker _older;
private Task<RazorCodeDocument> _task;
private IReadOnlyList<TagHelperDescriptor> _tagHelpers;
private IReadOnlyList<ImportItem> _imports;
public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older)
{
_older = older;
_lock = new object();
}
public bool IsResultAvailable => _task?.IsCompleted == true;
public DocumentGeneratedOutputTracker Older => _older;
public Task<RazorCodeDocument> GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (_task == null)
{
lock (_lock)
{
if (_task == null)
{
_task = GetGeneratedOutputInitializationTaskCore(project, document);
}
}
}
return _task;
}
public DocumentGeneratedOutputTracker Fork()
{
return new DocumentGeneratedOutputTracker(this);
}
private async Task<RazorCodeDocument> GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document)
{
var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false);
var imports = await GetImportsAsync(project, document);
if (_older != null && _older.IsResultAvailable)
{
var tagHelperDifference = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
tagHelperDifference.UnionWith(_older._tagHelpers);
tagHelperDifference.SymmetricExceptWith(tagHelpers);
var importDifference = new HashSet<ImportItem>();
importDifference.UnionWith(_older._imports);
importDifference.SymmetricExceptWith(imports);
if (tagHelperDifference.Count == 0 && importDifference.Count == 0)
{
// We can use the cached result.
var result = _older._task.Result;
// Drop reference so it can be GC'ed
_older = null;
// Cache the tag helpers and imports so the next version can use them
_tagHelpers = tagHelpers;
_imports = imports;
return result;
}
}
// Drop reference so it can be GC'ed
_older = null;
// Cache the tag helpers and imports so the next version can use them
_tagHelpers = tagHelpers;
_imports = imports;
var importSources = new List<RazorSourceDocument>();
foreach (var item in imports)
{
var sourceDocument = await GetRazorSourceDocumentAsync(item.Import);
importSources.Add(sourceDocument);
}
var documentSource = await GetRazorSourceDocumentAsync(document);
var projectEngine = project.GetProjectEngine();
var codeDocument = projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers);
var csharpDocument = codeDocument.GetCSharpDocument();
if (document is DefaultDocumentSnapshot defaultDocument)
{
defaultDocument.State.HostDocument.GeneratedCodeContainer.SetOutput(csharpDocument, defaultDocument);
}
return codeDocument;
}
private async Task<RazorSourceDocument> GetRazorSourceDocumentAsync(DocumentSnapshot document)
{
var sourceText = await document.GetTextAsync();
return sourceText.GetRazorSourceDocument(document.FilePath);
}
private async Task<IReadOnlyList<ImportItem>> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document)
{
var imports = new List<ImportItem>();
foreach (var snapshot in document.GetImports())
{
var versionStamp = await snapshot.GetTextVersionAsync();
imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot));
}
return imports;
}
private struct ImportItem : IEquatable<ImportItem>
{
public ImportItem(string filePath, VersionStamp versionStamp, DocumentSnapshot import)
{
FilePath = filePath;
VersionStamp = versionStamp;
Import = import;
}
public string FilePath { get; }
public VersionStamp VersionStamp { get; }
public DocumentSnapshot Import { get; }
public bool Equals(ImportItem other)
{
return
FilePathComparer.Instance.Equals(FilePath, other.FilePath) &&
VersionStamp == other.VersionStamp;
}
public override bool Equals(object obj)
{
return obj is ImportItem item ? Equals(item) : false;
}
public override int GetHashCode()
{
var hash = new HashCodeCombiner();
hash.Add(FilePath, FilePathComparer.Instance);
hash.Add(VersionStamp);
return hash;
}
}
}
}

View File

@ -1,167 +0,0 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class DocumentImportsTracker
{
private readonly object _lock;
private IReadOnlyList<DocumentSnapshot> _imports;
public DocumentImportsTracker()
{
_lock = new object();
}
public IReadOnlyList<DocumentSnapshot> GetImports(ProjectSnapshot project, DocumentSnapshot document)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (_imports == null)
{
lock (_lock)
{
if (_imports == null)
{
_imports = GetImportsCore(project, document);
}
}
}
return _imports;
}
private IReadOnlyList<DocumentSnapshot> GetImportsCore(ProjectSnapshot project, DocumentSnapshot document)
{
var projectEngine = project.GetProjectEngine();
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
var projectItem = projectEngine.FileSystem.GetItem(document.FilePath);
var importItems = importFeature?.GetImports(projectItem).Where(i => i.Exists);
if (importItems == null)
{
return Array.Empty<DocumentSnapshot>();
}
var imports = new List<DocumentSnapshot>();
foreach (var item in importItems)
{
if (item.PhysicalPath == null)
{
// This is a default import.
var defaultImport = new DefaultImportDocumentSnapshot(project, item);
imports.Add(defaultImport);
}
else
{
var import = project.GetDocument(item.PhysicalPath);
if (import == null)
{
// We are not tracking this document in this project. So do nothing.
continue;
}
imports.Add(import);
}
}
return imports;
}
private class DefaultImportDocumentSnapshot : DocumentSnapshot
{
private ProjectSnapshot _project;
private RazorProjectItem _importItem;
private SourceText _sourceText;
private VersionStamp _version;
private DocumentGeneratedOutputTracker _generatedOutput;
public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item)
{
_project = project;
_importItem = item;
_version = VersionStamp.Default;
_generatedOutput = new DocumentGeneratedOutputTracker(null);
}
public override string FilePath => null;
public override string TargetPath => null;
public override ProjectSnapshot Project => _project;
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
{
return _generatedOutput.GetGeneratedOutputInitializationTask(_project, this);
}
public override IReadOnlyList<DocumentSnapshot> GetImports()
{
return Array.Empty<DocumentSnapshot>();
}
public async override Task<SourceText> GetTextAsync()
{
using (var stream = _importItem.Read())
using (var reader = new StreamReader(stream))
{
var content = await reader.ReadToEndAsync();
_sourceText = SourceText.From(content);
}
return _sourceText;
}
public override Task<VersionStamp> GetTextVersionAsync()
{
return Task.FromResult(_version);
}
public override bool TryGetText(out SourceText result)
{
if (_sourceText != null)
{
result = _sourceText;
return true;
}
result = null;
return false;
}
public override bool TryGetTextVersion(out VersionStamp result)
{
result = _version;
return true;
}
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
{
if (_generatedOutput.IsResultAvailable)
{
result = GetGeneratedOutputAsync().Result;
return true;
}
result = null;
return false;
}
}
}
}

View File

@ -16,6 +16,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract ProjectSnapshot Project { get; }
public abstract bool SupportsOutput { get; }
public abstract IReadOnlyList<DocumentSnapshot> GetImports();
public abstract Task<SourceText> GetTextAsync();

View File

@ -2,7 +2,10 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
@ -18,14 +21,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private readonly object _lock;
private ComputedStateTracker _computedState;
private Func<Task<TextAndVersion>> _loader;
private Task<TextAndVersion> _loaderTask;
private SourceText _sourceText;
private VersionStamp? _version;
private DocumentGeneratedOutputTracker _generatedOutput;
private DocumentImportsTracker _imports;
public static DocumentState Create(
HostWorkspaceServices services,
HostDocument hostDocument,
@ -67,42 +69,35 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public GeneratedCodeContainer GeneratedCodeContainer => HostDocument.GeneratedCodeContainer;
public DocumentGeneratedOutputTracker GeneratedOutput
public bool IsGeneratedOutputResultAvailable => ComputedState.IsResultAvailable == true;
private ComputedStateTracker ComputedState
{
get
{
if (_generatedOutput == null)
if (_computedState == null)
{
lock (_lock)
{
if (_generatedOutput == null)
if (_computedState == null)
{
_generatedOutput = new DocumentGeneratedOutputTracker(null);
_computedState = new ComputedStateTracker(this);
}
}
}
return _generatedOutput;
return _computedState;
}
}
public DocumentImportsTracker Imports
public Task<(RazorCodeDocument output, VersionStamp inputVersion, VersionStamp outputVersion)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DefaultDocumentSnapshot document)
{
get
{
if (_imports == null)
{
lock (_lock)
{
if (_imports == null)
{
_imports = new DocumentImportsTracker();
}
}
}
return ComputedState.GetGeneratedOutputAndVersionAsync(project, document);
}
return _imports;
}
public IReadOnlyList<DocumentSnapshot> GetImports(DefaultProjectSnapshot project)
{
return GetImportsCore(project);
}
public async Task<SourceText> GetTextAsync()
@ -180,6 +175,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
state._version = _version;
state._loaderTask = _loaderTask;
// Do not cache computed state
return state;
}
@ -192,6 +189,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
state._version = _version;
state._loaderTask = _loaderTask;
// Optimisically cache the computed state
state._computedState = new ComputedStateTracker(state, _computedState);
return state;
}
@ -204,8 +204,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
state._version = _version;
state._loaderTask = _loaderTask;
// Opportunistically cache the generated code
state._generatedOutput = _generatedOutput?.Fork();
// Optimisically cache the computed state
state._computedState = new ComputedStateTracker(state, _computedState);
return state;
}
@ -217,6 +217,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(sourceText));
}
// Do not cache the computed state
return new DocumentState(Services, HostDocument, sourceText, version, null);
}
@ -227,7 +229,239 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(loader));
}
// Do not cache the computed state
return new DocumentState(Services, HostDocument, null, null, loader);
}
private IReadOnlyList<DocumentSnapshot> GetImportsCore(DefaultProjectSnapshot project)
{
var projectEngine = project.GetProjectEngine();
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
var projectItem = projectEngine.FileSystem.GetItem(HostDocument.FilePath);
var importItems = importFeature?.GetImports(projectItem);
if (importItems == null)
{
return Array.Empty<DocumentSnapshot>();
}
var imports = new List<DocumentSnapshot>();
foreach (var item in importItems)
{
if (item.PhysicalPath == null)
{
// This is a default import.
var defaultImport = new DefaultImportDocumentSnapshot(project, item);
imports.Add(defaultImport);
}
else
{
var import = project.GetDocument(item.PhysicalPath);
if (import == null)
{
// We are not tracking this document in this project. So do nothing.
continue;
}
imports.Add(import);
}
}
return imports;
}
// See design notes on ProjectState.ComputedStateTracker.
private class ComputedStateTracker
{
private readonly object _lock;
private ComputedStateTracker _older;
public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> TaskUnsafe;
public ComputedStateTracker(DocumentState state, ComputedStateTracker older = null)
{
_lock = state._lock;
_older = older;
}
public bool IsResultAvailable => TaskUnsafe?.IsCompleted == true;
public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DocumentSnapshot document)
{
if (project == null)
{
throw new ArgumentNullException(nameof(project));
}
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (TaskUnsafe == null)
{
lock (_lock)
{
if (TaskUnsafe == null)
{
TaskUnsafe = GetGeneratedOutputAndVersionCoreAsync(project, document);
}
}
}
return TaskUnsafe;
}
private async Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionCoreAsync(DefaultProjectSnapshot project, DocumentSnapshot document)
{
// We only need to produce the generated code if any of our inputs is newer than the
// previously cached output.
//
// First find the versions that are the inputs:
// - The project + computed state
// - The imports
// - This document
//
// All of these things are cached, so no work is wasted if we do need to generate the code.
var computedStateVersion = await project.State.GetComputedStateVersionAsync(project).ConfigureAwait(false);
var documentCollectionVersion = project.State.DocumentCollectionVersion;
var imports = await GetImportsAsync(project, document).ConfigureAwait(false);
var documentVersion = await document.GetTextVersionAsync().ConfigureAwait(false);
// OK now that have the previous output and all of the versions, we can see if anything
// has changed that would require regenerating the code.
var inputVersion = documentVersion;
if (inputVersion.GetNewerVersion(computedStateVersion) == computedStateVersion)
{
inputVersion = computedStateVersion;
}
if (inputVersion.GetNewerVersion(documentCollectionVersion) == documentCollectionVersion)
{
inputVersion = documentCollectionVersion;
}
for (var i = 0; i < imports.Count; i++)
{
var importVersion = imports[i].Version;
if (inputVersion.GetNewerVersion(importVersion) == importVersion)
{
inputVersion = importVersion;
}
}
RazorCodeDocument olderOutput = null;
var olderInputVersion = default(VersionStamp);
var olderOutputVersion = default(VersionStamp);
if (_older?.TaskUnsafe != null)
{
(olderOutput, olderInputVersion, olderOutputVersion) = await _older.TaskUnsafe.ConfigureAwait(false);
if (inputVersion.GetNewerVersion(olderInputVersion) == olderInputVersion)
{
// Nothing has changed, we can use the cached result.
lock (_lock)
{
TaskUnsafe = _older.TaskUnsafe;
_older = null;
return (olderOutput, olderInputVersion, olderOutputVersion);
}
}
}
// OK we have to generate the code.
var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false);
var importSources = new List<RazorSourceDocument>();
foreach (var item in imports)
{
var sourceDocument = await GetRazorSourceDocumentAsync(item.Document).ConfigureAwait(false);
importSources.Add(sourceDocument);
}
var documentSource = await GetRazorSourceDocumentAsync(document).ConfigureAwait(false);
var projectEngine = project.GetProjectEngine();
var codeDocument = projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers);
var csharpDocument = codeDocument.GetCSharpDocument();
// OK now we've generated the code. Let's check if the output is actually different. This is
// a valuable optimization for our use cases because lots of changes you could make require
// us to run code generation, but don't change the result.
//
// Note that we're talking about the effect on the generated C# code here (not the other artifacts).
// This is the reason why we have two versions associated with the output.
//
// The INPUT version is related the .cshtml files and tag helpers
// The OUTPUT version is related to the generated C#.
//
// Examples:
//
// A change to a tag helper not used by this document - updates the INPUT version, but not
// the OUTPUT version.
//
// A change in the HTML - updates the INPUT version, but not the OUTPUT version.
//
//
// Razor IDE features should always retrieve the output and party on it regardless. Depending
// on the use cases we may or may not need to synchronize the output.
var outputVersion = inputVersion;
if (olderOutput != null)
{
if (string.Equals(
olderOutput.GetCSharpDocument().GeneratedCode,
csharpDocument.GeneratedCode,
StringComparison.Ordinal))
{
outputVersion = olderOutputVersion;
}
}
if (document is DefaultDocumentSnapshot defaultDocument)
{
defaultDocument.State.HostDocument.GeneratedCodeContainer.SetOutput(
defaultDocument,
csharpDocument,
inputVersion,
outputVersion);
}
return (codeDocument, inputVersion, outputVersion);
}
private async Task<RazorSourceDocument> GetRazorSourceDocumentAsync(DocumentSnapshot document)
{
var sourceText = await document.GetTextAsync();
return sourceText.GetRazorSourceDocument(document.FilePath);
}
private async Task<IReadOnlyList<ImportItem>> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document)
{
var imports = new List<ImportItem>();
foreach (var snapshot in document.GetImports())
{
var versionStamp = await snapshot.GetTextVersionAsync();
imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot));
}
return imports;
}
private readonly struct ImportItem
{
public ImportItem(string filePath, VersionStamp version, DocumentSnapshot document)
{
FilePath = filePath;
Version = version;
Document = document;
}
public string FilePath { get; }
public VersionStamp Version { get; }
public DocumentSnapshot Document { get; }
}
}
}
}

View File

@ -13,7 +13,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public event EventHandler<TextChangeEventArgs> GeneratedCodeChanged;
private SourceText _source;
private VersionStamp? _sourceVersion;
private VersionStamp? _inputVersion;
private VersionStamp? _outputVersion;
private RazorCSharpDocument _output;
private DocumentSnapshot _latestDocument;
@ -37,13 +38,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
public VersionStamp SourceVersion
public VersionStamp InputVersion
{
get
{
lock (_setOutputLock)
{
return _sourceVersion.Value;
return _inputVersion.Value;
}
}
}
public VersionStamp OutputVersion
{
get
{
lock (_setOutputLock)
{
return _outputVersion.Value;
}
}
}
@ -81,19 +93,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
public void SetOutput(RazorCSharpDocument csharpDocument, DefaultDocumentSnapshot document)
public void SetOutput(
DefaultDocumentSnapshot document,
RazorCSharpDocument output,
VersionStamp inputVersion,
VersionStamp outputVersion)
{
lock (_setOutputLock)
{
if (!document.TryGetTextVersion(out var version))
{
Debug.Fail("The text version should have already been evaluated.");
return;
}
if (_sourceVersion.HasValue &&
_sourceVersion != version &&
_sourceVersion == SourceVersion.GetNewerVersion(version))
if (_inputVersion.HasValue &&
_inputVersion != inputVersion &&
_inputVersion == _inputVersion.Value.GetNewerVersion(inputVersion))
{
// Latest document is newer than the provided document.
return;
@ -106,10 +116,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
_source = source;
_sourceVersion = version;
_output = csharpDocument;
_inputVersion = inputVersion;
_outputVersion = outputVersion;
_output = output;
_latestDocument = document;
_textContainer.SetText(SourceText.From(Output.GeneratedCode));
_textContainer.SetText(SourceText.From(_output.GeneratedCode));
}
}

View File

@ -1,88 +0,0 @@
// 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.Collections.Immutable;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectEngineTracker
{
private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged;
private readonly object _lock = new object();
private readonly HostWorkspaceServices _services;
private RazorProjectEngine _projectEngine;
public ProjectEngineTracker(ProjectState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
_services = state.Services;
}
public ProjectEngineTracker ForkFor(ProjectState state, ProjectDifference difference)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if ((difference & Mask) != 0)
{
return null;
}
return this;
}
public RazorProjectEngine GetProjectEngine(ProjectState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if (_projectEngine == null)
{
lock (_lock)
{
if (_projectEngine == null)
{
var factory = _services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
_projectEngine = factory.Create(state.HostProject.Configuration, Path.GetDirectoryName(state.HostProject.FilePath), configure: null);
}
}
}
return _projectEngine;
}
public List<string> GetImportDocumentTargetPaths(ProjectState state, string targetPath)
{
var projectEngine = GetProjectEngine(state);
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
var projectItem = projectEngine.FileSystem.GetItem(targetPath);
var importItems = importFeature?.GetImports(projectItem).Where(i => i.FilePath != null);
// Target path looks like `Foo\\Bar.cshtml`
var targetPaths = new List<string>();
foreach (var importItem in importItems)
{
var itemTargetPath = importItem.FilePath.Replace('/', '\\').TrimStart('\\');
targetPaths.Add(itemTargetPath);
}
return targetPaths;
}
}
}

View File

@ -4,7 +4,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
@ -13,12 +16,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
// Internal tracker for DefaultProjectSnapshot
internal class ProjectState
{
private const ProjectDifference ClearComputedStateMask = ProjectDifference.ConfigurationChanged;
private const ProjectDifference ClearCachedTagHelpersMask =
ProjectDifference.ConfigurationChanged |
ProjectDifference.WorkspaceProjectAdded |
ProjectDifference.WorkspaceProjectChanged |
ProjectDifference.WorkspaceProjectRemoved;
private const ProjectDifference ClearDocumentCollectionVersionMask =
ProjectDifference.ConfigurationChanged |
ProjectDifference.DocumentAdded |
ProjectDifference.DocumentRemoved;
private static readonly ImmutableDictionary<string, DocumentState> EmptyDocuments = ImmutableDictionary.Create<string, DocumentState>(FilePathComparer.Instance);
private static readonly ImmutableDictionary<string, ImmutableArray<string>> EmptyImportsToRelatedDocuments = ImmutableDictionary.Create<string, ImmutableArray<string>>(FilePathComparer.Instance);
private readonly object _lock;
private ProjectEngineTracker _projectEngine;
private ProjectTagHelperTracker _tagHelpers;
private ComputedStateTracker _computedState;
public static ProjectState Create(HostWorkspaceServices services, HostProject hostProject, Project workspaceProject = null)
{
@ -34,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return new ProjectState(services, hostProject, workspaceProject);
}
private ProjectState(
HostWorkspaceServices services,
HostProject hostProject,
@ -46,6 +61,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Documents = EmptyDocuments;
ImportsToRelatedDocuments = EmptyImportsToRelatedDocuments;
Version = VersionStamp.Create();
DocumentCollectionVersion = Version;
_lock = new object();
}
@ -88,8 +104,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_lock = new object();
_projectEngine = older._projectEngine?.ForkFor(this, difference);
_tagHelpers = older._tagHelpers?.ForkFor(this, difference);
if ((difference & ClearDocumentCollectionVersionMask) == 0)
{
// Document collection hasn't changed
DocumentCollectionVersion = older.DocumentCollectionVersion;
}
else
{
DocumentCollectionVersion = Version;
}
if ((difference & ClearComputedStateMask) == 0 && older._computedState != null)
{
// Optimistically cache the RazorProjectEngine.
_computedState = new ComputedStateTracker(this, older._computedState);
}
if ((difference & ClearCachedTagHelpersMask) == 0 && _computedState != null)
{
// It's OK to keep the computed Tag Helpers.
_computedState.TaskUnsafe = older._computedState?.TaskUnsafe;
}
}
// Internal set for testing.
@ -104,46 +139,68 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public Project WorkspaceProject { get; }
/// <summary>
/// Gets the version of this project, INCLUDING content changes. The <see cref="Version"/> is
/// incremented for each new <see cref="ProjectState"/> instance created.
/// </summary>
public VersionStamp Version { get; }
// Computed State
public ProjectEngineTracker ProjectEngine
/// <summary>
/// Gets the version of this project, NOT INCLUDING computed or content changes. The
/// <see cref="DocumentCollectionVersion"/> is incremented each time the configuration changes or
/// a document is added or removed.
/// </summary>
public VersionStamp DocumentCollectionVersion { get; }
public RazorProjectEngine ProjectEngine => ComputedState.ProjectEngine;
public bool IsTagHelperResultAvailable => ComputedState.TaskUnsafe?.IsCompleted == true;
private ComputedStateTracker ComputedState
{
get
{
if (_projectEngine == null)
if (_computedState == null)
{
lock (_lock)
{
if (_projectEngine == null)
if (_computedState == null)
{
_projectEngine = new ProjectEngineTracker(this);
_computedState = new ComputedStateTracker(this);
}
}
}
return _projectEngine;
return _computedState;
}
}
// Computed State
public ProjectTagHelperTracker TagHelpers
/// <summary>
/// Gets the version of this project based on the computed state, NOT INCLUDING content
/// changes. The computed state is guaranteed to change when the configuration or tag helpers
/// change.
/// </summary>
/// <returns>Asynchronously returns the computed version.</returns>
public async Task<VersionStamp> GetComputedStateVersionAsync(ProjectSnapshot snapshot)
{
get
if (snapshot == null)
{
if (_tagHelpers == null)
{
lock (_lock)
{
if (_tagHelpers == null)
{
_tagHelpers = new ProjectTagHelperTracker(this);
}
}
}
return _tagHelpers;
throw new ArgumentNullException(nameof(snapshot));
}
var (_, version) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false);
return version;
}
public async Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync(ProjectSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
var (tagHelpers, _) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false);
return tagHelpers;
}
public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func<Task<TextAndVersion>> loader)
@ -164,13 +221,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
return this;
}
var documents = Documents.Add(hostDocument.FilePath, DocumentState.Create(Services, hostDocument, loader));
// Compute the effect on the import map
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, hostDocument.TargetPath);
var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath);
var importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths);
// Now check if the updated document is an import - it's important this this happens after
// updating the imports map.
if (importsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments))
@ -196,7 +253,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
return this;
}
var documents = Documents.Remove(hostDocument.FilePath);
// First check if the updated document is an import - it's important that this happens
@ -210,7 +267,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
// Compute the effect on the import map
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, hostDocument.TargetPath);
var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath);
var importsToRelatedDocuments = RemoveFromImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths);
var state = new ProjectState(this, ProjectDifference.DocumentRemoved, HostProject, WorkspaceProject, documents, importsToRelatedDocuments);
@ -280,7 +337,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
return this;
}
var documents = Documents.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.WithConfigurationChange(), FilePathComparer.Instance);
// If the host project has changed then we need to recompute the imports map
@ -288,11 +345,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
foreach (var document in documents)
{
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, document.Value.HostDocument.TargetPath);
var importTargetPaths = GetImportDocumentTargetPaths(document.Value.HostDocument.TargetPath);
importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, document.Value.HostDocument, importTargetPaths);
}
var state = new ProjectState(this, ProjectDifference.ConfigurationChanged, hostProject, WorkspaceProject, documents, importsToRelatedDocuments);
return state;
}
@ -367,5 +423,114 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return importsToRelatedDocuments;
}
private RazorProjectEngine CreateProjectEngine()
{
var factory = Services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
return factory.Create(HostProject.Configuration, Path.GetDirectoryName(HostProject.FilePath), configure: null);
}
public List<string> GetImportDocumentTargetPaths(string targetPath)
{
var projectEngine = ComputedState.ProjectEngine;
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
var projectItem = projectEngine.FileSystem.GetItem(targetPath);
var importItems = importFeature?.GetImports(projectItem).Where(i => i.FilePath != null);
// Target path looks like `Foo\\Bar.cshtml`
var targetPaths = new List<string>();
foreach (var importItem in importItems)
{
var itemTargetPath = importItem.FilePath.Replace('/', '\\').TrimStart('\\');
targetPaths.Add(itemTargetPath);
}
return targetPaths;
}
// ComputedStateTracker is the 'holder' of all of the state that can be cached based on
// the data in a ProjectState. It should not hold onto a ProjectState directly
// as that could lead to things being in memory longer than we want them to.
//
// Rather, a ComputedStateTracker instance can hold on to a previous instance from an older
// version of the same project.
private class ComputedStateTracker
{
// ProjectState.Version
private readonly VersionStamp _projectStateVersion;
private readonly object _lock;
private ComputedStateTracker _older; // We be set to null when state is computed
public Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> TaskUnsafe;
public ComputedStateTracker(ProjectState state, ComputedStateTracker older = null)
{
_projectStateVersion = state.Version;
_lock = state._lock;
_older = older;
ProjectEngine = _older?.ProjectEngine;
if (ProjectEngine == null)
{
ProjectEngine = state.CreateProjectEngine();
}
}
public RazorProjectEngine ProjectEngine { get; }
public Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> GetTagHelpersAndVersionAsync(ProjectSnapshot snapshot)
{
if (TaskUnsafe == null)
{
lock (_lock)
{
if (TaskUnsafe == null)
{
TaskUnsafe = GetTagHelpersAndVersionCoreAsync(snapshot);
}
}
}
return TaskUnsafe;
}
private async Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> GetTagHelpersAndVersionCoreAsync(ProjectSnapshot snapshot)
{
// Don't allow synchronous execution - we expect this to always be called with the lock.
await Task.Yield();
var services = ((DefaultProjectSnapshot)snapshot).State.Services;
var resolver = services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<TagHelperResolver>();
var tagHelpers = (await resolver.GetTagHelpersAsync(snapshot).ConfigureAwait(false)).Descriptors;
if (_older?.TaskUnsafe != null)
{
// We have something to diff against.
var (olderTagHelpers, olderVersion) = await _older.TaskUnsafe.ConfigureAwait(false);
var difference = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
difference.UnionWith(olderTagHelpers);
difference.SymmetricExceptWith(tagHelpers);
if (difference.Count == 0)
{
lock (_lock)
{
// Everything is the same. Return the cached version.
TaskUnsafe = _older.TaskUnsafe;
_older = null;
return (olderTagHelpers, olderVersion);
}
}
}
lock (_lock)
{
_older = null;
return (tagHelpers, _projectStateVersion);
}
}
}
}
}

View File

@ -1,79 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class ProjectTagHelperTracker
{
private const ProjectDifference Mask =
ProjectDifference.ConfigurationChanged |
ProjectDifference.WorkspaceProjectAdded |
ProjectDifference.WorkspaceProjectChanged |
ProjectDifference.WorkspaceProjectRemoved;
private readonly object _lock = new object();
private readonly HostWorkspaceServices _services;
private Task<IReadOnlyList<TagHelperDescriptor>> _task;
public ProjectTagHelperTracker(ProjectState state)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
_services = state.Services;
}
public bool IsResultAvailable => _task?.IsCompleted == true;
public ProjectTagHelperTracker ForkFor(ProjectState state, ProjectDifference difference)
{
if (state == null)
{
throw new ArgumentNullException(nameof(state));
}
if ((difference & Mask) != 0)
{
return null;
}
return this;
}
public Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTask(ProjectSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
if (_task == null)
{
lock (_lock)
{
if (_task == null)
{
_task = GetTagHelperInitializationTaskCore(snapshot);
}
}
}
return _task;
}
private async Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTaskCore(ProjectSnapshot snapshot)
{
var resolver = _services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<TagHelperResolver>();
return (await resolver.GetTagHelpersAsync(snapshot)).Descriptors;
}
}
}

View File

@ -94,30 +94,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)));
}
[Fact]
public void ProjectSnapshot_CachesTagHelperTask()
{
// Arrange
TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
try
{
var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject);
var snapshot = new DefaultProjectSnapshot(state);
// Act
var task1 = snapshot.GetTagHelpersAsync();
var task2 = snapshot.GetTagHelpersAsync();
// Assert
Assert.Same(task1, task2);
}
finally
{
TagHelperResolver.CompletionSource.SetCanceled();
}
}
[Fact]
public void IsImportDocument_NonImportDocument_ReturnsFalse()
{

View File

@ -33,13 +33,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
SomeTagHelpers = new List<TagHelperDescriptor>();
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
Document = TestProjectData.SomeProjectFile1;
HostDocument = TestProjectData.SomeProjectFile1;
Text = SourceText.From("Hello, world!");
TextLoader = () => Task.FromResult(TextAndVersion.Create(Text, VersionStamp.Create()));
}
private HostDocument Document { get; }
private HostDocument HostDocument { get; }
private HostProject HostProject { get; }
@ -64,7 +64,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_CreatedNew_HasEmptyText()
{
// Arrange & Act
var state = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
var state = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
// Assert
var text = await state.GetTextAsync();
@ -75,7 +75,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_WithText_CreatesNewState()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
// Act
var state = original.WithText(Text, VersionStamp.Create());
@ -89,7 +89,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_WithTextLoader_CreatesNewState()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
// Act
var state = original.WithTextLoader(TextLoader);
@ -103,7 +103,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void DocumentState_WithConfigurationChange_CachesSnapshotText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithText(Text, VersionStamp.Create());
// Act
@ -118,7 +118,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_WithConfigurationChange_CachesLoadedText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithTextLoader(TextLoader);
await original.GetTextAsync();
@ -135,7 +135,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void DocumentState_WithImportsChange_CachesSnapshotText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithText(Text, VersionStamp.Create());
// Act
@ -150,7 +150,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_WithImportsChange_CachesLoadedText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithTextLoader(TextLoader);
await original.GetTextAsync();
@ -167,7 +167,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void DocumentState_WithWorkspaceProjectChange_CachesSnapshotText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithText(Text, VersionStamp.Create());
// Act
@ -182,7 +182,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public async Task DocumentState_WithWorkspaceProjectChange_CachesLoadedText()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
.WithTextLoader(TextLoader);
await original.GetTextAsync();
@ -194,20 +194,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.True(state.TryGetText(out _));
Assert.True(state.TryGetTextVersion(out _));
}
[Fact]
public void DocumentState_WithWorkspaceProjectChange_TriesToCacheGeneratedOutput()
{
// Arrange
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
GC.KeepAlive(original.GeneratedOutput);
// Act
var state = original.WithWorkspaceProjectChange();
// Assert
Assert.Same(state.GeneratedOutput.Older, original.GeneratedOutput);
}
}
}

View File

@ -15,22 +15,26 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void SetOutput_AcceptsSameVersionedDocuments()
{
// Arrange
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
var services = TestWorkspace.Create().Services;
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
var projectState = ProjectState.Create(services, hostProject);
var project = new DefaultProjectSnapshot(projectState);
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
var text = SourceText.From("...");
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Default);
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
var documentState = new DocumentState(services, hostDocument, text, VersionStamp.Default, () => Task.FromResult(textAndVersion));
var document = new DefaultDocumentSnapshot(project, documentState);
var newDocument = new DefaultDocumentSnapshot(project, documentState);
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
var version = VersionStamp.Create();
var container = new GeneratedCodeContainer();
container.SetOutput(csharpDocument, document);
container.SetOutput(document, csharpDocument, version, version);
// Act
container.SetOutput(csharpDocument, newDocument);
container.SetOutput(newDocument, csharpDocument, version, version);
// Assert
Assert.Same(newDocument, container.LatestDocument);
@ -40,20 +44,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public void SetOutput_AcceptsInitialOutput()
{
// Arrange
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
var services = TestWorkspace.Create().Services;
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
var projectState = ProjectState.Create(services, hostProject);
var project = new DefaultProjectSnapshot(projectState);
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
var text = SourceText.From("...");
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Default);
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
var documentState = new DocumentState(services, hostDocument, text, VersionStamp.Default, () => Task.FromResult(textAndVersion));
var document = new DefaultDocumentSnapshot(project, documentState);
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
var version = VersionStamp.Create();
var container = new GeneratedCodeContainer();
// Act
container.SetOutput(csharpDocument, document);
container.SetOutput(document, csharpDocument, version, version);
// Assert
Assert.NotNull(container.LatestDocument);

View File

@ -0,0 +1,264 @@
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class ProjectStateGeneratedOutputTest : WorkspaceTestBase
{
public ProjectStateGeneratedOutputTest()
{
HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0);
var projectId = ProjectId.CreateNewId("Test");
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
projectId,
VersionStamp.Default,
"Test",
"Test",
LanguageNames.CSharp,
TestProjectData.SomeProject.FilePath));
WorkspaceProject = solution.GetProject(projectId);
SomeTagHelpers = new List<TagHelperDescriptor>();
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
HostDocument = TestProjectData.SomeProjectFile1;
Text = SourceText.From("Hello, world!");
TextLoader = () => Task.FromResult(TextAndVersion.Create(Text, VersionStamp.Create()));
}
private HostDocument HostDocument { get; }
private HostProject HostProject { get; }
private HostProject HostProjectWithConfigurationChange { get; }
private Project WorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; } = new TestTagHelperResolver();
private List<TagHelperDescriptor> SomeTagHelpers { get; }
private Func<Task<TextAndVersion>> TextLoader { get; }
private SourceText Text { get; }
protected override void ConfigureLanguageServices(List<ILanguageService> services)
{
services.Add(TagHelperResolver);
}
protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder)
{
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
builder.Features.Add(new TestImportProjectFeature());
}
[Fact]
public async Task HostDocumentAdded_CachesOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var state = original.WithAddedHostDocument(TestProjectData.AnotherProjectFile1, DocumentState.EmptyLoader);
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.Same(originalOutput, actualOutput);
Assert.Equal(originalInputVersion, actualInputVersion);
Assert.Equal(originalOutputVersion, actualOutputVersion);
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualOutputVersion);
}
[Fact]
public async Task HostDocumentAdded_Import_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var state = original.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.Equal(originalOutputVersion, actualOutputVersion);
Assert.Equal(state.DocumentCollectionVersion, actualInputVersion);
}
[Fact]
public async Task HostDocumentChanged_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var version = VersionStamp.Create();
var state = original.WithChangedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version));
});
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
Assert.Equal(version, actualInputVersion);
}
[Fact]
public async Task HostDocumentChanged_Import_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var version = VersionStamp.Create();
var state = original.WithChangedHostDocument(TestProjectData.SomeProjectImportFile, () =>
{
return Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version));
});
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
Assert.Equal(version, actualInputVersion);
}
[Fact]
public async Task HostDocumentRemoved_Import_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var state = original.WithRemovedHostDocument(TestProjectData.SomeProjectImportFile);
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.Equal(originalOutputVersion, actualOutputVersion);
Assert.Equal(state.DocumentCollectionVersion, actualInputVersion);
}
[Fact]
public async Task WorkspaceProjectChange_CachesOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2"));
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.Same(originalOutput, actualOutput);
Assert.Equal(originalInputVersion, actualInputVersion);
Assert.Equal(originalOutputVersion, actualOutputVersion);
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
}
// The generated code's text doesn't change as a result, so the output version does not change
[Fact]
public async Task WorkspaceProjectChange_WithTagHelperChange_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
TagHelperResolver.TagHelpers = SomeTagHelpers;
// Act
var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2"));
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.Equal(originalOutputVersion, actualOutputVersion);
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
}
[Fact]
public async Task ConfigurationChange_DoesNotCacheOutput()
{
// Arrange
var original =
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
// Act
var state = original.WithHostProject(HostProjectWithConfigurationChange);
// Assert
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
Assert.NotSame(originalOutput, actualOutput);
Assert.NotEqual(originalInputVersion, actualInputVersion);
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
}
private static Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetOutputAsync(ProjectState project, HostDocument hostDocument)
{
var document = project.Documents[hostDocument.FilePath];
return GetOutputAsync(project, document);
}
private static Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetOutputAsync(ProjectState project, DocumentState document)
{
var projectSnapshot = new DefaultProjectSnapshot(project);
var documentSnapshot = new DefaultDocumentSnapshot(projectSnapshot, document);
return document.GetGeneratedOutputAndVersionAsync(projectSnapshot, documentSnapshot);
}
}
}

View File

@ -17,8 +17,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public ProjectStateTest()
{
TagHelperResolver = new TestTagHelperResolver();
HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0);
HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0);
@ -56,7 +54,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
private Project WorkspaceProject { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private TestTagHelperResolver TagHelperResolver { get; set; }
private List<TagHelperDescriptor> SomeTagHelpers { get; }
@ -66,6 +64,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
protected override void ConfigureLanguageServices(List<ILanguageService> services)
{
TagHelperResolver = new TestTagHelperResolver();
services.Add(TagHelperResolver);
}
@ -103,6 +102,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Collection(
state.Documents.OrderBy(kvp => kvp.Key),
d => Assert.Same(Documents[0], d.Value.HostDocument));
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact] // When we first add a document, we have no way to read the text, so it's empty.
@ -138,6 +138,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
d => Assert.Same(Documents[2], d.Value.HostDocument),
d => Assert.Same(Documents[0], d.Value.HostDocument),
d => Assert.Same(Documents[1], d.Value.HostDocument));
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact]
@ -225,7 +226,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
[Fact]
public void ProjectState_AddHostDocument_RetainsComputedState()
public async Task ProjectState_AddHostDocument_RetainsComputedState()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -233,15 +234,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader);
// Assert
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.Same(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
@ -278,6 +283,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var text = await state.Documents[Documents[1].FilePath].GetTextAsync();
Assert.Same(Text, text);
Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact]
@ -296,10 +303,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var text = await state.Documents[Documents[1].FilePath].GetTextAsync();
Assert.Same(Text, text);
Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact]
public void ProjectState_WithChangedHostDocument_Loader_RetainsComputedState()
public async Task ProjectState_WithChangedHostDocument_Loader_RetainsComputedState()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -307,21 +316,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithChangedHostDocument(Documents[1], TextLoader);
// Assert
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState()
public async Task ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -329,15 +342,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithChangedHostDocument(Documents[1], Text, VersionStamp.Create());
// Assert
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
@ -389,6 +406,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Collection(
state.Documents.OrderBy(kvp => kvp.Key),
d => Assert.Same(Documents[2], d.Value.HostDocument));
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact]
@ -454,7 +473,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
[Fact]
public void ProjectState_RemoveHostDocument_RetainsComputedState()
public async Task ProjectState_RemoveHostDocument_RetainsComputedState()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -462,15 +481,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithRemovedHostDocument(Documents[2]);
// Assert
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.Same(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
@ -491,7 +514,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
[Fact]
public void ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState()
public async Task ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -499,8 +522,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
TagHelperResolver.TagHelpers = SomeTagHelpers;
// Act
var state = original.WithHostProject(HostProjectWithConfigurationChange);
@ -509,15 +534,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.NotEqual(original.Version, state.Version);
Assert.Same(HostProjectWithConfigurationChange, state.HostProject);
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
Assert.NotSame(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.NotSame(originalTagHelpers, actualTagHelpers);
Assert.NotEqual(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
}
[Fact]
public void ProjectState_WithHostProject_NoConfigurationChange_Noops()
public async Task ProjectState_WithHostProject_NoConfigurationChange_Noops()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -525,8 +556,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithHostProject(HostProject);
@ -560,7 +591,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
[Fact]
public void ProjectState_WithWorkspaceProject_Removed()
public async Task ProjectState_WithWorkspaceProject_Removed()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -568,8 +599,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithWorkspaceProject(null);
@ -578,15 +609,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.NotEqual(original.Version, state.Version);
Assert.Null(state.WorkspaceProject);
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
// The configuration didn't change, and the tag helpers didn't actually change
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
}
[Fact]
public void ProjectState_WithWorkspaceProject_Added()
public async Task ProjectState_WithWorkspaceProject_Added()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, null)
@ -594,8 +630,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
// Act
var state = original.WithWorkspaceProject(WorkspaceProject);
@ -604,15 +640,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.NotEqual(original.Version, state.Version);
Assert.Same(WorkspaceProject, state.WorkspaceProject);
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
// The configuration didn't change, and the tag helpers didn't actually change
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
}
[Fact]
public void ProjectState_WithWorkspaceProject_Changed()
public async Task ProjectState_WithWorkspaceProject_Changed()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
@ -620,8 +661,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
GC.KeepAlive(original.ProjectEngine);
GC.KeepAlive(original.TagHelpers);
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
var changed = WorkspaceProject.WithAssemblyName("Test1");
@ -632,8 +673,50 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.NotEqual(original.Version, state.Version);
Assert.Same(changed, state.WorkspaceProject);
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
// The configuration didn't change, and the tag helpers didn't actually change
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotSame(original.TagHelpers, state.TagHelpers);
Assert.Same(originalTagHelpers, actualTagHelpers);
Assert.Equal(originalComputedVersion, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
}
[Fact]
public async Task ProjectState_WithWorkspaceProject_Changed_TagHelpersChanged()
{
// Arrange
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
.WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader)
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
// Force init
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
var changed = WorkspaceProject.WithAssemblyName("Test1");
// Now create some tag helpers
TagHelperResolver.TagHelpers = SomeTagHelpers;
// Act
var state = original.WithWorkspaceProject(changed);
// Assert
Assert.NotEqual(original.Version, state.Version);
Assert.Same(changed, state.WorkspaceProject);
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
// The configuration didn't change, but the tag helpers did
Assert.Same(original.ProjectEngine, state.ProjectEngine);
Assert.NotEqual(originalTagHelpers, actualTagHelpers);
Assert.NotEqual(originalComputedVersion, actualComputedVersion);
Assert.Equal(state.Version, actualComputedVersion);
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);

View File

@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor
{
public TaskCompletionSource<TagHelperResolutionResult> CompletionSource { get; set; }
public IList<TagHelperDescriptor> TagHelpers { get; } = new List<TagHelperDescriptor>();
public IList<TagHelperDescriptor> TagHelpers { get; set; } = new List<TagHelperDescriptor>();
public override Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
{

View File

@ -19,7 +19,7 @@ using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherTestBase
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherWorkspaceTestBase
{
public DefaultVisualStudioDocumentTrackerTest()
{
@ -32,27 +32,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
ImportDocumentManager = Mock.Of<ImportDocumentManager>();
WorkspaceEditorSettings = new DefaultWorkspaceEditorSettings(Mock.Of<ForegroundDispatcher>(), Mock.Of<EditorSettingsManager>());
TagHelperResolver = new TestTagHelperResolver();
SomeTagHelpers = new List<TagHelperDescriptor>()
{
TagHelperDescriptorBuilder.Create("test", "test").Build(),
};
HostServices = TestServices.Create(
new IWorkspaceService[] { },
new ILanguageService[] { TagHelperResolver, });
Workspace = TestWorkspace.Create(HostServices, w =>
{
WorkspaceProject = w.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
new VersionStamp(),
"Test1",
"TestAssembly",
LanguageNames.CSharp,
filePath: ProjectPath));
});
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace) { AllowNotifyListeners = true };
HostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_1);
@ -92,16 +76,29 @@ namespace Microsoft.VisualStudio.Editor.Razor
private List<TagHelperDescriptor> SomeTagHelpers { get; }
private TestTagHelperResolver TagHelperResolver { get; }
private TestTagHelperResolver TagHelperResolver { get; set; }
private ProjectSnapshotManagerBase ProjectManager { get; }
private HostServices HostServices { get; }
private Workspace Workspace { get; }
private DefaultVisualStudioDocumentTracker DocumentTracker { get; }
protected override void ConfigureLanguageServices(List<ILanguageService> services)
{
TagHelperResolver = new TestTagHelperResolver();
services.Add(TagHelperResolver);
}
protected override void ConfigureWorkspace(AdhocWorkspace workspace)
{
WorkspaceProject = workspace.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(),
new VersionStamp(),
"Test1",
"TestAssembly",
LanguageNames.CSharp,
filePath: TestProjectData.SomeProject.FilePath));
}
[ForegroundFact]
public void Subscribe_NoopsIfAlreadySubscribed()
{
@ -561,7 +558,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
await DocumentTracker.PendingTagHelperTask;
// Assert
Assert.Same(DocumentTracker.TagHelpers, SomeTagHelpers);
Assert.Same(SomeTagHelpers, DocumentTracker.TagHelpers);
Assert.Collection(
args,