Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automate CRUD creation in a layered architecture under .NET Core

Tags:

c#

.net

t4

I'm working in a new project under a typical three layer architecture: business, data and client using Angular as a front.

In this project we will have a repetitive task that we want to automate: The creation of CRUD. What we want to do is generate models and controllers(put, get, post, delete) as well as other basic project information from an entity and its properties.

What is my best option here? I had thought about templates T4, but my ignorance towards them make me doubt if it is the best option.

For example, from this entity:

public class User
{

    public int Id { get; set; }

    public string Name {get;set;}

    public string Email{ get; set; }

    public IEnumerable<Task> Task { get; set; }
}

I want to generate the following model:

public class UserModel
{

    public int Id { get; set; }

    public string Name {get;set;}

    public string Email{ get; set; }

    public IEnumerable<Task> Task { get; set; }
}

And also the controller:

{
    /// <summary>
    /// User controller
    /// </summary>
    [Route("api/[controller]")]
    public class UserController: Controller
    {
        private readonly LocalDBContext localDBContext;
        private UnitOfWork unitOfWork;

        /// <summary>
        /// Constructor
        /// </summary>
        public UserController(LocalDBContext localDBContext)
        {
            this.localDBContext = localDBContext;
            this.unitOfWork = new UnitOfWork(localDBContext);
        }

        /// <summary>
        /// Get user by Id
        /// </summary>
        [HttpGet("{id}")]
        [Produces("application/json", Type = typeof(UserModel))]
        public IActionResult GetById(int id)
        {
            var user = unitOfWork.UserRepository.GetById(id);
            if (user == null)
            {
                return NotFound();
            }

            var res = AutoMapper.Mapper.Map<UserModel>(user);
            return Ok(res);
        }

        /// <summary>
        /// Post an user
        /// </summary>
        [HttpPost]
        public IActionResult Post([FromBody]UserModel user)
        {
            Usuario u = AutoMapper.Mapper.Map<User>(user);
            var res = unitOfWork.UserRepository.Add(u);

            if (res?.Id > 0)
            {
                return Ok(res);
            }

            return BadRequest();

        }

        /// <summary>
        /// Edit an user
        /// </summary>
        [HttpPut]
        public IActionResult Put([FromBody]UserModel user)
        {
            if (unitOfWork.UserRepository.GetById(user.Id) == null)
            {
                return NotFound();
            }

            var u = AutoMapper.Mapper.Map<User>(user);

            var res = unitOfWork.UserRepository.Update(u);

            return Ok(res);

        }

        /// <summary>
        /// Delete an user
        /// </summary>
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {

            if (unitOfWork.UserRepository.GetById(id) == null)
            {
                return NotFound();
            }

            unitOfWork.UserRepository.Delete(id);

            return Ok();

        }

Also, we need to add AutoMapper mappings:

public AutoMapper()
{
    CreateMap<UserModel, User>();
    CreateMap<User, UserModel>();
}

And the UnitOfWork:

private GenericRepository<User> userRepository;

public GenericRepository<User> UserRepository
{
    get
    {

        if (this.userRepository== null)
        {
            this.userRepository= new GenericRepository<User>(context);
        }
        return userRepository;
    }
}

Most of the structures are going to be the same, except some specific cases of controllers that will have to be done manually.

like image 624
Pablo Avatar asked May 15 '18 07:05

Pablo


2 Answers

This is a simplified version of the project that you would need to write in order to generate the previous code. First of all create a directory wherein and any future entities will go. For simplicity's sake I called the directory Entities and created a file called User.cs which contains the source for the User class.

For each of these templates create a .tt file starting with the entity name followed by the function name. So the tt file for the user model would be called UserModel.tt into which you put the model template. For user controller, USerController.tt into which you'd put the controller template. There will only be automapper file, and the user generic repository will be called UserGenericRepository.tt into which (you've guessed it) you put the generic repository template

The template for the Model

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var hostFile = this.Host.TemplateFile;
    var entityName = System.IO.Path.GetFileNameWithoutExtension(hostFile).Replace("Model","");
    var directoryName = System.IO.Path.GetDirectoryName(hostFile);
    var fileName = directoryName + "\\Entities\\" + entityName + ".cs";
#>
<#= System.IO.File.ReadAllText(fileName).Replace("public class " + entityName,"public class " + entityName + "Model") #>

I noticed that the source file had no namespaces or usings, so the UserModel file won't compile without adding the usings to the User.cs file, however the file does generate as per spec

The template for the Controller

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var hostFile = this.Host.TemplateFile;
    var entityName = System.IO.Path.GetFileNameWithoutExtension(hostFile).Replace("Controller","");
    var directoryName = System.IO.Path.GetDirectoryName(hostFile);
    var fileName = directoryName + "\\" + entityName + ".cs";
#>
/// <summary>
/// <#= entityName #> controller
/// </summary>
[Route("api/[controller]")]
public class <#= entityName #>Controller : Controller
{
    private readonly LocalDBContext localDBContext;
    private UnitOfWork unitOfWork;

    /// <summary>
    /// Constructor
    /// </summary>
    public <#= entityName #>Controller(LocalDBContext localDBContext)
    {
        this.localDBContext = localDBContext;
        this.unitOfWork = new UnitOfWork(localDBContext);
    }

    /// <summary>
    /// Get <#= Pascal(entityName) #> by Id
    /// </summary>
    [HttpGet("{id}")]
    [Produces("application/json", Type = typeof(<#= entityName #>Model))]
    public IActionResult GetById(int id)
    {
        var <#= Pascal(entityName) #> = unitOfWork.<#= entityName #>Repository.GetById(id);
        if (<#= Pascal(entityName) #> == null)
        {
            return NotFound();
        }

        var res = AutoMapper.Mapper.Map<<#= entityName #>Model>(<#= Pascal(entityName) #>);
        return Ok(res);
    }

    /// <summary>
    /// Post an <#= Pascal(entityName) #>
    /// </summary>
    [HttpPost]
    public IActionResult Post([FromBody]<#= entityName #>Model <#= Pascal(entityName) #>)
    {
        Usuario u = AutoMapper.Mapper.Map<<#= entityName #>>(<#= Pascal(entityName) #>);
        var res = unitOfWork.<#= entityName #>Repository.Add(u);

        if (res?.Id > 0)
        {
            return Ok(res);
        }

        return BadRequest();

    }

    /// <summary>
    /// Edit an <#= Pascal(entityName) #>
    /// </summary>
    [HttpPut]
    public IActionResult Put([FromBody]<#= entityName #>Model <#= Pascal(entityName) #>)
    {
        if (unitOfWork.<#= entityName #>Repository.GetById(<#= Pascal(entityName) #>.Id) == null)
        {
            return NotFound();
        }

        var u = AutoMapper.Mapper.Map<<#= entityName #>>(<#= Pascal(entityName) #>);

        var res = unitOfWork.<#= entityName #>Repository.Update(u);

        return Ok(res);

    }

    /// <summary>
    /// Delete an <#= Pascal(entityName) #>
    /// </summary>
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {

        if (unitOfWork.<#= entityName #>Repository.GetById(id) == null)
        {
            return NotFound();
        }

        unitOfWork.<#= entityName #>Repository.Delete(id);

        return Ok();

    }
}
<#+
    public string Pascal(string input)
    {
        return input.ToCharArray()[0].ToString() + input.Substring(1);
    }
#>

The template for the AutoMapper

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var directoryName = System.IO.Path.GetDirectoryName(this.Host.TemplateFile) + "\\Entities";
    var files = System.IO.Directory.GetFiles(directoryName, "*.cs");
#>
public class AutoMapper
{
<#
foreach(var f in files) 
{
    var entityName = System.IO.Path.GetFileNameWithoutExtension(f);
#>
    CreateMap<<#= entityName #>Model, <#= entityName #>>();
    CreateMap<<#= entityName #>, <#= entityName #>Model>();
<#
}
#>}

This basically goes through each file in the Entities folder and creates mappers between Entity and Entity Model

The template for the Generic Repository

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var hostFile = this.Host.TemplateFile;
    var entityName = System.IO.Path.GetFileNameWithoutExtension(hostFile).Replace("GenericRepository","");
    var directoryName = System.IO.Path.GetDirectoryName(hostFile);
    var fileName = directoryName + "\\" + entityName + ".cs";
#>
public class GenericRepository
{
    private GenericRepository<<#= entityName #>> <#= Pascal(entityName) #>Repository;

    public GenericRepository<<#= entityName #>> UserRepository
    {
        get
        {
            if (this.<#= Pascal(entityName) #>Repository == null)
            {
                this.<#= Pascal(entityName) #>Repository = new GenericRepository<<#= entityName #>>(context);
            }
            return <#= Pascal(entityName) #>Repository;
        }
    }
}<#+
    public string Pascal(string input)
    {
        return input.ToCharArray()[0].ToString() + input.Substring(1);
    }
#>
like image 55
Thundter Avatar answered Oct 01 '22 18:10

Thundter


This might be a bit off topic, and not really answer related directly.

But why solve your problem that way?

Why not simply create a base CRUD controller. provide it with generic models, that relate to their data model counter parts.

So the BI models, has the same properties as the DAL models etc. Then you can make a generic converter that maps by property name. Or set a custom attribute on the properties to map to the intended names.

Then you would only ever need to say, import the a table into your entity model. And presto, all layers have access all the way down, because all conversions and CRUDS are generic.

Even better, if you need something specific to happen on your CRUD actions for say a specific table, you can simply overload the controller to a specific model type, and presto you have a clearly defined area to write code that are the exception to the generic way?

I am not really solving the underlying issue with this suggestion?

say a base controller for your db CRUD could look like (Pseudo code):

public TEntity Get<TContext>(Expression<Func<TEntity, bool>> predicate, TContext context) where TContext : DbContext
        {

            TEntity item = context.Set<TEntity>().FirstOrDefault(predicate);
            return item;
        }

        public List<TEntity> GetList<TContext>(Expression<Func<TEntity, bool>> predicate, TContext context) where TContext : DbContext
        {
            List<TEntity> item = context.Set<TEntity>().Where(predicate).ToList();
            return item;
        }

        public List<TEntity> GetAll<TContext>(TContext context) where TContext : DbContext
        {
            List<TEntity> item = context.Set<TEntity>().ToList();
            return item;
        }

        public TEntity Insert<TContext>(TEntity input, TContext context) where TContext : DbContext
        {
            context.Set<TEntity>().Add(input);
            context.SaveChanges();
            return input;
        }

        public TEntity UpSert<TContext>(TEntity input, Expression<Func<TEntity, bool>> predicate, TContext context) where TContext : DbContext
        {
            if (input == null)
                return null;

            TEntity existing = context.Set<TEntity>().FirstOrDefault(predicate);



            if (existing != null)
            {

                input.GetType().GetProperty("Id").SetValue(input, existing.GetType().GetProperty("Id").GetValue(existing));
                context.Entry(existing).CurrentValues.SetValues(input);

                context.SaveChanges();
            }
            else
            {
                RemoveNavigationProperties(input);
                context.Set<TEntity>().Add(input);
                context.SaveChanges();
                return input;
            }

            return existing;
        }
like image 34
Morten Bork Avatar answered Oct 01 '22 18:10

Morten Bork