Here is a working sample - Updated on June 5th 2018:
https://github.com/coni2k/DbContextGeneratorWithCodeFirst
- Visual Studio 2017
- Target Framework 4.6.1
- Entity Framework 6.2.0
There are couple of steps to achieve this.
1. Find and copy EF6.Utility.CS.ttinclude to your project
In T4 files (Model.Context.tt + Model.tt), inputFile
variable (edmx file) used in two locations;
const string inputFile = @"QAModel.edmx";
//var textTransform ...
//var code ...
//var ef ...
//var typeMapper ...
//var fileManager ...
var itemCollection = new EdmMetadataLoader(textTransform.Host, textTransform.Errors).CreateEdmItemCollection(inputFile);
//var codeStringGenerator ...
if (!typeMapper.VerifyCaseInsensitiveTypeUniqueness(typeMapper.GetAllGlobalItems(itemCollection), inputFile))
VerifyCaseInsensitiveTypeUniqueness
is a validation method and inputFile
used as source location for the error. The important one is EdmMetadataLoader
, which comes from EF6.Utility.CS.ttinclude
file that's defined in the beginning of the file;
<#@ include file="EF6.Utility.CS.ttinclude"#><#@
Since this file needs to be modified, find the file and copy to your project folder. In my case it's under this folder;
%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Entity Framework Tools\Templates\Includes\
As you can see, this is an optional step. We could modify the original file, but it's safer to play with a copy and leave the original one intact.
2. Modify ttinclude file
In the include file, there are three methods that use the edmx file.
public IEnumerable<GlobalItem> CreateEdmItemCollection(string sourcePath)
public XElement LoadRootElement(string sourcePath)
private void ProcessErrors(IEnumerable<EdmSchemaError> errors, string sourceFilePath)
The important one is LoadRootElement
method that reads the xml file. Instead of passing a physical xml, we can create a memory stream from Code First DbContext and let it read the data from this stream.
a. Add these two methods to the include file;
public IEnumerable<GlobalItem> CreateEdmItemCollection(DbContext dbContext)
{
ArgumentNotNull(dbContext, "dbContext");
var schemaElement = LoadRootElement(dbContext);
if (schemaElement != null)
{
using (var reader = schemaElement.CreateReader())
{
IList<EdmSchemaError> errors;
var itemCollection = EdmItemCollection.Create(new[] { reader }, null, out errors);
ProcessErrors(errors, dbContext.Database.Connection.ConnectionString);
return itemCollection ?? new EdmItemCollection();
}
}
return new EdmItemCollection();
}
public XElement LoadRootElement(DbContext dbContext)
{
ArgumentNotNull(dbContext, "dbContext");
XElement root;
using (var stream = new MemoryStream())
{
using (var writer = XmlWriter.Create(stream))
{
EdmxWriter.WriteEdmx(dbContext, writer);
}
stream.Position = 0;
root = XElement.Load(stream, LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
root = root.Elements()
.Where(e => e.Name.LocalName == "Runtime")
.Elements()
.Where(e => e.Name.LocalName == "ConceptualModels")
.Elements()
.Where(e => e.Name.LocalName == "Schema")
.FirstOrDefault()
?? root;
}
return root;
}
Now we are using DbContext
in the include file and this requires System.Data.Entity
library to be included.
b. Add these three lines in the beginning of the file;
//<#@ assembly name="%VS120COMNTOOLS%..\IDE\Microsoft.Data.Entity.Design.dll" #>
<#@ assembly name="System.Data.Entity" #>
<#@ import namespace="System.Data.Entity" #>
<#@ import namespace="System.Data.Entity.Infrastructure" #>
//<#@ import namespace="System" #>
3. Modify DbContext class by adding a new constructor
Since T4 file has its own domain, DbContext
can't be initialized by using connection string from the project's web/app.config file. Easiest workaround is to initialize it with an explicit connection string.
Modify your DBContext class by adding a new constructor that takes connection string as a parameter.
public QAContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
4. Modify the T4 file
Now T4 is ready to pass an Code First DbContext instance. Update the file accordingly.
a. To be able to access and instantiate it, add your the DbContext
class library as a assembly reference to the beginning of your T4 file;
//<#@ template language="C#" ...
<#@ assembly name="$(SolutionDir)DbContextGeneratorWithCodeFirst\bin\Debug\DbContextGeneratorWithCodeFirst.dll"#>
//<#@ include file="EF6 ...
b. Replace inputFile
variable with connectionString
const string connectionString = @"Server=(LocalDb)\v11.0;Database=QADb;Integrated Security=True;MultipleActiveResultSets=True";
//var textTransform ...
c. Update CreateEdmItemCollection
block
//var fileManager ...
IEnumerable<GlobalItem> itemCollection;
using (var dbContext = new DbContextGeneratorWithCodeFirst.QAContext(connectionString))
{
itemCollection = new EdmMetadataLoader(textTransform.Host, textTransform.Errors).CreateEdmItemCollection(dbContext);
if (itemCollection == null)
{
return string.Empty;
}
}
//var codeStringGenerator ...
d. Update VerifyCaseInsensitiveTypeUniqueness
method with connectionString
parameter
if (!typeMapper.VerifyCaseInsensitiveTypeUniqueness(typeMapper.GetAllGlobalItems(itemCollection), connectionString))
Now it's ready to use. You can modify the rest of the file according to your needs, to create any file you want like html, javascript, razor etc. based on your Code First model.
It's bit a of work and definitely can be improved. For instance, include file can take DbContext
type as a parameter, instantiate it, determine whether it's Code First or Database First, then continue to process. Or locate web/app.config file and read the connection string from there. However should be enough for a start.