4
votes

Database First (and Model First) approach has this nice DbContext Generator scaffolding files to generate the context and models; Model.Context.tt + Model.tt

Since they have built-in helper methods to retrieve navigation properties, it's quite handy to use them for other purposes as well, like creating controller, views etc. ASP.NET Scaffolding also does a similar job but in that case, the scaffolder needs to be called per model, in contrast these T4 files generate all the files at once.

However, they only use "edmx" file as an input. Is it possible to use / convert them for Code First approach?

Entity Framework version 6.1

1

1 Answers

11
votes

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.