0
\$\begingroup\$

I am currently building a REST webapi. Right now I am wondering if this design is in the right direction.

Firstly, I am using entity framework. I like to call the items in the database DbItems. The items returned by the api are called DtoItems. Just to keep them apart in this example.

Mapping

I use mappers that are added as services to the project. When mapping items, I can add a configuration to map items to certain depth. Next, the mappers don't create items, but only copy parameters. This is on purpose, so I can update existing database objects. Although the code could be simplified if this wasn't the case. (Now that I am writing this, I guess EF will solve this if I update an item with an existing Key or Id.)

Example of a mapper:

public class ProjectItemDbMapper : IMapper<ProjectItemDb, ProjectItemDbDto>
{
    private readonly IMapper<User, UserDto> _userMapper;
    private readonly AppDbContext _context;

    public ProjectItemDbMapper(IMapper<User, UserDto> userMapper, AppDbContext context)
    {
        _userMapper = userMapper;
        _context = context;
    }


    public async Task MapToDb(ProjectItemDb dbItem, ProjectItemDbDto dtoItem, MapperOptions options)
    {
        if (dtoItem.Id != null) dbItem.Id = dtoItem.Id.Value;
        if (dtoItem.Title != null) dbItem.Title = dtoItem.Title;
        if (dtoItem.Created != null) dbItem.Created = dtoItem.Created.Value;

        if (dtoItem.Parent?.Id != null)
            dbItem.Parent = await _context.ProjectItemDbs.FindAsync(dtoItem.Parent.Id);

        if (dtoItem.CreatedByUser?.Id != null)
            dbItem.CreatedByUser = await _context.Users.FindAsync(dtoItem.CreatedByUser.Id);

    }

    public async Task MapToDto(ProjectItemDb dbItem, ProjectItemDbDto dtoItem, MapperOptions options)
    {
        options.Depth--;
        dtoItem.Id = dbItem.Id;
        dtoItem.Title = dbItem.Title;
        dtoItem.Created = dbItem.Created;

        if(options.Depth > 0)   //Only map children if required
        {
            if (dbItem.CreatedByUser != null)
            {
                await _userMapper.MapToDto(dbItem.CreatedByUser, dtoItem.CreatedByUser, options);
            }
            
            if (dbItem.Parent != null)
            {
                await _MapToDto(dbItem.Parent, dtoItem.Parent, options);
            }
        } 
        else if(options.Depth == 0)
        {
            //TODO: Only load the Id of the object. (Make this a setting in MapperOptions)
        }
        options.Depth++;
    }
}

Repositories

The next problem I have is when deleting objects. When deleting objects, I also need to delete the children. Or throw an error if the children should be manually deleted. Or only detach the link between the objects. This is something I want to manage in repositories. (By lack of a better name) The repository works with the DBEntities. Since the repositories are more or less the same, I am considering making a generic one where the methods can be overridden.

In this example, I also delete child objects when deleting a projectItem. The reason I want to register these as services is, so I can use dependency injection to get other repositories if I want to delete the User for example.

