Reputation: 101
I am trying to build a item template that generates database models and a database context without storing the connection information in the source code.
I have successfully interfaced a item template wizard with server explorer and can set a connection key in the settings.ttinclude.
The problem is that I can not resolve the interface to the IVsDataExplorerConnectionManager from the DTE.
I believe I have barked up the wrong tree, because this is the way to get the server explorer in a VSIX project. I was hoping similar code would work for the T4 visual studio templating engine.
I have spent several hours looking to see if anyone else has already done something similar and I have found nothing. any ideas on how I could consume a connection from the server explorer in a T4 template would be appreciated.
Update 7/23/2020
I have since learned that the T4 ITextTemplatingEngineHost that is stock with the default Custom Tool does not support using Dependency Injection to retrieve the connection manager. the solution is implement a templating file generator that will access the information that I am looking for. It is also not as simple as implementing a EngineHost Service. Turns out the TextTemplatingService that is internal to visual studio may implement the interfaces required to support the text templating generator. But, internally the service does not use the interfaces. Which makes the templating service very rigid and not as robust as I would like. the solution in progress appears to build a new templating service that wraps visual studio service and override the TemplatedCodeGenerator and override ProcessTemplate and substitute the wrapped service. I am by no means finished and I am dealing with some additional hurdles, which I may ask questions in other posts.
<#@ 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)");
}
}
}
#>
the test code that includes the above
<#@ 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
}
}
Upvotes: 3
Views: 190
Reputation: 101
there is not an in the box solution to address the issue of getting the connection manager with visual studio as it is now. The solution resides in building a custom templating generator that will know how to communicate with the visual studio extensions. which is not an easy task on it's own.
okay, I have developed the answer for this, but first here is the settings.ttinclude, as outputted from the item template /with wizard
<#@ 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)");
}
}
}
#>
Second the t4 generated class file, this is a proof of concept and in the future the actual connection string will never surface in the generated code.
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
}
}
override the templating service ProcessTemplate method
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;
}
To make this happen I had to do the following:
Upvotes: 0