templating icon indicating copy to clipboard operation
templating copied to clipboard

In item templates, is it possible to determine things like project name / namespace?

Open tintoy opened this issue 7 years ago • 21 comments

Hi.

I've been wanting to build item templates for MVC controllers and views, but these require knowledge of things like the current project namespace (or, for nested folders, something even more complex, like joining sub-folders onto that as sub-namespaces).

I've had a browse through the code, but haven't found anything like that (apart from perhaps replacing the entire project name with the target project name, even in source code?) so before I go any further, I wanted to check if this is supposed to be a supported scenario.

Thanks,

Adam.

tintoy avatar Feb 16 '17 10:02 tintoy

Hi @tintoy, these values can be accepted via parameters but cannot be figured out automatically at this time. As far as the usage of these names go (file names/namespaces/literal strings/etc), we're tracking the work to make that possible with #141. For collecting the name of the project in the target directory, this could be done with implementations of IMacro (discussed how to get started with that here).

Without these, in terms of how you'd handle this if the user provided the parent namespace, let's say you had the following layout on disk

.template.config\
   template.json
Classes\
   TheClass.cs

Let's also say that TheClass.cs looks like this

namespace Company.Application.Classes
{
    public class TheClass
    {
    }
}

If you want to use the parent project's namespace rather than Company.Application, you'd add the symbol that takes the namespace like so:

"symbols": {
    "ParentProjectNamspace": {
        "type": "parameter",
        "replaces": "Company.Application",
        "dataType": "string",
        "defaultValue": "Company.Application"
    }
}

With this, if the user did something like this dotnet new TheClassTemplate --ParentProjectNamespace Project1.Stuff -n Hello, the resulting file (at Classes\Hello.cs) will look like:

namespace Project1.Stuff.Classes
{
    public class Hello
    {
    }
}

mlorbetske avatar Feb 22 '17 07:02 mlorbetske

Thanks, that's super useful! :)

I might start experimenting with this on one of my simpler templates for now (a binary PowerShell Core module), which the above should probably cover nicely. And I'll spend this weekend taking a closer look at the extensibility stuff from #286.

tintoy avatar Feb 22 '17 07:02 tintoy

Moving this to the backlog as this may be something interesting to add as a "generated" type symbol that would know how to look at existing content to try to determine the namespace. With the new fallback symbols, the user would still be able to override the value.

mlorbetske avatar Apr 27 '17 16:04 mlorbetske

Yeah this will be interesting and important. Not sure how to solve this in a good way though.

sayedihashimi avatar Apr 27 '17 17:04 sayedihashimi

I'm not sure how set in stone the template.json schema is. It would seem to me, if you could add a parameter to the schema that would allow the template generator to know it needs to go look for a project file and grab the root namespace that would certainly help.

dansiegel avatar May 08 '17 01:05 dansiegel

Properties can be added as they won't be understood by older versions. However, the approach mentioned doesn't require a schema modification (it fits nicely within the existing extensibility model); it's also the approach used for port generation & has evolution steps planned to pretty up the syntax.

Moreover, for .NET Core, the namespace isn't usually defined in the project file, the tooling makes some inferences about the project name & locations of the files relative to it to determine the target namespace. Even if that were reliably present, to keep item namespaces consistent with the convention being used in the user's existing artifacts, it should really be looking at the peer code files in the target location to make the determination as to what the target namespace is. This means that, whatever the component is that gets built, it'll need to understand the target language(s) for the template & be able to reason about the computed value (thinking of "flat namespace" projects which would normally conflict with item templates with opinionated namespacing for files generated into multiple directories).

mlorbetske avatar May 08 '17 04:05 mlorbetske

This is currently affecting the dotnet new page item template too (tripped up a bunch of folks in our workshop). Great that we can pass the namespace in, but very easy to forget :smile:

DamianEdwards avatar Jun 13 '17 11:06 DamianEdwards

Having done a bunch of work with the MSBuild engine recently, I think I'm fairly confident that I know how to evaluate / compute the project namespace for a given folder location. If you guys are interested, I could take a look at implementing a symbol generator to do this.

tintoy avatar Sep 05 '17 15:09 tintoy

$(RootNamespace) always has some sort of value in managed projects (anything that imports Microsoft.Common.CurrentVersion.targets and friends), and the rules for inferring it otherwise are pretty simple.

Root namespace (in fallback order):

  1. $(RootNamespace)
  2. $(MSBuildProjectName)

Relative namespace:

  • In project directory: RootNamespace
  • In project subdirectory: RootNamespace + '.' + TargetDirRelativeToProject.Replace(PathSeparatorChar, '.')

tintoy avatar Sep 05 '17 15:09 tintoy

(and it's not hard to use Microsoft.Build.Evaluation to inspect the effective value of the RootNamespace property in a project)

tintoy avatar Sep 05 '17 15:09 tintoy

to keep item namespaces consistent with the convention being used in the user's existing artifacts, it should really be looking at the peer code files in the target location to make the determination as to what the target namespace is

That'd be nice but, since Visual Studio doesn't do this either, not having it still keeps this at parity with the in-GUI experience :)

tintoy avatar Sep 05 '17 15:09 tintoy