public class GenericRepository<ProjectItem> : IRepository<ProjectItem>
    where ProjectItem : class
{
    protected readonly AppDbContext _context;

    public GenericRepository(AppDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<ProjectItem?> CreateAsync(ProjectItem entity)
    {
        if (entity == null)
            throw new ArgumentNullException(nameof(entity));

        _context.Set<ProjectItem>().Add(entity);
        await _context.SaveChangesAsync();
        return entity;
    }

    public async Task<bool> DeleteAsync(int id)
    {
        var entity = await _context.Set<ProjectItem>().FindAsync(id);
        if (entity == null)
            return false;
        
        var children = entity.Children.ToList();
        foreach(var child in children)
            await DeleteAsync(child.id);

        _context.Set<ProjectItem>().Remove(entity);
        await _context.SaveChangesAsync();
        return true;
    }

    public async Task<IEnumerable<ProjectItem>> GetAllAsync(string? filter, string orderBy, int page, int pageSize)
    {
        IQueryable<ProjectItem> query = _context.Set<ProjectItem>();

        // Apply filters to the query (you need to implement filter logic)

        // Apply ordering to the query if orderBy is provided
        if (!string.IsNullOrEmpty(orderBy))
        {
            // You need to implement your custom ordering logic here
            // Example: query = query.OrderBy(orderBy);
        }

        // Apply paging to the query
        if (pageSize > 0)
        {
            int skip = (page - 1) * pageSize;
            query = query.Skip(skip).Take(pageSize);
        }

        return await query.ToListAsync();
    }

    public async Task<ProjectItem?> GetByIdAsync(int id)
    {
        return await _context.Set<ProjectItem>().FindAsync(id);
    }

    public async Task<ProjectItem?> UpdateAsync(int id, ProjectItem entity)
    {
        if (entity == null)
            throw new ArgumentNullException(nameof(entity));

        var existingEntity = await _context.Set<ProjectItem>().FindAsync(id);
        if (existingEntity == null)
            return null;

        // Update the existing entity properties with the values from the entity
        // Make sure to implement this update logic
        // Example: existingEntity.Property = entity.Property;

        await _context.SaveChangesAsync();
        return existingEntity;
    }
}

Controllers

An example of a controller to check how all this is used:

[ApiController]
[Route("[controller]")]
public class ProjectItemsController : ControllerBase
{
    private readonly IRepository<DbItem> _repositoryContext;
    private readonly AppDbContext _context;
    private readonly IMapper<DbItem, DtoItem> _mapper;
    private readonly UserManager _userManager;

    public ProjectItemsController(IRepository<DbItem> repositoryContext, AppDbContext context, IMapper<DbItem, DtoItem> mapper, UserManager userManager)
    {
        _repositoryContext = repositoryContext;
        _context = context;
        _mapper = mapper;
        _userManager = userManager;
    }

    [HttpGet]
    [Route("GetById")]
    virtual public async Task<ActionResult<DtoItem>> GetById(int id)
    {
        DbItem? dbItem = await _repositoryContext.GetByIdAsync(id);
        if (dbItem == null)
            return NotFound();
        DtoItem dtoItem = Activator.CreateInstance<DtoItem>();
        await _mapper.MapToDto(dbItem, dtoItem, new MapperOptions { Depth = 1 });
        return Ok(dtoItem);
    }

    [HttpGet]
    [Route("GetAll")]
    virtual public async Task<ActionResult<DtoItem[]>> GetAll(string? filters, string? order, int? pageNo, int? pageSize)
    {
        var dbItems = await _repositoryContext.GetAllAsync(filters, order ?? "Id", pageNo ?? 1, pageSize ?? 25);
        List<DtoItem> dtoItems = new List<DtoItem>();

        foreach (var dbItem in dbItems)
        {
            DtoItem dtoItem = Activator.CreateInstance<DtoItem>();
            await _mapper.MapToDto(dbItem, dtoItem, new MapperOptions { Depth = 1 });
            dtoItems.Add(dtoItem);
        }

        return Ok(dtoItems);
    }

    [HttpPost]
    [Route("Create")]
    virtual public async Task<ActionResult<DtoItem>> Create(DtoItem dtoItem)
    {
        if (ModelState.IsValid)
        {
            DbItem dbItem = Activator.CreateInstance<DbItem>();
            await _mapper.MapToDb(dbItem, dtoItem, new MapperOptions { Depth = 1 });
            dbItem.CreatedByUser = await _userManager.GetUser(User);
            dbItem.Created = DateTime.Now;

            var newDbItem = await _repositoryContext.CreateAsync(dbItem);
            DtoItem newDtoItem = Activator.CreateInstance<DtoItem>();
            await _mapper.MapToDto(newDbItem, newDtoItem, new MapperOptions { Depth = 1 });
            return CreatedAtAction("GetById", newDtoItem);
        }
        return BadRequest();
    }

    [HttpPatch]
    [Route("Patch")]
    virtual public async Task<ActionResult<DtoItem>> Patch(int id, DtoItem dtoItem)
    {
        if (ModelState.IsValid)
        {
            if (await _repositoryContext.GetByIdAsync(id) is DbItem dbItem)
            {
                if (dbItem.CreatedByUser?.Id != _userManager.GetUser(User).Id)
                    return BadRequest();

                await _mapper.MapToDb(dbItem, dtoItem, new MapperOptions { Depth = 1 });
                var newDbItem = await _repositoryContext.CreateAsync(dbItem);

                DtoItem newDtoItem = Activator.CreateInstance<DtoItem>();
                await _mapper.MapToDto(newDbItem, newDtoItem, new MapperOptions { Depth = 1 });
                return Ok(newDtoItem);
            }
            return NotFound();
        }
        return BadRequest();
    }

    [HttpDelete]
    [Route("Delete")]
    virtual public async Task<IActionResult> Delete(int id)
    {
        var result = await _repositoryContext.DeleteAsync(id);
        if (result)
            return Ok();
        return NoContent();
    }
}

Program.cs

How I register the services.

builder.Services
    .AddScoped<IRepository<User>, UserRepository>()
    .AddScoped<IMapper<User, UserDto>, UserMapper>()
    .AddScoped<IRepository<ProjectItemDb>, ProjectItemDbRepository>()
    .AddScoped<IMapper<ProjectItemDb, ProjectItemDbDto>, ProjectItemDbMapper>();
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.