Condividi tramite


Inserimento delle dipendenze in API Web ASP.NET 2

Scaricare il progetto completato

Questa esercitazione illustra come inserire le dipendenze nel controller API Web ASP.NET.

Versioni software usate nell'esercitazione

Che cos'è l'inserimento delle dipendenze?

Una dipendenza è qualsiasi oggetto richiesto da un altro oggetto. Ad esempio, è comune definire un repository che gestisce l'accesso ai dati. Di seguito viene illustrato un esempio. Prima di tutto si definirà un modello di dominio:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Ecco una semplice classe di repository che archivia gli elementi in un database usando Entity Framework.

public class ProductsContext : DbContext
{
    public ProductsContext()
        : base("name=ProductsContext")
    {
    }
    public DbSet<Product> Products { get; set; }
}

public class ProductRepository : IDisposable
{
    private ProductsContext db = new ProductsContext();

    public IEnumerable<Product> GetAll()
    {
        return db.Products;
    }
    public Product GetByID(int id)
    {
        return db.Products.FirstOrDefault(p => p.Id == id);
    }
    public void Add(Product product)
    {
        db.Products.Add(product);
        db.SaveChanges();
    }

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (db != null)
            {
                db.Dispose();
                db = null;
            }
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Definire ora un controller API Web che supporta le richieste GET per Product le entità. (Sto lasciando POST e altri metodi per semplicità. Ecco un primo tentativo:

public class ProductsController : ApiController
{
    // This line of code is a problem!
    ProductRepository _repository = new ProductRepository();

    public IEnumerable<Product> Get()
    {
        return _repository.GetAll();
    }

    public IHttpActionResult Get(int id)
    {
        var product = _repository.GetByID(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
}

Si noti che la classe controller dipende ProductRepositoryda e si consente al controller di creare l'istanza ProductRepository . Tuttavia, è una cattiva idea impostare come hardcoded la dipendenza in questo modo, per diversi motivi.

  • Se si vuole sostituire ProductRepository con un'implementazione diversa, è anche necessario modificare la classe controller.
  • Se ha ProductRepository dipendenze, è necessario configurare queste dipendenze all'interno del controller. Per un progetto di grandi dimensioni con più controller, il codice di configurazione diventa sparsi nel progetto.
  • È difficile eseguire unit test, perché il controller è hardcoded per eseguire query sul database. Per uno unit test, è consigliabile usare un repository fittizio o stub, che non è possibile con la progettazione corrente.

È possibile risolvere questi problemi inserendo il repository nel controller. Prima di tutto, eseguire il refactoring della ProductRepository classe in un'interfaccia:

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    Product GetById(int id);
    void Add(Product product);
}

public class ProductRepository : IProductRepository
{
    // Implementation not shown.
}

Specificare quindi come parametro del IProductRepository costruttore:

public class ProductsController : ApiController
{
    private IProductRepository _repository;

    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }

    // Other controller methods not shown.
}

Questo esempio usa l'inserimento del costruttore. È anche possibile usare setter injection, in cui si imposta la dipendenza tramite un metodo o una proprietà setter.

Ma ora c'è un problema, perché l'applicazione non crea direttamente il controller. L'API Web crea il controller quando instrada la richiesta e l'API Web non conosce nulla su IProductRepository. È qui che entra in gioco il sistema di risoluzione delle dipendenze dell'API Web.

Sistema di risoluzione delle dipendenze dell'API Web

L'API Web definisce l'interfaccia IDependencyResolver per la risoluzione delle dipendenze. Ecco la definizione dell'interfaccia:

public interface IDependencyResolver : IDependencyScope, IDisposable
{
    IDependencyScope BeginScope();
}

public interface IDependencyScope : IDisposable
{
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

L'interfaccia IDependencyScope ha due metodi:

  • GetService crea un'istanza di un tipo.
  • GetServices crea una raccolta di oggetti di un tipo specificato.

Il metodo IDependencyResolver eredita IDependencyScope e aggiunge il metodo BeginScope . Più avanti in questa esercitazione verranno descritti gli ambiti.

Quando l'API Web crea un'istanza del controller, chiama prima IDependencyResolver.GetService passando il tipo di controller. È possibile usare questo hook di estendibilità per creare il controller, risolvendo eventuali dipendenze. Se GetService restituisce null, l'API Web cerca un costruttore senza parametri nella classe controller.

Risoluzione delle dipendenze con il contenitore Unity

Sebbene sia possibile scrivere un'implementazione IDependencyResolver completa da zero, l'interfaccia è davvero progettata per fungere da ponte tra l'API Web e i contenitori IoC esistenti.

Un contenitore IoC è un componente software responsabile della gestione delle dipendenze. Registrare i tipi con il contenitore e quindi usare il contenitore per creare oggetti. Il contenitore individua automaticamente le relazioni di dipendenza. Molti contenitori IoC consentono anche di controllare elementi come la durata e l'ambito degli oggetti.

Nota

"IoC" è l'acronimo di "inversione del controllo", che è un modello generale in cui un framework chiama nel codice dell'applicazione. Un contenitore IoC costruisce automaticamente gli oggetti, che "inverte" il normale flusso di controllo.

Per questa esercitazione si userà Unity di Microsoft Patterns & Practices. Altre librerie popolari includono Castle Windsor, Spring.Net, Autofac, Ninject, Simple Injector e StructureMap. È possibile usare nuGet Gestione pacchetti per installare Unity. Dal menu Strumenti in Visual Studio selezionare NuGet Gestione pacchetti e quindi selezionare Gestione pacchetti Console. Nella finestra della console di Gestione pacchetti digitare il comando seguente:

Install-Package Unity

Ecco un'implementazione di IDependencyResolver che esegue il wrapping di un contenitore Unity.

using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

    public UnityResolver(IUnityContainer container)
    {
        if (container == null)
        {
            throw new ArgumentNullException(nameof(container));
        }
        this.container = container;
    }

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

    public IDependencyScope BeginScope()
    {
        var child = container.CreateChildContainer();
        return new UnityResolver(child);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        container.Dispose();
    }
}

Configurazione del sistema di risoluzione delle dipendenze

Impostare il sistema di risoluzione delle dipendenze nella proprietà DependencyResolver dell'oggetto HttpConfiguration globale.

Il codice seguente registra l'interfaccia IProductRepository con Unity e quindi crea un oggetto UnityResolver.

public static void Register(HttpConfiguration config)
{
    var container = new UnityContainer();
    container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
    config.DependencyResolver = new UnityResolver(container);

    // Other Web API configuration not shown.
}

Ambito di dipendenza e durata del controller

I controller vengono creati per ogni richiesta. Per gestire la durata degli oggetti, IDependencyResolver usa il concetto di ambito.

Il sistema di risoluzione delle dipendenze collegato all'oggetto HttpConfiguration ha un ambito globale. Quando l'API Web crea un controller, chiama BeginScope. Questo metodo restituisce un oggetto IDependencyScope che rappresenta un ambito figlio.

L'API Web chiama quindi GetService nell'ambito figlio per creare il controller. Al termine della richiesta, l'API Web chiama Dispose nell'ambito figlio. Utilizzare il metodo Dispose per eliminare le dipendenze del controller.

La modalità di implementazione di BeginScope dipende dal contenitore IoC. Per Unity, l'ambito corrisponde a un contenitore figlio:

public IDependencyScope BeginScope()
{
    var child = container.CreateChildContainer();
    return new UnityResolver(child);
}

La maggior parte dei contenitori IoC ha equivalenti simili.