来自服务器资源管理器的 T4 模板使用连接

Posted

技术标签:

【中文标题】来自服务器资源管理器的 T4 模板使用连接【英文标题】:T4 Template Consume connection from Server Explorer 【发布时间】: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 ISubSonicSetCollectionTEntity Collection Properties
        #endregion
    

【问题讨论】:

【参考方案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 ISubSonicSetCollectionTEntity 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 引擎仍然存在一个错误,它忽略了代码库位置被忽略的事实,它在其他地方查找库但没有找到它们。

【讨论】:

以上是关于来自服务器资源管理器的 T4 模板使用连接的主要内容,如果未能解决你的问题,请参考以下文章

如何从 .t​​t 文件(T4 模板)中读取来自 web.config 的 appSetting

T4模板:T4模板之菜鸟篇

T4模板编辑器

T4 模板自动生成带注释的实体类文件 - 只需要一个 SqlSugar.dll

T4模板:将现有文件转换为运行时模板

配置管理器的SQL Server服务器里只有SQLEXPERSS,没有MSSQLSERVER,无法连接数据库