Using EDMX file and T4 in .NET Core to Generate Code (Entities, DTO, API, Services etc.)

ashirafumiiro

Miiro Ashirafu

Posted on May 4, 2022

Using EDMX file and T4 in .NET Core to Generate Code (Entities, DTO, API, Services etc.)

Hello there!
I am going to briefly show you how we can use existing EDMX files from EF 6 to generate code in .NET Core applications (migrate EF 6 to EF Core). This sample will be using T4 text Templates. I will share a link to the completed project at the end of the post.

From the docs:

Database First allows you to reverse engineer a model from an existing database. The model is stored in an EDMX file (.edmx extension) and can be viewed and edited in the Entity Framework Designer.

After getting our model from EF 6 project, we can add the .edmx file to the solution where we want to generate the code.

Image description

We then add the .tt file to a .NET core project (Class Library or Web API) at the location where the entities are to be generated.
In Visual Studio 2022, we shall need to adjust the T4 template code in order to have access to the VS host environment.

DataModel.tt



<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF6.Utility.CS.ttinclude"#>
<#@ output extension=".cs"#>
<#@ assembly name="Microsoft.VisualStudio.Interop" #>
<#@ import namespace="EnvDTE" #>

<#
IServiceProvider serviceProvider = (IServiceProvider)this.Host;
DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE;

// path to the DataModel.edmx file 
string inputFile = Path.Combine(Path.GetDirectoryName(dte.Solution.FullName), "DataAccess", "DataModel", "DataModel.edmx");

var textTransform = DynamicTextTransformation.Create(this);
var code = new CodeGenerationTools(this);

...


Enter fullscreen mode Exit fullscreen mode

After that, we shall have our entities in the same directory containing the .tt file.

Image description

Generating DTOs

DTOs are usually derived by eliminating Navigation properties from entities. They are a more portable version of the DB entities which can be used in data transfer. For our case, we shall also make all value type properties nullable to use them in updating our database.

  • To a location where you want to add the DTOs (project or folder), right click in the VS solution explorer to add an existing item

  • Browse to the location where the entities are and add the DataModel.tt file.

  • Rename the file to DTO.tt and then modify it to generate the DTOs.

Modifying the DTO.tt file