Ok, so here's a quick proof-of-concept:

using Microsoft.Build.Evaluation;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;

namespace FileNSDemo
{
    static class Program
    {
        static void Main(string[] args)
        {
            try
            {
                FileInfo targetFile = new FileInfo(args[0]);
                Project owningProject = GetOwningProject(targetFile);
                if (owningProject == null)
                    return;

                string fileNamespace = owningProject.GetPropertyValue("RootNamespace");
                if (String.IsNullOrWhiteSpace(fileNamespace))
                    fileNamespace = Path.GetFileNameWithoutExtension(owningProject.FullPath);

                string relativeDirectory = Path.GetRelativePath(owningProject.DirectoryPath, targetFile.Directory.FullName);
                if (relativeDirectory != ".")
                    fileNamespace += "." + relativeDirectory.Replace(Path.DirectorySeparatorChar, '.');

                Console.WriteLine("File '{0}' is owned by project '{1}' (namespace would be '{2}').",
                    targetFile.FullName,
                    owningProject.FullPath,
                    fileNamespace
                );                
            }
            catch (Exception unexpectedError)
            {
                Console.WriteLine(unexpectedError);
            }
        }

        static Project GetOwningProject(FileInfo file)
        {
            DirectoryInfo directory = file.Directory;
            if (!directory.Exists)
            {
                Console.WriteLine("Directory '{0}' does not exist.", directory.FullName);

                return null;
            }

            while (directory != null)
            {
                FileInfo projectFile = directory.EnumerateFiles("*.*proj").FirstOrDefault();
                if (projectFile != null)
                {
                    string fileRelativePath = file.FullName.Substring(directory.FullName.Length + 1);
                    using (ProjectCollection projectCollection = MSBuildHelper.CreateProjectCollection(directory.FullName))
                    {
                        Project project = projectCollection.LoadProject(projectFile.FullName);
                        ICollection<ProjectItem> matchingFileItems = project.GetItemsByEvaluatedInclude(fileRelativePath);
                        if (matchingFileItems.Count > 0)
                            return project;
                    }
                }

                directory = directory.Parent;
            }

            return null;
        }
    }
}

(fully functional example here)

tintoy avatar Sep 05 '17 19:09 tintoy

Thanks for the PoC @tintoy, I'm hesitant to take a dependency on MSBuild here though due to coordination concerns with all the different release vehicles for this project.

mlorbetske avatar Sep 06 '17 19:09 mlorbetske

Yeah, I think I see what you're getting at :)

Then again, given that dotnet new generates projects files that MSBuild has to be able to parse, isn't it already dependent on a version (or version range) of MSBuild?

tintoy avatar Sep 06 '17 21:09 tintoy

Actually, I think I now get where you're coming from - the issue is distributing the MSBuild engine alongside dotnet new, correct?

tintoy avatar Sep 06 '17 21:09 tintoy

This issue was last touched some years ago. We are working on a new delivery road map. Please reopen if this is something we want & we'll properly assess its' priority compared to other work aimed at improving the overall templating UX.

donJoseLuis avatar Mar 19 '20 13:03 donJoseLuis

Yes, I'd like this re-opened, please!

Please can I have something that can evaluate anything MSBuild can evaluate?

The dotnet new template approach of letting me use compileable code as a template is a brilliant improvement on old-style templates. But this poses challenges for item templates in particular.

"symbols": {
    "anythingMSBuildCanDoICanDoToo": {
        "type": "computed-but-can-be-overriden-by-a-parameter",
        "dataType": "string",
        "replaces": "NamespaceForNewItem",
        "value": "string.Join('.' ,  $RootNamespace, $DottedRelativePathToProjectDir,  $sourceName)"
    }
}

Without some kind of evaluator able to do string manipulation and/or variables and/or everything that an msbuild expression can do, item templates for code are semi-automated-semi-manual needs editing after running the template?

chrisfcarroll avatar Jul 10 '20 15:07 chrisfcarroll

Reviving this issue, imo it makes a lot of sense. @KathleenDollard could you please take a look?

vlada-shubina avatar Jul 05 '21 07:07 vlada-shubina

This is absolutely needed as I'm creating and distributing item templates for .NET MAUI, in which C# classes (both code-behind for XAML style definition and Direct C# style definition) require the C# class type a namespace.

At present, managing it as user input and that's not the ideal way to do it as the chances of making an error is high.

Unable to implicitly look up the value, my suggestion would be to expose the RootNamespace as a binding symbol quite like HostIdentifier.

That would ease out the job. Also, other MSBuild properties can also be exposed as binding symbols.

Notify to @sayedihashimi

Regards, Vijay Anand E G

egvijayanand avatar Feb 22 '22 04:02 egvijayanand

We talked about this recently, I believe that the associated work that is needed is described at https://github.com/dotnet/templating/issues/3107.

sayedihashimi avatar Feb 22 '22 18:02 sayedihashimi

This will be implemented in https://github.com/dotnet/templating/issues/3829

vlada-shubina avatar Apr 04 '22 11:04 vlada-shubina

Closed in https://github.com/dotnet/templating/issues/3829

vlada-shubina avatar Aug 15 '22 16:08 vlada-shubina