23

Is it possible to have a generic web api that will support any model in your project?

class BaseApiController<T> :ApiController
{
    private IRepository<T> _repository;

    // inject repository

    public virtual IEnumerable<T> GetAll()
    {
       return _repository.GetAll();
    }

    public virtual T Get(int id)
    {
       return _repositry.Get(id);
    }

    public virtual void Post(T item)
    {
       _repository.Save(item);
    }
    // etc...
}

class FooApiController : BaseApiController<Foo>
{
   //..

}

class BarApiController : BaseApiController<Bar>
{
   //..
}

Would this be a good approach?

After all, i m just repeating the CRUD methods ? Can i use this base class to do the work for me?

is this OK? would you do this? any better ideas?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
DarthVader
  • 52,984
  • 76
  • 209
  • 300
  • 1
    Hi, I know this is an old question, but could you explain how you managed to call your generic action methods? It doesn't look like you can accomplish this with just routing rules. I would greatly appreciate it. – Brett Nov 17 '13 at 09:01

6 Answers6

28

I did this for a small project to get something up and running to demo to a client. Once I got into specifics of business rules, validation and other considerations, I ended up having to override the CRUD methods from my base class so it didn't pan out as a long term implementation.

I ran into problems with the routing, because not everything used an ID of the same type (I was working with an existing system). Some tables had int primary keys, some had strings and others had guids.

I ended up having problems with that as well. In the end, while it seemed slick when I first did it, actually using it in a real world implementation proved to be a different matter and didn't put me any farther ahead at all.

Ryan Gates
  • 4,501
  • 6
  • 50
  • 90
Nick Albrecht
  • 16,607
  • 10
  • 66
  • 101
  • 7
    well you could use an Interface call it `IIdentifier` to represent different kind of id s. – DarthVader Aug 22 '12 at 17:53
  • 1
    True, but in hindsight I still don't think I benefited from trying to preemptively refactor the CRUD operations to one generic controller. It caused too many headaches later in development. – Nick Albrecht Aug 22 '12 at 18:49
  • If you are greenfield, there is no reason why you shouldn't have all "Id" of the same type.. – I Stand With Russia Aug 22 '17 at 18:30
6
 public class GenericApiController<TEntity> : BaseApiController
    where TEntity : class, new()
{
    [HttpGet]
    [Route("api/{Controller}/{id}")]       
    public IHttpActionResult Get(int id)
    {
        try
        {
            var entity = db.Set<TEntity>().Find(id);
            if(entity==null)
            {
                return NotFound();
            }
            return Ok(entity);

        }
        catch(Exception ex)
        {
            return InternalServerError(ex);
        }
    }

    [HttpGet]
    [Route("api/{Controller}")]
    public IHttpActionResult Post(TEntity entity)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        try
        {
            var primaryKeyValue = GetPrimaryKeyValue(entity);
            var primaryKeyName = GetPrimaryKeyName(entity);
            var existing = db.Set<TEntity>().Find(primaryKeyValue);
            ReflectionHelper.Copy(entity, existing, primaryKeyName);
            db.Entry<TEntity>(existing).State = EntityState.Modified;
            db.SaveChanges();
            return Ok(entity);
        }
        catch (Exception ex)
        {
            return InternalServerError(ex);
        }
    }

    [HttpGet]
    [Route("api/{Controller}/{id}")]
    public IHttpActionResult Put(int id, TEntity entity)
    {
        try
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var existing = db.Set<TEntity>().Find(id);
            if (entity == null)
            {
                return NotFound();
            }
            ReflectionHelper.Copy(entity, existing);
            db.SaveChanges();
            return Ok(entity);
        }
        catch (Exception ex)
        {
            return InternalServerError(ex);
        }
    }

    [HttpDelete]
    [Route("api/{Controller}/{id}")]
    public IHttpActionResult Delete(int id)
    {
        try
        {
            var entity = db.Set<TEntity>().Find(id);
            if(entity==null)
            {
                return NotFound();
            }
            db.Set<TEntity>().Remove(entity);
            db.SaveChanges();
            return Ok();
        }
        catch (Exception ex)
        {
            return InternalServerError(ex);
        }
    }

    protected internal int GetPrimaryKeyValue(TEntity entity)
    {
        return ReflectionHelper.GetPrimaryKeyValue(entity);
    }

    protected internal string GetPrimaryKeyName(TEntity entity)
    {
        return ReflectionHelper.GetPrimaryKeyName(entity);
    }

    protected internal bool Exists(int id)
    {
        return db.Set<TEntity>().Find(id) != null;
    }
}
5

