web-dev-qa-db-fra.com

Contrôleur Web Api générique pour prendre en charge tous les modèles

Est-il possible d'avoir une API Web générique qui supportera n'importe quel modèle de votre projet?

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>
{
   //..
}

Serait-ce une bonne approche?

Après tout, je ne fais que répéter les méthodes CRUD? Puis-je utiliser cette classe de base pour faire le travail pour moi?

est-ce correct? Est-ce que tu ferais ça? de meilleures idées?

19
DarthVader

Je l’ai fait pour un petit projet afin d’obtenir quelque chose de prêt à être présenté à un client. Une fois que je suis entré dans les détails des règles métier, de la validation et d’autres considérations, j’ai dû redéfinir les méthodes CRUD de ma classe de base afin que cela ne se traduise pas par une implémentation à long terme. 

J'ai eu des problèmes avec le routage, parce que tout n'utilisait pas unIDdu même type (je travaillais avec un existant système). Certaines tables avaient int clés primaires, d'autres strings et d'autres guids

J'ai fini par avoir des problèmes avec ça aussi. En fin de compte, bien que cela paraisse louche lorsque je l'ai fait pour la première fois, son utilisation dans une implémentation réelle s'est avérée être une affaire différente et ne m'a pas donné plus d'avance du tout.

22
Nick Albrecht

C'est tout à fait possible. Je n'avais jamais eu une raison de le faire auparavant, mais si cela fonctionne dans votre situation, il devrait être bon.

Si tous vos modèles peuvent être sauvegardés et récupérés exactement de la même manière, peut-être qu'ils devraient tous se trouver dans le même contrôleur à la place?

5
eouw0o83hf
 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;
    }
}
2
CourtenayDavison

Si vous avez des classes prédéfinies au moment de la conception, comme celles générées à partir d'un modèle EF ou Code First, cela est trop compliqué pour votre système. C'est très bien si vous n'avez pas de classes prédéfinies (comme dans mon projet où les classes d'entité de données sont générées au moment de l'exécution).

Ma solution (pas encore correctement implémentée) était de créer un IHttpControllerSelector personnalisé qui sélectionne mon contrôleur générique pour toutes les demandes. Je peux ainsi définir le type de descripteur du contrôleur sur concret à partir d'un paramètre générique via le paramètre générique de réglage de réflexion en fonction du chemin d'accès à la demande.

Également un bon point de départ est http://entityrepository.codeplex.com/ (je l’ai trouvé quelque part ici sur stackoverflow)

1
Alexey

Ce que vous faites est certainement possible, comme l'ont dit d'autres personnes. Mais pour les dépendances de référentiel, vous devez utiliser l'injection de dépendance. Mon contrôleur typique (Api ou MVC) serait comme suit.

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;
    }
}

C'est le schéma d'injection typique du constructeur. Vous devez avoir une bonne compréhension de DI et de conteneurs tels que NInject ou Autofac. Si vous ne connaissez pas l'ID, la route est longue. Mais c'est une excellente approche. Regardez ce livre. https://www.manning.com/books/dependency-injection-in-dot-net

1
VivekDev

Rien de mal à cela tant que vous gérez tous les travaux lourds dans vos dépôts. Vous souhaiterez peut-être encapsuler/gérer des exceptions Modelstate dans votre contrôleur de base.

Je suis en train de faire quelque chose de similaire pour un grand projet où les utilisateurs peuvent définir leurs propres entités et API: un utilisateur peut vouloir avoir des utilisateurs et des comptes, tandis qu'un autre peut vouloir suivre les voitures, etc. Ils utilisent tous le même contrôleur interne, mais ils ont chacun leurs propres points de terminaison. 

Vous n'êtes pas sûr de l'utilité de notre code puisque nous n'utilisons pas de génériques (chaque objet est maintenu sous forme de métadonnées et manipulé/transmis sous forme de dictionnaires JObject), mais voici un code pour vous donner une idée de ce que nous faisons et peut-être donner matière à réflexion:

[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);

}
1
AlexGad

C'est tout à fait possible, comme indiqué dans la réponse précédente. C'est une bonne approche et une bonne architecture. Mais je ne comprends pas pourquoi vos contrôleurs ne sont pas publics. Peut-être que c'est votre problème et à cause de cela votre code n'a pas fonctionné?

0
Kirill Bestemyanov