【问题标题】:T4 Template Consume connection from Server Explorer来自服务器资源管理器的 T4 模板使用连接
【发布时间】:2020-11-12 15:23:11
【问题描述】:

我正在尝试构建一个生成数据库模型和数据库上下文的项目模板,而不在源代码中存储连接信息。

我已经成功地将项目模板向导与服务器资源管理器连接起来,并且可以在 settings.ttinclude 中设置连接密钥。

问题是我无法从 DTE 解析到 IVsDataExplorerConnectionManager 的接口。

我相信我找错了树,因为这是在 VSIX 项目中获取服务器资源管理器的方法。我希望类似的代码适用于 T4 Visual Studio 模板引擎。

我花了几个小时查看是否有其他人已经做过类似的事情,但我什么也没找到。任何关于如何在 T4 模板中使用来自服务器资源管理器的连接的想法都将不胜感激。

2020 年 7 月 23 日更新

我后来了解到,默认自定义工具的库存 T4 ITextTemplatingEngineHost 不支持使用依赖注入来检索连接管理器。解决方案是实现一个模板文件生成器,它将访问我正在寻找的信息。它也不像实现 EngineHost 服务那么简单。原来 Visual Studio 内部的 TextTemplatingService 可以实现支持文本模板生成器所需的接口。但是,内部服务不使用接口。这使得模板服务非常僵化并且不像我想要的那样健壮。正在进行的解决方案似乎构建了一个新的模板服务,该服务包装了 Visual Studio 服务并覆盖了 TemplatedCodeGenerator 并覆盖了 ProcessTemplate 并替换了包装的服务。我还没有完成,我正在处理一些额外的障碍,我可能会在其他帖子中提出问题。

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="$(DevEnvDir)PublicAssemblies\Microsoft.VisualStudio.Data.Services.dll" #>
<#@ assembly name="$(DevEnvDir)PublicAssemblies\Microsoft.VisualStudio.OLE.Interop.dll" #>
<#@ assembly name="$(DevEnvDir)PublicAssemblies\Microsoft.VisualStudio.Shell.15.0.dll" #>
<#@ assembly name="$(DevEnvDir)PublicAssemblies\Microsoft.VisualStudio.Shell.Interop.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Configuration" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data.Common" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Globalization" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Configuration" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="Microsoft.VisualStudio.Shell" #>
<#@ import namespace="Interop = Microsoft.VisualStudio.OLE.Interop" #>
<#@ import namespace="Microsoft.VisualStudio.Data.Services" #>
<#+
public class Settings
{
    const string connectionKey = @"$connectionKey$";
    readonly Guid connectionExplorerGuid = Guid.Parse("8B6159D9-A634-4549-9EAC-8642744F1042");

    public static ITextTemplatingEngineHost Host { get; set; }

    public string[] ExcludeTables
    {
        get
        { 
            return new string[]{
                "sysdiagrams",
                "BuildVersion",
                "aspnet_Applications",
                "aspnet_Membership",
                "aspnet_Paths",
                "aspnet_PersonalizationAllUsers",
                "aspnet_PersonalizationPerUser",
                "aspnet_Profile",
                "aspnet_Roles",
                "aspnet_SchemaVersions",
                "aspnet_Users",
                "aspnet_UsersInRoles",
                "aspnet_WebEvent_Events"
                };
        }
    }

    public static IVsDataConnection Connection
    {
        get
        {
            if (Host is IServiceProvider service)
            {
                if (service.GetService(typeof(EnvDTE.DTE)) is Interop.IServiceProvider provider)
                {
                    if (PackageUtilities.QueryService<IVsDataExplorerConnectionManager>(provider) is IVsDataExplorerConnectionManager manager)
                    {
                        return manager.Connections[connectionKey].Connection;
                    }
                    throw new InvalidOperationException("Unable to resolve IVsDataExplorerConnectionManager!");
                }
                throw new InvalidOperationException("Unable to resolve DTE as Interop.IServiceProvider!");
            }
            throw new Exception("Host property returned unexpected value (null)");
        }
    }
}
#>

包含以上内容的测试代码

<#@ template hostspecific="true" language="C#" #>
<#@ include file="Settings.ttinclude" #>
<#
    Settings.Host = Host;
#>

using Microsoft.Extensions.DependencyInjection;
using SubSonic;
using System;

namespace $rootnamespace$
{
    public partial class $safeitemrootname$
        : SubSonicContext
    {
        private readonly IServiceCollection services = null;

        public $safeitemrootname$(IServiceCollection services)
        {
            this.services = services ?? throw new ArgumentNullException(nameof(services));
        }

        public string ConnectionString => "<#= Settings.Connection.DisplayConnectionString #>";

        #region ISubSonicSetCollection{TEntity} Collection Properties
        #endregion
    }
}