It's definitely possible. I've never had a reason to do that before, but if it works for your situation, it should be good.

If all of your models can be saved and retrieved in the exact same way, maybe they should just all be in the same controller instead though?

eouw0o83hf
  • 9,438
  • 5
  • 53
  • 75
  • well why wouldnt it work? essentially most of the time, all we do is to use the same operations for all domain models and repeat them many times. – DarthVader Aug 22 '12 at 16:22
  • 1
    You asked if it was possible, so I answered that :) Admittedly I think I fell short of the rest of the question, I may just remove my answer... – eouw0o83hf Aug 22 '12 at 16:23
  • i ll give u +1 for not removing your answer:) – DarthVader Aug 22 '12 at 16:24
  • yeah repository is generic. that s already standard in the industry. and there are 10 ways to make it generic – DarthVader Aug 22 '12 at 16:27
  • @DarthVader and now years later the so called industry regard the generic repostiory as anti-pattern. :-) – Pascal Jan 28 '16 at 18:35
1

Nothing wrong with this as long as you handle all the heavy lifting in your repositories. You may want to wrap/handle modelstate exceptions in your base controller.

I am actually doing something similar for a large project where users can define their own entities and APIs - ie: one user may want to have users and accounts while another may want to track cars and whatever else. They all use the same internal controller, but they each have their own endpoints.

Not sure how useful our code is to you since we don't use generics (each object is maintained as metadata and manipulated/passed back and forth as JObject dictionaries) but here is some code to give you an idea of what we are doing and maybe provide food for thought:

[POST("{primaryEntity}", RouteName = "PostPrimary")]
public async Task<HttpResponseMessage> CreatePrimary(string primaryEntity, JObject entity)
{
   // first find out which params are necessary to accept the request based on the entity's mapped metadata type
   OperationalParams paramsForRequest = GetOperationalParams(primaryEntity, DatasetOperationalEntityIntentIntentType.POST);

   // map the passed values to the expected params and the intent that is in use
   IDictionary<string, object> objValues = MapAndValidateProperties(paramsForRequest.EntityModel, paramsForRequest.IntentModel, entity);

   // get the results back from the service and return the data to the client.
   QueryResults results = await paramsForRequest.ClientService.CreatePrimaryEntity(paramsForRequest.EntityModel, objValues, entity, paramsForRequest.IntentModel);
        return HttpResponseMessageFromQueryResults(primaryEntity, results);

}
AlexGad
  • 6,612
  • 5
  • 33
  • 45
  • Good luck :) looks very complicated. i would avoid all that.:) – DarthVader Aug 23 '12 at 02:14
  • 1
    For what you are doing, yes, slight overkill ;). However, the code above allows multiple customer accounts with multiple apps in each account to manage multiple entities using one generic controller (actually we have broken it out into a controller per action type) so not so complex when you consider the problem it has to solve. And if we can use the above code and support thousands of clients with under 100ms response time, you're good to go ;). Anyway, just trying to show another way of accomplishing it as food for thought. – AlexGad Aug 23 '12 at 02:39
1

If you have predefined design-time classes, like one that generated from EF model or Code First then this is too complicated for your system. This is great if you don't have predefined classes (like in my project where data entity classes are generated at run-time).

My solution (not yet correctly implemented) was to create custom IHttpControllerSelector which selects my generic controller for all requests, there i can set controller's descriptor type to concrete from generic via reflection setting generic parameter depending on request path.

Also a good starting point is http://entityrepository.codeplex.com/ (I've found this somewhere here on stackoverflow)

Alexey
  • 251
  • 2
  • 8
1

What you doing is definitely possible as others said. But for repository dependencies, you should use dependency injection. My typical controller(Api or MVC) would be as follows.

public class PatientCategoryApiController : ApiController
{

    private IEntityRepository<PatientCategory, short> m_Repository;
    public PatientCategoryApiController(IEntityRepository<PatientCategory, short> repository)
    {
        if (repository == null)
            throw new ArgumentNullException("entitiesContext is null");

        m_Repository = repository;
    }
}

This is the typical constructor injection pattern. You need to have a sound understanding of DI and containers like NInject or Autofac. If you dont know DI, then you have a long road ahead. But this is an excellent approach. Take a look at this book. https://www.manning.com/books/dependency-injection-in-dot-net

VivekDev
  • 20,868
  • 27
  • 132
  • 202