来自服务器资源管理器的 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 模板使用连接的主要内容,如果未能解决你的问题,请参考以下文章
如何从 .tt 文件(T4 模板)中读取来自 web.config 的 appSetting