SkyeBoniwell
SkyeBoniwell

Reputation: 7092

generic methods for API controller

I'm writing an API for my game and I'm starting to realize that the amount of GET, POST, and PUT API methods can really add up.

So right now, I'm trying to make it more generic so that I don't have to write a separate method like GetMonsterList, GetTreasureList, GetPlayerInfo, etc.

But I'm not quite sure how to go about doing that.

Here is a non-generic PUT method that I currently have.

    // PUT: api/MonsterLists/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutMonsterList(string id, MonsterList monsterList)
    {
        if (id != monsterList.MonsterId)
        {
            return BadRequest();
        }

        _context.Entry(monsterList).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MonsterListExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return NoContent();
    }

And here is my attempt at outlining a generic method:

    // PUT: api/AnyLists/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutAnyList(string id, AnyList anyList)
    {
        if (id != anyList.AnyId)
        {
            return BadRequest();
        }

        _context.Entry(anyList).State = EntityState.Modified;

        return NoContent();
    }

My problem that I don't understand is, how do I pass in a model to a generic control like this? Like if I have a model for MonsterList, TreasureList, PlayerInfo, WeaponList, etc.

How could I use one generic method for all of them?

I did find one similiar question here, Generic Web Api controller to support any model , but the answer seemed to imply that this isn't a good idea.

Is that possible?

Thanks!

Upvotes: 3

Views: 1642

Answers (2)

LazZiya
LazZiya

Reputation: 5719

Before we create the generic controller, it is worth to mention that the structure model of your entities is so important to easily or hardly build the generic controller.

For example you could have some models with int id and others with string id, so we need to have a common base for both types.

Start by creating the common interface for Id property to handle int or string Ids in the generic interface:

public interface IHasId<TKey> 
    where TKey : IEquatable<TKey>
{
    TKey Id { get; set; }
}

Another thing to consider is ordering the entities, when querying for a list of entities we need to sort them to get the right paged entities. So, we can create another interface to specify the sorting property e.g. Name.

public interface IOrdered
{
    string Name { get; set; }
}

Our objects must implement the common interfaces like below:

public class Player : IHasId<string>, IOrdered
{
    public string Id { get; set; }
    public string Name { get; set; }
    ...
}

public class Treasure : IHasId<int>, IOrdered
{
    public int Id { get; set; }
    public string Name { get; set; }
    ...
}

Now create a generic base api controller, make sure to mark the methods as virtual so we can override them in the inherited api controllers if necessary.

[Route("api/[controller]")]
[ApiController]
public class GenericBaseController<T, TKey> : ControllerBase
    where T : class, IHasId<TKey>, IOrdered
    where TKey : IEquatable<TKey>
{
    private readonly ApplicationDbContext _context;

    public GenericBaseController(ApplicationDbContext context)
    {
        _context = context;
    }

    // make methods as virtual, 
    // so they can be overridden in inherited api controllers
    [HttpGet("{id}")]
    public virtual T Get(TKey id)
    {
        return _context.Set<T>().Find(id);
    }

    [HttpPost]
    public virtual bool Post([FromBody] T value)
    {
        _context.Set<T>().Add(value);
        return _context.SaveChanges() > 0;
    }

    [HttpPut("{id}")]
    public virtual bool Put(TKey id)
    {
        var entity = _context.Set<T>().AsNoTracking().SingleOrDefault(x => x.Id.Equals(id));
        if (entity != null)
        {
            _context.Entry<T>(value).State = EntityState.Modified;
            return _context.SaveChanges() > 0;
        }

        return false;
    }

    [HttpDelete("{id}")]
    public virtual bool Delete(TKey id)
    {
        var entity = _context.Set<T>().Find(id);
        if (entity != null)
        {
            _context.Entry<T>(entity).State = EntityState.Deleted;
            return _context.SaveChanges() > 0;
        }

        return false;
    }

    [HttpGet("list/{pageNo}-{pageSize}")]
    public virtual (IEnumerable<T>, int) Get(int pageNo, int pageSize)
    {
        var query = _context.Set<T>();

        var totalRecords = query.Count();
        var items = query.OrderBy(x => x.Name)
            .Skip((pageNo - 1) * pageSize)
            .Take(pageSize)
            .AsEnumerable();

        return (items, totalRecords);
    }
}

The rest is easy, just create api controllers that inherits from the base generic controller:

PlayersController :

[Route("api/[controller]")]
[ApiController]
public class PlayersController : GenericBaseController<Player, string>
{
    public PlayersController(ApplicationDbContext context) : base(context)
    {

    }
}

TreasuresController :

[Route("api/[controller]")]
[ApiController]
public class TreasuresController : GenericBaseController<Treasure, int>
{
    public TreasuresController(ApplicationDbContext context) : base(context)
    {

    }
}

you don't have to create any methods, but you are still able to override the base methods since we marked them as virtual e.g.:

[Route("api/[controller]")]
[ApiController]
public class TreasuresController : GenericBaseController<Treasure, int>
{
    public TreasuresController(ApplicationDbContext context) : base(context)
    {
        public ovedrride Treasure Get(int id)
        {
            // custom logic ….

            return base.Get(id);
        }
    }
}

You can download a sample project from GitHub: https://github.com/LazZiya/GenericApiSample

Upvotes: 4

Sergii Kudriavtsev
Sergii Kudriavtsev

Reputation: 10512

I guess you can pass over the name of the type of the parameter and do something like this (not tested):

// PUT: api/AnyLists/5
[HttpPut("{id}")]
public async Task<IActionResult> PutAnyList(string id, object anyList, string anyListType)
{
    var anyListObject = Convert.ChangeType(anyList, Type.GetType(anyListType)));
    if (id != anyListObject.AnyId)
    {
        return BadRequest();
    }

    _context.Entry(anyListObject).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        // Whatever error handling you need
    }
    return NoContent();
}

However, I wouldn't recommend to use this in production code. What will likely happen is that you will need to create quite a lot of exceptions for different types in the end - and you'll end up with the code that is much more convoluted and hard to support than if you just had separate methods per type.

Also, I'm not sure it will be easy to test this.

Upvotes: 1

Related Questions