Automate CRUD creation in a layered architecture under .NET Core
Asked Answered
S

3

12

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.

Shaddock answered 15/5, 2018 at 7:57 Comment(6)
you can easily do this by following this ->c-sharpcorner.com/article/scaffolding-asp-net-core-mvcHaemolysis
That doesn't let me add the mapping, context and swagger annotations if I'm not wrong. Also, the architecture wont let us work that way because we aren't creating a dbset for each entity, we are using a generic.Shaddock
Well if you cannot find ready-made solution - then T4 is the way to go I think.Beutner
google for scaffolding - that how they call this approach. As an alternative to T4, you may try to create console app project and use Roslyn API to generate the code. It is really nice and you can do quite advanced stuff with it, but it's a little bit harder to read than T4. I've done some custom C# code generation from XML schema this way and it worked quite well.Disjunctive
this project may be a helpful starting point, github.com/amelmusic/REST-FrameworkExosmosis
Are all your entities going to be in the same database/repo?Somewhat
C
4

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);
    }
#>
Copybook answered 18/5, 2018 at 20:36 Comment(1)
This is a great guide point for me, as soon as I can I'm going to test this.Shaddock
G
4

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;
        }
Gnostic answered 23/5, 2018 at 8:59 Comment(0)
A
0

If you using three layer architecture then create core and add Interface Repository line this ` public partial interface IRepository where T : BaseEntity {

    T GetById(object id);


    void Insert(T entity);


    void Insert(IEnumerable<T> entities);


    void Update(T entity);


    void Update(IEnumerable<T> entities);


    void Delete(T entity);


    void Delete(IEnumerable<T> entities);

    IQueryable<T> Table { get; }

    IQueryable<T> TableNoTracking { get; }
}

public interface IDbContext
{

    IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity;


    int SaveChanges();


    IList<TEntity> ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters)
        where TEntity : BaseEntity, new();


    IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters);


    int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters);


    void Detach(object entity);


    bool ProxyCreationEnabled { get; set; }


    bool AutoDetectChangesEnabled { get; set; }

}`

these interfaces can be use in service modules like public partial class BlogService : IBlogService{ private readonly IRepository<BlogPost> _blogPostRepository; private readonly IRepository<BlogComment> _blogCommentRepository;} this is based on DI

Thanks

Allocution answered 24/5, 2018 at 6:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.