【问题讨论】:

    标签: c# visual-studio t4 server-explorer


    【解决方案1】:

    目前没有现成的解决方案来解决使用 Visual Studio 获取连接管理器的问题。解决方案在于构建一个自定义模板生成器,该生成器将知道如何与 Visual Studio 扩展进行通信。这不是一件容易的事。

    好的,我已经为此制定了答案,但首先是 settings.ttinclude,它是从项目模板 /with 向导输出的

    <#@ template language="C#" #>
    <#@ assembly name="EnvDTE" #>
    <#@ assembly name="System.Core.dll" #>
    <#@ assembly name="System.Data" #>
    <#@ assembly name="System.Xml" #>
    <#@ assembly name="System.Configuration" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ import namespace="System.Data" #>
    <#@ import namespace="System.Data.SqlClient" #>
    <#@ import namespace="System.Data.Common" #>
    <#@ import namespace="System.Diagnostics" #>
    <#@ import namespace="System.Globalization" #>
    <#@ import namespace="System.IO" #>
    <#@ import namespace="System.Linq" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Text.RegularExpressions" #>
    <#@ import namespace="System.Configuration" #>
    <#@ import namespace="System.IO" #>
    <#@ import namespace="EnvDTE" #>
    <#+
    public class Settings
    {
        const string connectionKey = @"home-prime\localdb#7fe04ab3.DbSubSonic.dbo";
    
        public static ITextTemplatingEngineHost Host { get; set; }
    
        public string[] ExcludeTables
        {
            get
            { 
                return new string[]{
                    "sysdiagrams",
                    "BuildVersion",
                    "aspnet_Applications",
                    "aspnet_Membership",
                    "aspnet_Paths",
                    "aspnet_PersonalizationAllUsers",
                    "aspnet_PersonalizationPerUser",
                    "aspnet_Profile",
                    "aspnet_Roles",
                    "aspnet_SchemaVersions",
                    "aspnet_Users",
                    "aspnet_UsersInRoles",
                    "aspnet_WebEvent_Events"
                    };
            }
        }
    
        public static IDataConnection Connection
        {
            get
            {
                if (Host is IServiceProvider service)
                {
                    if (service.GetService(typeof(ISubSonicCoreService)) is ISubSonicCoreService subsonic)
                    {
                        return subsonic.ConnectionManager[connectionKey];
                    }
                    throw new InvalidOperationException("Unable to resolve ISubSonicCoreService!");
                }
                throw new Exception("Host property returned unexpected value (null)");
            }
        }
    }
    #>
    

    第二个 t4 生成的类文件,这是一个概念证明,将来实际的连接字符串将永远不会出现在生成的代码中。

    using Microsoft.Extensions.DependencyInjection;
    using SubSonic;
    using System;
    
    namespace TemplateIntegrationTest.DAL
    {
        public partial class DataContext1
            : SubSonicContext
        {
            private readonly IServiceCollection services = null;
    
            public DataContext1(IServiceCollection services)
            {
                this.services = services ?? throw new ArgumentNullException(nameof(services));
            }
    
            public string ConnectionString => @"Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=DbSubSonic;Integrated Security=True";
    
            #region ISubSonicSetCollection{TEntity} Collection Properties
            #endregion
        }
    }
    

    覆盖模板服务 ProcessTemplate 方法

    public string ProcessTemplate(string inputFile, string content, ITextTemplatingCallback callback = null, object hierarchy = null)
            {
                ThreadHelper.ThrowIfNotOnUIThread();
    
                string result = "";
    
                if (this is ITextTemplatingComponents SubSonicComponents)
                {
                    SubSonicComponents.Hierarchy = hierarchy;
                    SubSonicComponents.Callback = callback;
                    SubSonicComponents.InputFile = inputFile;
    
                    SubSonicComponents.Host.SetFileExtension(SearchForLanguage(content, "C#") ? ".cs" : ".vb");
    
                    result = SubSonicComponents.Engine.ProcessTemplate(content, SubSonicComponents.Host);
    
                    // TextTemplatingService which is private and can not be replicated with out implementing from scratch.
    
                    // SqmFacade is a DTE wrapper that can send additional commands to VS
                    if (SearchForLanguage(content, "C#"))
                    {
                        SqmFacade.T4PreprocessTemplateCS();
                    }
                    else if (SearchForLanguage(content, "VB"))
                    {
                        SqmFacade.T4PreprocessTemplateVB();
                    }
                }
    
                return result;
            }
    

    为了实现这一点,我必须执行以下操作:

    • 在 Visual Studio 中构建一个使用封装的 STextTemplating 服务的模板生成器。
    • 包装的服务必须覆盖 ITextTemplatingEngineHost
    • 覆盖 ProcessTemplate 方法,vs 服务可以实现接口,但它以主机无法覆盖的方式实现。
    • 最后,将我构建的 dll 库放入 GAC。显然,T4 引擎仍然存在一个错误,它忽略了代码库位置被忽略的事实,它在其他地方查找库但没有找到它们。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-09
      • 1970-01-01
      • 1970-01-01
      • 2016-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多