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.
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);
...
After that, we shall have our entities in the same directory containing the .tt
file.
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()))
{ ...
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
{
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);
}
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; }
}
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; }
}
}
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; }
}
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();
}
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);
}
}
}
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>
{
}
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;
}
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;
}
}
}
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>();
A t4 template is added to generate the container and modified to produce the correct file names and code.
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));
}
}
}
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.
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.
Top comments (0)