diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs new file mode 100644 index 0000000000..6c972f271a --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactory.cs @@ -0,0 +1,101 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + // This is hardcoded for now. A more complete design would fan out to a list of providers. + internal class DefaultProjectExtensibilityConfigurationFactory : ProjectExtensibilityConfigurationFactory + { + private const string MvcAssemblyName = "Microsoft.AspNetCore.Mvc.Razor"; + private const string RazorV1AssemblyName = "Microsoft.AspNetCore.Razor"; + private const string RazorV2AssemblyName = "Microsoft.AspNetCore.Razor.Language"; + + // Using MaxValue here so that we ignore patch and build numbers. We only want to compare major/minor. + private static readonly Version MaxSupportedRazorVersion = new Version(2, 0, Int32.MaxValue, Int32.MaxValue); + private static readonly Version MaxSupportedMvcVersion = new Version(2, 0, Int32.MaxValue, Int32.MaxValue); + + private static readonly Version DefaultRazorVersion = new Version(2, 0, 0, 0); + private static readonly Version DefaultMvcVersion = new Version(2, 0, 0, 0); + + public async override Task GetConfigurationAsync(Project project, CancellationToken cancellationToken = default(CancellationToken)) + { + if (project == null) + { + throw new ArgumentNullException(nameof(project)); + } + + var compilation = await project.GetCompilationAsync(cancellationToken); + return GetConfiguration(compilation.ReferencedAssemblyNames); + } + + // internal/separate for testing. + internal ProjectExtensibilityConfiguration GetConfiguration(IEnumerable references) + { + // Avoiding ToDictionary here because we don't want a crash if there is a duplicate name. + var assemblies = new Dictionary(); + foreach (var assembly in references) + { + assemblies[assembly.Name] = assembly; + } + + // First we look for the V2+ Razor Assembly. If we find this then its version is the correct Razor version. + AssemblyIdentity razorAssembly; + if (assemblies.TryGetValue(RazorV2AssemblyName, out razorAssembly)) + { + if (razorAssembly.Version == null || razorAssembly.Version > MaxSupportedRazorVersion) + { + // This is a newer Razor version than we know, treat it as a fallback case. + razorAssembly = null; + } + } + else if (assemblies.TryGetValue(RazorV1AssemblyName, out razorAssembly)) + { + // This assembly only counts as the 'Razor' assembly if it's a version lower than 2.0.0. + if (razorAssembly.Version == null || razorAssembly.Version >= new Version(2, 0, 0, 0)) + { + razorAssembly = null; + } + } + + AssemblyIdentity mvcAssembly; + if (assemblies.TryGetValue(MvcAssemblyName, out mvcAssembly)) + { + if (mvcAssembly.Version == null || mvcAssembly.Version > MaxSupportedMvcVersion) + { + // This is a newer MVC version than we know, treat it as a fallback case. + mvcAssembly = null; + } + } + + if (razorAssembly != null && mvcAssembly != null) + { + // This means we've definitely found a supported Razor version and an MVC version. + return new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.ApproximateMatch, + new ProjectExtensibilityAssembly(razorAssembly), + new ProjectExtensibilityAssembly(mvcAssembly)); + } + + // If we get here it means we didn't find everything, so we have to guess. + if (razorAssembly == null || razorAssembly.Version == null) + { + razorAssembly = new AssemblyIdentity(RazorV2AssemblyName, DefaultRazorVersion); + } + + if (mvcAssembly == null || mvcAssembly.Version == null) + { + mvcAssembly = new AssemblyIdentity(MvcAssemblyName, DefaultMvcVersion); + } + + return new MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind.Fallback, + new ProjectExtensibilityAssembly(razorAssembly), + new ProjectExtensibilityAssembly(mvcAssembly)); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs new file mode 100644 index 0000000000..93d7400ee3 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryFactory.cs @@ -0,0 +1,25 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + [Shared] + [ExportLanguageServiceFactory(typeof(ProjectExtensibilityConfigurationFactory), RazorLanguage.Name)] + internal class DefaultProjectExtensibilityConfigurationFactoryFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + if (languageServices == null) + { + throw new ArgumentNullException(nameof(languageServices)); + } + + return new DefaultProjectExtensibilityConfigurationFactory(); + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs new file mode 100644 index 0000000000..febbf2a738 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/MvcExtensibilityConfiguration.cs @@ -0,0 +1,41 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class MvcExtensibilityConfiguration : ProjectExtensibilityConfiguration + { + public MvcExtensibilityConfiguration( + ProjectExtensibilityConfigurationKind kind, + ProjectExtensibilityAssembly razorAssembly, + ProjectExtensibilityAssembly mvcAssembly) + { + if (razorAssembly == null) + { + throw new ArgumentNullException(nameof(razorAssembly)); + } + + if (mvcAssembly == null) + { + throw new ArgumentNullException(nameof(mvcAssembly)); + } + + Kind = kind; + RazorAssembly = razorAssembly; + MvcAssembly = mvcAssembly; + + Assemblies = new[] { RazorAssembly, MvcAssembly, }; + } + + public override IReadOnlyList Assemblies { get; } + + public override ProjectExtensibilityConfigurationKind Kind { get; } + + public override ProjectExtensibilityAssembly RazorAssembly { get; } + + public ProjectExtensibilityAssembly MvcAssembly { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs new file mode 100644 index 0000000000..64b7a78706 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityAssembly.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal sealed class ProjectExtensibilityAssembly + { + public ProjectExtensibilityAssembly(AssemblyIdentity identity) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + Identity = identity; + } + + public AssemblyIdentity Identity { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs new file mode 100644 index 0000000000..15cf8d5429 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfiguration.cs @@ -0,0 +1,16 @@ +// 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.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectExtensibilityConfiguration + { + public abstract IReadOnlyList Assemblies { get; } + + public abstract ProjectExtensibilityConfigurationKind Kind { get; } + + public abstract ProjectExtensibilityAssembly RazorAssembly { get; } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs new file mode 100644 index 0000000000..71d929dd29 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationFactory.cs @@ -0,0 +1,14 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal abstract class ProjectExtensibilityConfigurationFactory : ILanguageService + { + public abstract Task GetConfigurationAsync(Project project, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs new file mode 100644 index 0000000000..0efc5e5e37 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectExtensibilityConfigurationKind.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + /// + /// Describes how closely the configuration of Razor tooling matches the actual project dependencies. + /// + internal enum ProjectExtensibilityConfigurationKind + { + ApproximateMatch, + Fallback, + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs new file mode 100644 index 0000000000..4df841a401 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectExtensibilityConfigurationFactoryTest.cs @@ -0,0 +1,193 @@ +// 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 Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class DefaultProjectExtensibilityConfigurationFactoryTest + { + [Theory] + [InlineData("1.0.0.0", "1.0.0.0")] + [InlineData("1.1.0.0", "1.1.0.0")] + [InlineData("2.0.0.0", "2.0.0.0")] + [InlineData("2.0.2.0", "2.0.2.0")] + public void GetConfiguration_FindsSupportedConfiguration_ForNewRazor(string razorVersion, string mvcVersion) + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version(razorVersion)), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version(mvcVersion)), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); + Assert.Equal(razorVersion, configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal(mvcVersion, configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Theory] + [InlineData("1.0.0.0", "1.0.0.0")] + [InlineData("1.1.0.0", "1.1.0.0")] + [InlineData("1.9.9.9", "2.0.0.0")] // MVC version is ignored + public void GetConfiguration_FindsSupportedConfiguration_ForOldRazor(string razorVersion, string mvcVersion) + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version(razorVersion)), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version(mvcVersion)), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); + Assert.Equal(razorVersion, configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal(mvcVersion, configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_RazorVersion_NewAssemblyWinsOverOld() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("1.0.0.0")), + new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.ApproximateMatch, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_RazorVersion_OldAssemblyIgnoredPastV1() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor", new Version("2.0.0.0")), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_NoRazorVersion_ChoosesDefault() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_UnsupportedRazorVersion_ChoosesDefault() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("3.0.0.0")), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("2.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_NoMvcVersion_ChoosesDefault() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + + [Fact] + public void GetConfiguration_UnsupportedMvcVersion_ChoosesDefault() + { + // Arrange + var references = new AssemblyIdentity[] + { + new AssemblyIdentity("Microsoft.AspNetCore.Razor.Language", new Version("2.0.0.0")), + new AssemblyIdentity("Microsoft.AspNetCore.Mvc.Razor", new Version("3.0.0.0")), + }; + + var factory = new DefaultProjectExtensibilityConfigurationFactory(); + + // Act + var result = factory.GetConfiguration(references); + + // Assert + var configuration = Assert.IsType(result); + Assert.Equal(ProjectExtensibilityConfigurationKind.Fallback, configuration.Kind); + Assert.Equal("2.0.0.0", configuration.RazorAssembly.Identity.Version.ToString()); + Assert.Equal("2.0.0.0", configuration.MvcAssembly.Identity.Version.ToString()); + } + } +}