diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Extensions/EfControllerScaffolderBuilderExtensions.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Extensions/EfControllerScaffolderBuilderExtensions.cs
index d71586174..c0a719d1b 100644
--- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Extensions/EfControllerScaffolderBuilderExtensions.cs
+++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Extensions/EfControllerScaffolderBuilderExtensions.cs
@@ -177,7 +177,21 @@ public static IScaffoldBuilder WithMvcViewsStep(this IScaffoldBuilder builder)
//TODO add extensions if 'TemplateFoldersUtilities' is not reworked.
var allT4TemplatePaths = new TemplateFoldersUtilities().GetAllT4TemplatesForTargetFramework(["Views"], viewModel.ProjectInfo.ProjectPath);
- var viewTemplateProperties = ViewHelper.GetTextTemplatingProperties(allT4TemplatePaths, viewModel);
+
+ // Use controller root name for view folder to match MVC conventions
+ // e.g. BlogsController -> /Views/Blogs/
+ string? viewFolderName = null;
+ context.Properties.TryGetValue(nameof(EfControllerModel), out var efControllerModelObj);
+ if (efControllerModelObj is EfControllerModel efControllerModel)
+ {
+ var controllerName = efControllerModel.ControllerName;
+ const string controllerSuffix = "Controller";
+ viewFolderName = controllerName.EndsWith(controllerSuffix, StringComparison.Ordinal)
+ ? controllerName.Substring(0, controllerName.Length - controllerSuffix.Length)
+ : controllerName;
+ }
+
+ var viewTemplateProperties = ViewHelper.GetTextTemplatingProperties(allT4TemplatePaths, viewModel, viewFolderName);
if (viewTemplateProperties.Any())
{
step.TextTemplatingProperties = viewTemplateProperties;
diff --git a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/ViewHelper.cs b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/ViewHelper.cs
index 237575859..b6c4cf555 100644
--- a/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/ViewHelper.cs
+++ b/src/dotnet-scaffolding/dotnet-scaffold/AspNet/Helpers/ViewHelper.cs
@@ -40,8 +40,9 @@ internal class ViewHelper
///
/// The collection of all T4 template paths.
/// The view model containing project and model information.
+ /// An optional folder name for the view output path. If not provided, the model type name will be used.
/// An enumerable collection of instances.
- internal static IEnumerable GetTextTemplatingProperties(IEnumerable allT4TemplatePaths, ViewModel viewModel)
+ internal static IEnumerable GetTextTemplatingProperties(IEnumerable allT4TemplatePaths, ViewModel viewModel, string? viewFolderName = null)
{
var textTemplatingProperties = new List();
if (allT4TemplatePaths is not null && allT4TemplatePaths.Any())
@@ -57,7 +58,10 @@ internal static IEnumerable GetTextTemplatingProperties(
continue;
}
- string baseOutputPath = GetBaseOutputPath(viewModel.ModelInfo.ModelTypeName, viewModel.ProjectInfo.ProjectPath);
+ string folderName = viewFolderName is not null && IsValidFolderName(viewFolderName)
+ ? viewFolderName
+ : viewModel.ModelInfo.ModelTypeName;
+ string baseOutputPath = GetBaseOutputPath(folderName, viewModel.ProjectInfo.ProjectPath);
string outputFileName = Path.Combine(baseOutputPath, $"{templateName}{Common.Constants.ViewExtension}");
textTemplatingProperties.Add(new()
{
@@ -153,4 +157,12 @@ private static string GetBaseOutputPath(string modelName, string? projectPath)
string projectBasePath = Path.GetDirectoryName(projectPath) ?? Directory.GetCurrentDirectory();
return Path.Combine(projectBasePath, "Views", modelName);
}
+
+ private static bool IsValidFolderName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name)) return false;
+ if (name == "." || name == "..") return false;
+ if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) return false;
+ return true;
+ }
}
diff --git a/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Helpers/ViewHelperTests.cs b/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Helpers/ViewHelperTests.cs
index 84885a61e..6d008c846 100644
--- a/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Helpers/ViewHelperTests.cs
+++ b/test/dotnet-scaffolding/dotnet-scaffold.Tests/AspNet/Helpers/ViewHelperTests.cs
@@ -305,4 +305,85 @@ public void IndexTemplate_Constant_HasExpectedValue()
// Assert
Assert.Equal("Index.tt", ViewHelper.IndexTemplate);
}
+
+ [Fact]
+ public void GetTextTemplatingProperties_WithViewFolderName_UsesViewFolderNameInOutputPath()
+ {
+ // Arrange
+ string projectPath = Path.Combine("test", "project", "TestProject.csproj");
+ string templatePath = Path.Combine("templates", ViewHelper.CreateTemplate);
+ List templatePaths = [templatePath];
+
+ ViewModel viewModel = new ViewModel
+ {
+ PageType = "Create",
+ ModelInfo = new ModelInfo { ModelTypeName = "Blog" },
+ ProjectInfo = new ProjectInfo(projectPath),
+ DbContextInfo = new DbContextInfo()
+ };
+
+ // Act
+ IEnumerable result = ViewHelper.GetTextTemplatingProperties(templatePaths, viewModel, "Blogs");
+
+ // Assert
+ TextTemplatingProperty? property = result.FirstOrDefault();
+ Assert.NotNull(property);
+ Assert.Contains("Blogs", property.OutputPath);
+ Assert.DoesNotContain("Blog" + Path.DirectorySeparatorChar, property.OutputPath.Replace("Blogs", string.Empty));
+ }
+
+ [Fact]
+ public void GetTextTemplatingProperties_WithNullViewFolderName_FallsBackToModelTypeName()
+ {
+ // Arrange
+ string projectPath = Path.Combine("test", "project", "TestProject.csproj");
+ string templatePath = Path.Combine("templates", ViewHelper.CreateTemplate);
+ List templatePaths = [templatePath];
+
+ ViewModel viewModel = new ViewModel
+ {
+ PageType = "Create",
+ ModelInfo = new ModelInfo { ModelTypeName = "Product" },
+ ProjectInfo = new ProjectInfo(projectPath),
+ DbContextInfo = new DbContextInfo()
+ };
+
+ // Act
+ IEnumerable result = ViewHelper.GetTextTemplatingProperties(templatePaths, viewModel, null);
+
+ // Assert
+ TextTemplatingProperty? property = result.FirstOrDefault();
+ Assert.NotNull(property);
+ Assert.Contains("Product", property.OutputPath);
+ }
+
+ [Theory]
+ [InlineData("..")]
+ [InlineData(".")]
+ [InlineData("Views/../secrets")]
+ [InlineData("foo/bar")]
+ [InlineData("foo\\bar")]
+ public void GetTextTemplatingProperties_WithInvalidViewFolderName_FallsBackToModelTypeName(string invalidFolderName)
+ {
+ // Arrange
+ string projectPath = Path.Combine("test", "project", "TestProject.csproj");
+ string templatePath = Path.Combine("templates", ViewHelper.CreateTemplate);
+ List templatePaths = [templatePath];
+
+ ViewModel viewModel = new ViewModel
+ {
+ PageType = "Create",
+ ModelInfo = new ModelInfo { ModelTypeName = "Product" },
+ ProjectInfo = new ProjectInfo(projectPath),
+ DbContextInfo = new DbContextInfo()
+ };
+
+ // Act
+ IEnumerable result = ViewHelper.GetTextTemplatingProperties(templatePaths, viewModel, invalidFolderName);
+
+ // Assert
+ TextTemplatingProperty? property = result.FirstOrDefault();
+ Assert.NotNull(property);
+ Assert.Contains("Product", property.OutputPath);
+ }
}