templating
templating copied to clipboard
Extra folders when using solution template in Visual Studio
I have a solution template that creates a solution and 2 projects. The output structure should look like this:
- MyName.sln
- src
- MyName
- MyName.csproj
- MyName
- tests
- MyName.UnitTests
- MyName.UnitTests.csproj
- MyName.UnitTests
When I run my template in the CLI, it works as expected.
When I run my template from Visual Studio and check "Place solution and project in same directory", I get an extra MyName folder in my chosen location with everything else under it like this:
- MyName <- Not wanted
- MyName.sln
- src
- MyName
- MyName.csproj
- MyName
- tests
- MyName.UnitTests
- MyName.UnitTests.csproj
- MyName.UnitTests
Also, when doing this, I get an error message saying the solution has been modified outside of the environment. I think this is related to #3282. However, I don't understand the proposed solutions because they seem to result in a Visual Studio generated solution having a name other than MyName.sln which is not what I'm after.
When I run my template from Visual Studio and uncheck "Place solution and project in same directory", I don't get the error message about the solution being modified outside of the environment. However, I get an even worse folder structure that looks like:
- MyName <- Not wanted
- MyName.sln
- MyName <- Not wanted
- src
- MyName
- MyName.csproj
- MyName
- tests
- MyName.UnitTests
- MyName.UnitTests.csproj
- MyName.UnitTests
- src
Is there a way for me to generate the structure I want regardless of it being CLI or VS?
I realize that some of this may be due to existing VS behavior. For a while, it has often created a new parent folder for every solution and it's kind of maddening. But the goal of the template is to make it easy for other devs to bootstrap new development with some corporate standards. I don't want to have to instruct them to be sure to check "Place solution and project in same directory", then close VS, copy the contents to the parent directory, delete the extraneous folder, and then open the solution in VS again.
Thanks
@phenning could you please check this one? Thank you.
@agilenut What exactly are you specifying for the location?

