diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorProject.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorProject.cs
new file mode 100644
index 0000000000..98d3a386f7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorProject.cs
@@ -0,0 +1,20 @@
+// 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.AspNetCore.Razor.Evolution
+{
+ ///
+ /// An abstraction for working with a project containing Razor files.
+ ///
+ public abstract class RazorProject
+ {
+ ///
+ /// Gets a sequence of under the specific path in the project.
+ ///
+ /// The base path.
+ /// The sequence of .
+ public abstract IEnumerable EnumerateItems(string basePath);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorProjectItem.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorProjectItem.cs
new file mode 100644
index 0000000000..108bc8c498
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorProjectItem.cs
@@ -0,0 +1,104 @@
+// 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.Diagnostics;
+using System.IO;
+
+namespace Microsoft.AspNetCore.Razor.Evolution
+{
+ ///
+ /// An item in .
+ ///
+ [DebuggerDisplay("{CombinedPath}")]
+ public abstract class RazorProjectItem
+ {
+ ///
+ /// Path specified in .
+ ///
+ public abstract string BasePath { get; }
+
+ ///
+ /// Path relative to .
+ ///
+ public abstract string Path { get; }
+
+ ///
+ /// The absolute path to the file, including the file name.
+ ///
+ public abstract string PhysicalPath { get; }
+
+ ///
+ /// Gets the file contents as readonly .
+ ///
+ /// The .
+ public abstract Stream Read();
+
+ ///
+ /// The root relative path of the item.
+ ///
+ public virtual string CombinedPath
+ {
+ get
+ {
+ if (BasePath == "/")
+ {
+ return Path;
+ }
+ else
+ {
+ return BasePath + Path;
+ }
+ }
+ }
+
+ ///
+ /// The extension of the file.
+ ///
+ public virtual string Extension
+ {
+ get
+ {
+ var index = Filename.LastIndexOf('.');
+ if (index == -1)
+ {
+ return null;
+ }
+ else
+ {
+ return Filename.Substring(index);
+ }
+ }
+ }
+
+ ///
+ /// The name of the file including the extension.
+ ///
+ public virtual string Filename
+ {
+ get
+ {
+ var index = Path.LastIndexOf('/');
+ return Path.Substring(index + 1);
+ }
+ }
+
+ ///
+ /// Path relative to without the extension.
+ ///
+ public virtual string PathWithoutExtension
+ {
+ get
+ {
+ var index = Path.LastIndexOf('.');
+ if (index == -1)
+ {
+ return Path;
+ }
+ else
+ {
+ return Path.Substring(0, index);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorProjectItemTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorProjectItemTest.cs
new file mode 100644
index 0000000000..26b830968a
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorProjectItemTest.cs
@@ -0,0 +1,133 @@
+// 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.IO;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Razor.Evolution
+{
+ public class RazorProjectItemTest
+ {
+ [Fact]
+ public void CombinedPath_ReturnsPathIfBasePathIsEmpty()
+ {
+ // Arrange
+ var emptyBasePath = "/";
+ var path = "/foo/bar.cshtml";
+ var projectItem = new TestRazorProjectItem(emptyBasePath, path);
+
+ // Act
+ var combinedPath = projectItem.CombinedPath;
+
+ // Assert
+ Assert.Equal(path, combinedPath);
+ }
+
+ [Theory]
+ [InlineData("/root", "/root/foo/bar.cshtml")]
+ [InlineData("root/subdir", "root/subdir/foo/bar.cshtml")]
+ public void CombinedPath_ConcatsPaths(string basePath, string expected)
+ {
+ // Arrange
+ var path = "/foo/bar.cshtml";
+ var projectItem = new TestRazorProjectItem(basePath, path);
+
+ // Act
+ var combinedPath = projectItem.CombinedPath;
+
+ // Assert
+ Assert.Equal(expected, combinedPath);
+ }
+
+ [Theory]
+ [InlineData("/Home/Index")]
+ [InlineData("EditUser")]
+ public void Extension_ReturnsNullIfFileDoesNotHaveExtension(string path)
+ {
+ // Arrange
+ var projectItem = new TestRazorProjectItem("/views", path);
+
+ // Act
+ var extension = projectItem.Extension;
+
+ // Assert
+ Assert.Null(extension);
+ }
+
+ [Theory]
+ [InlineData("/Home/Index.cshtml", ".cshtml")]
+ [InlineData("/Home/Index.en-gb.cshtml", ".cshtml")]
+ [InlineData("EditUser.razor", ".razor")]
+ public void Extension_ReturnsFileExtension(string path, string expected)
+ {
+ // Arrange
+ var projectItem = new TestRazorProjectItem("/views", path);
+
+ // Act
+ var extension = projectItem.Extension;
+
+ // Assert
+ Assert.Equal(expected, extension);
+ }
+
+ [Theory]
+ [InlineData("Home/Index.cshtml", "Index.cshtml")]
+ [InlineData("/Accounts/Customers/Manage-en-us.razor", "Manage-en-us.razor")]
+ public void FileName_ReturnsFileNameWithExtension(string path, string expected)
+ {
+ // Arrange
+ var projectItem = new TestRazorProjectItem("/", path);
+
+ // Act
+ var fileName = projectItem.Filename;
+
+ // Assert
+ Assert.Equal(expected, fileName);
+ }
+
+ [Theory]
+ [InlineData("Home/Index", "Home/Index")]
+ [InlineData("Home/Index.cshtml", "Home/Index")]
+ [InlineData("/Accounts/Customers/Manage.en-us.razor", "/Accounts/Customers/Manage.en-us")]
+ [InlineData("/Accounts/Customers/Manage-en-us.razor", "/Accounts/Customers/Manage-en-us")]
+ public void PathWithoutExtension_ExcludesExtension(string path, string expected)
+ {
+ // Arrange
+ var projectItem = new TestRazorProjectItem("/", path);
+
+ // Act
+ var fileName = projectItem.PathWithoutExtension;
+
+ // Assert
+ Assert.Equal(expected, fileName);
+ }
+
+
+ private class TestRazorProjectItem : RazorProjectItem
+ {
+ public TestRazorProjectItem(string basePath, string path)
+ {
+ BasePath = basePath;
+ Path = path;
+ }
+
+ public override string BasePath { get; }
+
+ public override string Path { get; }
+
+ public override string PhysicalPath
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override Stream Read()
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}