Firstly, we prevent the generation of the constructor in case there are collection navigation properties using the code below.



 if (false && (propertiesWithDefaultValues.Any() || collectionNavigationProperties.Any() || complexProperties.Any()))
    { ...


Enter fullscreen mode Exit fullscreen mode

It can also be done by deleting the entire if statement.
Secondly, we eliminate the generation of navigation properties, either using the same technique or deleting the entire if statement.



 var navigationProperties = typeMapper.GetNavigationProperties(entity);
    if (navigationProperties.Any() && false) // && false will make it always false
    {


Enter fullscreen mode Exit fullscreen mode

Lastly, we specify that all fields are nullable by modifying the following method in the TypeMapper class as follows:



public string GetTypeName(TypeUsage typeUsage)
    {
        // the second parameter is given a value true
        return typeUsage == null ? null : GetTypeName(typeUsage.EdmType, true, modelNamespace: null);
    }


Enter fullscreen mode Exit fullscreen mode

With those modifications we have our DTOs generated as required

Employee.cs



 public partial class Employee
    {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Gender { get; set; }
        public Nullable<int> Salary { get; set; }
        public Nullable<int> DepartmentId { get; set; }

        public virtual Department Department { get; set; }
    }


Enter fullscreen mode Exit fullscreen mode

EmployeeDTO.cs



//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace DataAccess.DTO
{
    using System;
    using System.Collections.Generic;

    public partial class EmployeeDTO
    {
        public Nullable<int> ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Gender { get; set; }
        public Nullable<int> Salary { get; set; }
        public Nullable<int> DepartmentId { get; set; }
    }
}



Enter fullscreen mode Exit fullscreen mode

After that, we now create the Context manually and use it in our services in the next step.



 public partial class EmployeeContext : DbContext
    {
        public string DbPath { get; }
        public EmployeeContext(DbContextOptions<EmployeeContext> options)
            : base(options)
        {
            var folder = Environment.SpecialFolder.LocalApplicationData;
            var path = Environment.GetFolderPath(folder);
            DbPath = System.IO.Path.Join(path, "employees.db");
        }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite($"Data Source={DbPath}");

        public virtual DbSet<AuditEntry> AuditEntries { get; set; }
        public virtual DbSet<Department> Departments { get; set; }
        public virtual DbSet<Employee> Employees { get; set; }
    }


Enter fullscreen mode Exit fullscreen mode

Generating Services

For the services, we shall use a generic Repository just for demonstration. The code used is not the exact approach that is documented to describe the repository pattern.



public interface IRepository<T, TDTO>
    {
        T Add(T entity);
        T Update(TDTO entity);
        T Delete(T entity);
        T GetOne(int? id);
        List<T> GetAll();
    }


Enter fullscreen mode Exit fullscreen mode

For further simplicity, we shall also add a default implementation to provide defaults for some methods.



public class BaseRepository<T, TDTO> : IDisposable, IRepository<T, TDTO> where T : class
    {
        private readonly DbSet<T> _table;
        private readonly EmployeeContext _context;
        protected readonly ILogger logger;

        public BaseRepository(EmployeeContext context, ILogger logger)
        {
            _context = context;
            this.logger = logger;
            _table = _context.Set<T>();
        }
        protected EmployeeContext Context => _context;

        public T Add(T entity)
        {
            _table.Add(entity);
            SaveChanges();
            return entity;
        }

        public T Delete(T entity)
        {
            _context.Entry(entity).State = EntityState.Deleted;
            SaveChanges();
            return entity;
        }

        public void Dispose()
        {
            _context?.Dispose();
        }

        public List<T> GetAll() => _table.ToList();

        public T GetOne(int? id)
        {
            return _table.Find(id);
        }

        public virtual T Update(TDTO entity)
        {
            throw new NotImplementedException();
        }

        internal void SaveChanges()
        {
            try
            {
                _context.SaveChanges();
            }
            catch (DbUpdateException e)
            {
                var sb = new StringBuilder();
                sb.AppendLine($"DbUpdateException error details - {e?.InnerException?.InnerException?.Message}");

                foreach (var eve in e.Entries)
                {
                    sb.AppendLine($"Entity of type {eve.Entity.GetType().Name} in state {eve.State} could not be updated");
                }
                logger.LogError(e, sb.ToString());
                throw;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, ex.Message);
            }
        }
    }


Enter fullscreen mode Exit fullscreen mode

In order to generate the services that we shall use in the generated API, we create the interfaces first.



  public partial interface IAuditEntryService : IRepository<AuditEntry, AuditEntryDTO>
    {

    }
public partial interface IDepartmentService : IRepository<Department, DepartmentDTO>
    {

    }
  public partial interface IEmployeeService : IRepository<Employee, EmployeeDTO>
    {

    }


Enter fullscreen mode Exit fullscreen mode

The Services.tt file is added and modified to generate the Servies. The Update method is also implemented in the services and a snipped for this in the t4 template is shown below



 public override <#=code.Escape(entity)#> Update(<#=code.Escape(entity)#>DTO entity)
    {
        <#=code.Escape(entity)#> returnValue = null;

        try
        {
            var model = this.Context.<#=code.Escape(entity).Pluralize()#>.Where(p => p.<#=code.Escape(idProperty)#> == entity.<#=code.Escape(idProperty)#>).FirstOrDefault();


            if (model != null)
            {
            <#     

        if (simpleProperties.Any())
        {
            foreach (var edmProperty in simpleProperties)
            {#>

                if (entity.<#=code.Escape(edmProperty) #> != null)
                {
                    model.<#=code.Escape(edmProperty) #> = entity.<#=codeStringGenerator.PropertyAssign(edmProperty) #>;
                }
        <#
            }
        }#>

                this.Context.<#=code.Escape(entity).Pluralize()#>.Update(model);
                this.SaveChanges();

                // refresh entity
                returnValue = this.Context.<#=code.Escape(entity).Pluralize()#>.Where(p => p.<#=code.Escape(idProperty)#> == entity.<#=code.Escape(idProperty)#>).FirstOrDefault();;

            }

        }
        catch (Exception ex)
        {
            logger.LogError(ex, ex.Message);
        }

        return returnValue;
    }


Enter fullscreen mode Exit fullscreen mode

In the services, Humanizer.Core is used to provide more control in the name naming of properties other class members.
The following code shows one of the services generated by the .tt file



//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace DataAccess.Services
{
    using System;
    using System.Collections.Generic;

    using DataAccess.DTO;
    using DataAccess.Entities;
    using DataAccess.Repository;
    using DataAccess.Interfaces;
    using DataAccess.Context;
    using Microsoft.Extensions.Logging;

    public partial class EmployeeService : BaseRepository<Employee, EmployeeDTO>,  IEmployeeService
    {

        public EmployeeService(EmployeeContext context, ILogger<EmployeeService> logger): base(context, logger)
        {
        }

        public override Employee Update(EmployeeDTO entity)
        {
            Employee returnValue = null;

            try
            {
                var model = this.Context.Employees.Where(p => p.ID == entity.ID).FirstOrDefault();


                if (model != null)
                {

                    if (entity.ID != null)
                    {
                        model.ID = entity.ID.Value;
                    }

                    if (entity.FirstName != null)
                    {
                        model.FirstName = entity.FirstName;
                    }

                    if (entity.LastName != null)
                    {
                        model.LastName = entity.LastName;
                    }

                    if (entity.Gender != null)
                    {
                        model.Gender = entity.Gender;
                    }

                    if (entity.Salary != null)
                    {
                        model.Salary = entity.Salary.Value;
                    }

                    if (entity.DepartmentId != null)
                    {
                        model.DepartmentId = entity.DepartmentId.Value;
                    }

                    this.Context.Employees.Update(model);
                    this.SaveChanges();

                    // refresh entity
                    returnValue = this.Context.Employees.Where(p => p.ID == entity.ID).FirstOrDefault();;

                }

            }
            catch (Exception ex)
            {
                logger.LogError(ex, ex.Message);
            }

            return returnValue;
        }

    }
}



Enter fullscreen mode Exit fullscreen mode

Generating the API

To generate the API, an ASP.NET Core web application is created and a reference is added to the project containing our entities, DTOs and Services.
The Context is configured in the Program.cs as well as the Services in the DI container.




builder.Services.AddDbContext<EmployeeContext>(options =>
  options.UseSqlite(builder.Configuration.GetConnectionString("TestConnectionString"), b=> b.MigrationsAssembly("WebApi")));

builder.Services.AddTransient<IEmployeeService, EmployeeService>();
builder.Services.AddTransient<IAuditEntryService, AuditEntryService>();
builder.Services.AddTransient<IDepartmentService, DepartmentService>();


Enter fullscreen mode Exit fullscreen mode

A t4 template is added to generate the container and modified to produce the correct file names and code.

Image description

The code below shows a sample of the generated API controller



//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace WebApi.Controllers
{
    using System;
    using System.Collections.Generic;

    using DataAccess.DTO;
    using DataAccess.Entities;
    using DataAccess.Interfaces;
    using DataAccess.Context;
    using Microsoft.Extensions.Logging;
    using Microsoft.AspNetCore.Mvc;

    [ApiController]
    [Route("api/[controller]")]
    public partial class DepartmentController : ControllerBase
    {
        private readonly IDepartmentService departmentService;

        public DepartmentController(IDepartmentService departmentService)
        {
            this.departmentService = departmentService;
        }


        [HttpGet]
        public async Task<IActionResult> GetListAsync()
        {
            var response = this.departmentService.GetAll();
            return await Task.FromResult(new JsonResult(response));
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetOne(int? id)
        {
            var response = this.departmentService.GetOne(id);
            return await Task.FromResult(new JsonResult(response));
        }

        [HttpPost]
        public async Task<IActionResult> CreateAsync(Department entity)
        {
            var response = this.departmentService.Add(entity);
            return await Task.FromResult(new JsonResult(response));
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

We now have the API generated using T4 template and we can do a smoke test.
The migrations are created and the database is initialized. After that, we can run the application and everything works fine.

Image description

Benefits

  • The code generated can be added to source control the t4 template can be eliminated.

  • The build system does not necessarily have to process the text templates

Possible restrictions

  • T4 templates may be dropped in favor of Source Generators (to be implemented in the future). As long as they are supported in VS, this approach will work.

The code can be found here on github.

Enjoy your free time after this automation.

💖 💪 🙅 🚩
ashirafumiiro
Miiro Ashirafu

Posted on May 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related