There will always be a folder created under the specified Location regardless of whether place solution and project in same directory is checked. Visual Studio does this to isolate the newly generated template from the parent location to avoid the case where there were already templates or projects in that location. If it didn't do this, a template could potentially overwrite already existing user files.
If Place solution and project in same directory is checked, the folder structure will be:
- source
- repos
- MyName (project name, as specified in the project dialog)
- MyName.sln (VS created SLN)
- (template gets created here)
- MyName (project name, as specified in the project dialog)
- repos
If the option is NOT selected, the folder structure will be:
- source
- repos
- SolutionName (Solution Name as specified in the project dialog, defaults to project name unless overwritten)
- SolutionName.sln (VS created SLN)
- ProjectName (this is the default name passed to the template (MyName)
- (template gets created here)
- ProjectName (this is the default name passed to the template (MyName)
- SolutionName.sln (VS created SLN)
- SolutionName (Solution Name as specified in the project dialog, defaults to project name unless overwritten)
- repos
The issue you are seeing about the solution file being modified, is because you are creating your template SLN file as MyName.sln, which will result in a conflict of the Visual Studio generated SLN. The guidance here is to generate your sln file something like MyName.generated.sln for Visual Studio, but have a source rename so that when the CLI creates it the proper file name is used (https://github.com/sayedihashimi/template-sample#how-to-create-a-multi-project-solution-template):
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"rename": {
"MyName.generated.sln": "MyName.sln"
}
Additionally in 17.1, you can also utilize the following binds to get the Visual Studio specific options which were chosen on the new project dialog. You can then use these in conditionals in template.json or your content.
SoluitionName (The name of the Solution the project is being added to) IsExclusive (True if being new solution is being created in via NPD, False if adding to existing solution) CreateSolutionDirectory (Set to True if the above mentioned checkbox is UNCHECKED, False otherwise, including when adding to existing project)
Usage:
"IsExclusive": { // you can name the symbol whatever you want, the important thing is the binding below - that must match
"type": "bind",
"binding": "context:IsExclusive" // or "context:solutionName", etc
}
Thanks for the response @phenning.
Some Background
Our process is to create a new repository in Azure DevOps, set all of the appropriate policies, clone locally, create a feature branch, then run a template in that newly created branch under the repo folder. While I'm sure we have a few organization specific steps, I assume that this basic process of 1) create repository 2) add code is common. I'd venture to guess it is the most common.
Our repositories are named in lower-kebab-case. While we have some more specific naming conventions, I'd also suggest that this repo name style is very common.
Our repositories follow David Fowler's suggested structure for .Net repositories. This means the solution file is in the root of the repository folder. We also follow common convention to have the solution file named using dot separated PascalCase (e.g. MyOrg.MySolution.sln).
The template is meant to make it easy to add code directly to the repo folder after creating a new repo and follow all of these best practices. Again, some of this is style and there is no official standard but I'd argue that each of these styles are very common and seen in multiple open source repositories. In fact, even this repository looks to be mostly using those conventions. This repository also has a repository name that does not match the solution file at the root.
Given all this, the best way I have found to run the template in Visual Studio is to set location to be the location of the newly cloned repo (e.g. C:\...\source\repos\my-cloned-repository).
If I were to choose C:\...\source\repos as the location and pass the name of my repository my-repostory, then it will fail because the cloned repository folder already exists. Even if it did work, it would use my-repository as the name argument which would impact my solution file name, project names, folder names, and namespaces which would not be correct.
So, the only choice is to choose C:\...\source\repos\my-repository. And, once I do that, it creates a separate sub-folder that I do not want to contain my solution.
I suppose I could also change our process, though I'd rather not, such that, for new repositories, we first run the template, then git init, then push to the server and go configure the policy options. However, that would still have the issue that the value passed in as the Project Name would be used for both the repository folder and the name argument, which again is not correct. In other words, VS is forcing its assumption that the parent folder and the solution name should be the same when that is frequently not the case.
Some Conclusions on Visual Studio Integration
- I applaud the new ability to have the same template work in both CLI and VS. I think that feature is worth continuing to build out.
- The fact that the behavior of the CLI and the behavior of VS differ in how the results are rendered makes for a bad developer experience. These experiences need to be brought more inline so that developers aren't left trying to workaround the differences.
- Some of the differences between CLI and VS produce issues, as in this case, that I'm not sure can be easily worked around. If you know of a way, please let me know.
- It should not be assumed that the parent folder / repo and the solution name are the same. These will often not be the same. So, they should not use the same input parameter.
- I do not see a reason for VS to "protect" developers from themselves when creating a solution. I normally already have created the folder that I want the solution file to be placed in. And, I have the ability to choose that location and set the solution name. So, it is my own fault if I overwrite anything. Plus, VS could warn that something is going to be overwritten if it really was going to happen. Now, this decision is a little different when creating a new project because you are normally working at the solution root and the project folder usually doesn't exist yet. So, I can understand the desire to have VS create the folder for you so that you don't have to type the name of the project in both the location and the project dialog box. Ultimately, it seems like whether to create a surrounding folder should be opt-in for both cases.
So, in an effort to share templates between both CLI and VS, make CLI and VS behave similarly, be flexible for the different needs, and create sane defaults, I'd suggest that both CLI and VS templates support an optional parameter to either opt-in or opt-out of the extra folder. I'd probably do that for both projects and solutions just to make them consistent. And, that option should probably allow you to set the name to be different (not just a boolean). I'm not quite sure yet but I think the bindings you have mentioned could be interesting. Though I don't yet see how they can fix the issue with VS always creating this unwanted folder.
Also, if I recall, VS previously had a checkbox that allowed you to choose if you wanted to create the extra folder. I believe it was removed for some reason because I remember there were a lot of blog articles around that time explaining how to work around the problem (which does not really work because it assumes too much). And, I'm pretty sure I've seen some feedback asking for it to be re-added. I'm not completely sure of this. Just what I recall.
Modified Outside of Environment Issue
I'm not sure I understand the suggested workaround for this issue.
I now have a solution named MyApi.1.generated.sln. My sourceName is MyApi.1. I have also included the new conditional source in my sources array along with 2 other sources:
"sources": [
{
"source": "./",
"target": "./"
},
{
"source": "../EditorConfig/",
"target": "./",
"include": ".editorconfig"
},
{
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"rename": {
"MyApi.1.generated.sln": "MyApi.1.sln"
}
}
]
When I run the template in the CLI with --name TodoItemsApi, I get a solution file named TodoItemsApi.genrated.sln. But I'm expecting TodoItemsApi.sln.
This is the same result I get without the source entry in the array. So, I'm wondering if it is working correctly. I also tried moving the new source to be the first item in the sources array. I also tried making the ./ source exclude the solution and making the new conditional source include the solution. Nothing changed that behavior.
When I run the template in VS with the Project Name as TodoItemsApi, I do get a solution named TodoItemsApi.sln as I would expect. However, I still have the TodoItemsApi.generated.sln which I don't want.
New Bindings
The information on the new bindings is interesting. Thanks for that. Unfortunately, I haven't had enough time to play with them yet to figure out how exactly they might help this situation. I can see how it could allow me to react to their choices on creating an extra solution folder level or not. But really, I don't want to have to deal with any of those extra folder levels.
Thanks for the additional context about your scenario. This will help us to better understand what you are trying to achieve here.
One of the challenges here is how decoupled the various pieces are here as well as the exposed API surfaces between each distinct layer. There are actually three distinct layers here, the core template engine (assemblies in this repo), the Visual Studio new project dialog and the integration piece which sits between the new project dialog and the template engine. The new project dialog needs to continue to be able to support the vstemplate style of templates as well.
Any future changes we make here to potentially enable your scenario will need to take into account existing templates and the expected behavior for the more common cases, both first party and other third party project (non solution) templates.
I did notice a few specific things about your current template authoring which may be able to improve your experience here given the current Visual Studio behavior.
Firstly, I think there is some json missing from @sayedihashimi's example, which is why you are not seeing the rename work in the CLI.
The source rename you have is fine, except it needs to be in a modifers array:
"sources": [
{
"source": "./",
"target": "./"
},
{
"source": "../EditorConfig/",
"target": "./",
"include": ".editorconfig"
},
"modifiers": [
{
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"rename": {
"MyApi.1.generated.sln": "MyApi.1.sln"
}
}
]
]
Secondly, the generated solution file should get deleted in Visual Studio if you have specified "solution" as your type in the template.json
"tags": {
"type": "solution"
}
Out of curiosity, which version of Visual Studio have you primarily been testing on? The most current? I ask so that I could go back and see if there were any relevant fixes we may have made which you may not be seeing
Oh one other thing, you also need the following symbol defined, if you don't already.
"symbols": {
"HostIdentifier": {
"type": "bind",
"binding": "HostIdentifier"
}
}
Ah. I was missing the modifiers array as well as the HostIdentifier symbol.
I did have the type set to solution in the tags array.
After making those 2 changes, the solution file is now getting renamed when I run it in the CLI.
However, I was still getting a copy of the TodoItemsApi.generated.sln. It turns out that the source for ./ was copying that file in as well.
I was able to solve that by using the exclude property on the source. However, then I lost the defaults of the exclude and had to add them back. So, the sources now look like this:
"sources": [
{
"source": "./",
"target": "./",
"exclude": [
"MyApi.1.generated.sln",
"MyApi.1.generated.sln.DotSettings",
"**/[Bb]in/**",
"**/[Oo]bj/**",
".template.config/**/**",
"**/*.filelist",
"**/*.user",
"**/*.lock.json"
]
},
{
"source": "../EditorConfig/",
"target": "./",
"include": ".editorconfig"
},
{
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"modifiers": [
{
"rename": {
"MyApi.1.generated.sln": "MyApi.1.sln"
}
}
]
}
]
It's a little verbose to have to do this for every solution template and I'll miss out on any new defaults that get added to exclude in a future update, but it's workable. All of this might be good to have in some documentation because I think it will be required for every solution template.
I suppose another option might be to support conditions on a modifier instead of just the source so that I could put that modifier on the ./ source instead of having 2 sources that will scan the same solution file.
When I run this template from Visual Studio, now I only get the TodoItemsApi.sln file which is nice. However, I also am expecting a TodoItemsApi.sln.DotSettings file (ReSharper). This file is missing when I run the template with VS but it is present when run from CLI.
I tried to add the DotSettings file to the primaryOutputs array but that didn't seem to help.
As for the version of Visual Studio, yes, I am using latest of 2022. I would be fine explaining to consumers of the template that VS2019 and older versions are not supported.
As for the complexity of the dependencies and desire for backwards compatibility, I get it. I kind of expected it. But given the difference between the behavior in CLI and VS, and the fact that, at least in my opinion, the assumption VS is making is often not going to be the case, I think it would be great to put some thought into it to see what is possible.
I think I made some alignment and copy and paste errors, you should put the modifiers object as a property inside a source.
See the Blazor WASM template here: https://github.com/dotnet/aspnetcore/blob/0cdea60e07a16bd31a5b41c5d4eac3ebeff9acd8/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json#L64
Also, conditions are supported on individual modifiers, as you can see in the linked template.json.
I agree that getting the desired behavior for certain template patterns can be rather complex and we are looking to reduce the complexity as we can.
So, I think I did have the modifiers in a source object. But I put it in a separate source object than the primary ./ source because I didn't know you could use a condition on a modifier itself. So, next attempt...
"sources": [
{
"source": "./",
"target": "./",
"modifiers": [
{
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"rename": {
"MyApi.1.generated.sln": "MyApi.1.sln"
}
}
]
},
{
"source": "../EditorConfig/",
"target": "./",
"include": ".editorconfig"
}
]
This is much cleaner because I get rid of the extra source and the exclude. It also works correctly in CLI. However, in VS, I still get a TodoItemsApi.generated.sln in addition to the expected TodoItemsApi.sln and the DotSettings file is not being fully renamed. It is still TodoItemsApi.generated.sln.DotSettings.
If I change sources to this...
"sources": [
{
"source": "./",
"target": "./",
"modifiers": [
{
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
"rename": {
"MyApi.1.generated.sln": "MyApi.1.sln"
}
},
{
"condition": "(HostIdentifier != \"dotnetcli\" && HostIdentifier != \"dotnetcli-preview\")",
"rename": {
"MyApi.1.generated.sln.DotSettings": "MyApi.1.sln.DotSettings"
}
}
]
},
{
"source": "../EditorConfig/",
"target": "./",
"include": ".editorconfig"
}
]
Then, it still works correctly in CLI. And, when I run in VS, I now get the correct renamed DotSettings file of TodoItemsApi.sln.DotSettings.
However, I still have an extra TodoItemsApi.generated.sln file when run in VS.
I just wanted to let everyone know that we are considering some changes to the New Project Dialog which will improve these Solution templates. We don't have anything specific to share yet, but I'm hoping that we can get something going soon.
Sounds good. Looking forward to it.
the ticket was moved to https://developercommunity.visualstudio.com/t/Extra-folders-when-using-solution-templa/10300741