13 messaggi dal 13 febbraio 2012
Buonasera a tutti, sto sviluppando API con Web API 2 e entity framework, avrei una curiosità banale che mi sta portando parecchia confusione sull'Unit of Work, ho visto che alcuni lo implementano inserendo anche le interfacce dei repository che si andranno ad utilizzare, mentre altri mettono solo il metodo di salvataggio e magari quelli per la gestione delle transazioni.
Ora il dubbio è, entrambe sono corrette? e se si qual'è il metodo migliore?

Grazie mille a tutti
11.886 messaggi dal 09 febbraio 2002
Contributi
Ciao,
prima di darti la mia opinione (che sarebbe una fra le tante) vorrei chiederti se hai ben capito quali sono i vantaggi che otterrai nel tuo progetto implementando i pattern repository e unit e work.

Cita giusto un paio di cose e poi posta un esempio di codice di come strutturerai le interfacce per il repository e lo unit e work.

ciao,
Moreno

Enjoy learning and just keep making
13 messaggi dal 13 febbraio 2012
Ciao, grazie per la risposta, da quello che ho capito si utilizza il pattern repository per disaccoppiare la web api dal motore database, in poche parole si potrebbe cambiare il tipo di db senza che il controller ne risenta, basta adattare il repository, mentre l'unit of work serve per condividere lo stesso db con tutti i repository avendo una transazione unica, non so se ho capito bene i due concetti, in rete trovo parecchia confusione.

Non potendo condividere il progetto reale (perchè un progetto aziendale) ho messo insieme poche righe di codice.

namespace RepositoryTest.Controllers
{
  [RoutePrefix("api/Person")]
  public class PersonController : BaseApiController
  {
    public PersonController(IUnitOfWork uow)
    {
    }

    [HttpDelete]
    [Route("{id:long}")]
    [SwaggerResponse(HttpStatusCode.OK, "", typeof(void))]
    public IHttpActionResult Delete(long id)
    {
      using (TransactionScope scope = new TransactionScope(
        TransactionScopeOption.Required, TRANSACTION_OPTIONS))
      {
        try
        {
          Person person = UoW.Person.GetById(id);
          if (person == null)
            return NotFound();
          UoW.Person.Delete(person);
          scope.Complete();
          return Ok();
        }
        catch
        {
          scope.Dispose();
          throw;
        }
      }
    }

    [HttpGet]
    [Route("Find/{nome}")]
    [SwaggerResponse(HttpStatusCode.OK, "", typeof(IEnumerable<PersonDTO>))]
    public IHttpActionResult FindByNome(string nome) =>
      Ok(UoW.Person.GetUsersWithHobbiesByName(nome));

    [HttpGet]
    [Route("")]
    [SwaggerResponse(HttpStatusCode.OK, "", typeof(IEnumerable<PersonDTO>))]
    public IHttpActionResult Get() =>
      Ok(UoW.Person.GetAllWithHobbies().ToList());

    [HttpGet]
    [Route("{id:long}")]
    [SwaggerResponse(HttpStatusCode.OK, "", typeof(PersonDTO))]
    public IHttpActionResult GetById(long id)
    {
      PersonDTO user = UoW.Person.GetUserWithHobbiesById(id);
      if (user == null)
        return NotFound();
      return Ok(user);
    }

    [HttpPost]
    [Route("")]
    [SwaggerResponse(HttpStatusCode.Created, "", typeof(PersonDTO))]
    [ValidateModelFilter]
    public IHttpActionResult Post(PersonUpdateDto person)
    {
      using (TransactionScope scope = new TransactionScope(
        TransactionScopeOption.Required, TRANSACTION_OPTIONS))
      {
        try
        {
          PersonDTO model = UoW.Person.CreateWithHobbies(person);
          scope.Complete();
          return CreatedAtRoute("DefaultApi", new
          {
            controller = "Person",
            id = person.Id
          }, model);
        }
        catch
        {
          scope.Dispose();
          throw;
        }
      }
    }

    [HttpPut]
    [Route("{id:long}")]
    [SwaggerResponse(HttpStatusCode.NoContent, "", typeof(void))]
    public IHttpActionResult Update(long id, PersonUpdateDto person)
    {
      using (TransactionScope scope = new TransactionScope(
        TransactionScopeOption.Required, TRANSACTION_OPTIONS))
      {
        try
        {
          if (person.Id != id)
            return BadRequest();
          UoW.Person.UpdateWithHobbies(person);
          scope.Complete();
          return StatusCode(HttpStatusCode.NoContent);
        }
        catch
        {
          scope.Dispose();
          throw;
        }
      }
    }
  }
}

namespace RepositoryTest.BL.Repository
{
  public interface IPersonRepository : IRepositoryBase<Person>
  {
    PersonDTO CreateWithHobbies(PersonUpdateDto person);
    IEnumerable<PersonDTO> GetAllWithHobbies();
    PersonDTO GetUserWithHobbiesById(long id);
    IEnumerable<PersonDTO> GetUsersWithHobbiesByName(string name);
    void UpdateWithHobbies(PersonUpdateDto person);
  }
}

namespace RepositoryTest.BL.Repository
{
  public interface IUnitOfWork : IDisposable
  {
    IPersonRepository Person { get; set; }
  }
}

namespace RepositoryTest.BL.Services
{
  public class UnitOfWork : IUnitOfWork
  {
    private bool m_disposed;
    private readonly IRepositoryTestEntities m_context;

    [Inject]
    public IPersonRepository Person { get; set; }

    public UnitOfWork(IRepositoryTestEntities context)
    {
      m_context = context;
    }

    ~UnitOfWork()
    {
      Dispose();
    }

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

    protected virtual void Dispose(bool disposing)
    {
      if (!m_disposed && disposing)
        m_context.Dispose();
      m_disposed = true;
    }
  }
}

namespace RepositoryTest.BL.Services
{
  public class PersonService : RepositoryBase<Person>, IPersonRepository
  {
    private readonly IRepositoryBase<Hobby> m_hobby;

    public PersonService(RepositoryTestEntities db, IRepositoryBase<Hobby> hobby)
      : base(db)
    {
      m_hobby = hobby;
    }

    public PersonDTO CreateWithHobbies(PersonUpdateDto person)
    {
      Person created = Create(person);
      foreach (long hobbyId in person.Hobbies)
        Db.R_Person_Hobby.Add(new R_Person_Hobby
        {
          HobbyId = hobbyId,
          PersonId = created.Id
        });
      Db.SaveChanges();
      return GetUserWithHobbiesById(created.Id);
    }

    public IEnumerable<PersonDTO> GetAllWithHobbies() =>
      GetWithHobbies(GetAll());

    public PersonDTO GetUserWithHobbiesById(long id)
    {
      List<PersonDTO> persons = GetWithHobbies(GetAll()
        .Where(x => x.Id == id)
        .AsQueryable())
        .ToList();
      return persons.Count == 1
        ? persons[0]
        : null;
    }

    public IEnumerable<PersonDTO> GetUsersWithHobbiesByName(string name)
    {
      IQueryable<Person> persons = GetAll()
        .Where(x => x.Nome.ToLower()
        .Contains(name.ToLower()))
        .AsQueryable();
      return GetWithHobbies(persons);
    }

    private IEnumerable<PersonDTO> GetWithHobbies(IQueryable<Person> persons)
    {
      List<Hobby> hobbies = m_hobby.GetAll().ToList();
      List<PersonDTO> results = new List<PersonDTO>();
      foreach (Person person in persons)
      {
        PersonDTO dto = person;
        IEnumerable<HobbyDTO> r = HobbyDTO.ToList(
          hobbies.Where(x => person.R_Person_Hobby
            .Select(y => y.HobbyId)
            .Contains(x.Id))
          .ToList());
        dto.Hobbies.AddRange(r);
        results.Add(dto);
      }
      return results;
    }

    public void UpdateWithHobbies(PersonUpdateDto person)
    {
      Update(person);

      List<R_Person_Hobby> hobbies = Db.R_Person_Hobby
        .Where(x => x.PersonId == person.Id)
        .ToList();

      List<long> hobbyIds = hobbies
        .Select(x => x.HobbyId)
        .ToList();

      IEnumerable<long> delete = hobbyIds.Except(person.Hobbies);
      foreach (long l in delete)
      {
        R_Person_Hobby r = hobbies.Find(x => x.HobbyId == l);
        if (r != null)
          Db.R_Person_Hobby.Remove(r);
      }

      IEnumerable<long> add = person.Hobbies.Except(hobbyIds);
      foreach (long l in add)
      {
        R_Person_Hobby r = hobbies.Find(x => x.HobbyId == l);
        if (r == null)
          Db.R_Person_Hobby.Add(new R_Person_Hobby
          {
            HobbyId = l,
            PersonId = person.Id
          });
      }

      Db.SaveChanges();
    }
  }
}

namespace RepositoryTest.BL.Services
{
  public class RepositoryBase<TRecord> : IRepositoryBase<TRecord>
    where TRecord : class
  {
    private bool m_disposed;

    private readonly DbSet<TRecord> m_dbSet;

    protected RepositoryTestEntities Db { get; }

    public RepositoryBase(RepositoryTestEntities db)
    {
      Db = db;
      m_dbSet = db.Set<TRecord>();
    }

    ~RepositoryBase()
    {
      Dispose();
    }

    public virtual TRecord Create(TRecord entity)
    {
      Db.Entry(entity).State = EntityState.Added;
      Db.SaveChanges();
      return entity;
    }

    public void Delete(object id)
    {
      TRecord entity = m_dbSet.Find(id);
      Delete(entity);
    }

    public virtual void Delete(TRecord entity)
    {
      if (Db.Entry(entity).State == EntityState.Detached)
        m_dbSet.Attach(entity);
      m_dbSet.Remove(entity);
      Db.SaveChanges();
    }

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

    protected virtual void Dispose(bool disposing)
    {
      if (!m_disposed && disposing)
        Db.Dispose();
      m_disposed = true;
    }

    public virtual IQueryable<TRecord> GetAll() => m_dbSet;

    public virtual TRecord GetById(object id) => m_dbSet.Find(id);

    public virtual void Update(TRecord entity)
    {
      Db.Entry(entity).State = EntityState.Modified;
      Db.SaveChanges();
    }
  }
}


spero sia chiaro e ringrazio nuovamente per l'aiuto.

PS: l'utilizzo del repository pattern mi sarebbe utile nel caso in cui l'azienda decidesse di cambiare di punto in bianco il motore db, adesso utilizziamo sql, però in futuro non si sa
Modificato da Ninja87 il 18 giugno 2019 23:33 -
11.886 messaggi dal 09 febbraio 2002
Contributi
Ok, grazie mille

si utilizza il pattern repository per disaccoppiare la web api dal motore database

Entity Framework, dato che è un ORM basato su provider, già ti consente di disaccoppiare la web api dal motore database.
Ecco l'elenco delle tecnologie database supportate:
https://entityframework.net/supported-database-providers

l'utilizzo del repository pattern mi sarebbe utile nel caso in cui l'azienda decidesse di cambiare di punto in bianco il motore db

Giusta osservazione ma ricordati che non devi prendere per oro colato quello che trovi online (compreso quello che ti sto scrivendo io). Sii sempre critico e realista. Nel tuo caso, che probabilità ha questa situazione di verificarsi?
E perché considerare solo questo, e non altri temi forse più probabili, tipo:
  • E se 100.000 utenti volessero consumare la mia webapi, terrebbe il carico?
  • E se mi chiedessero di creare anche un sito web, oltre alla Web API?
  • E se si verifica un errore imprevisto, informo correttamente l'utente su cosa fare?
  • E se la mia azienda volesse passare a Java perché non si trovano abbastanza sviluppatori .NET?
  • E se l'Italia tornasse alla lira?

Scrivere codice in maniera modulare va bene, ma scriverlo solo perché altri hanno fatto così può portarti a perdere le tempo. Infatti tu non sai a che tipo di progetto stavano lavorando gli altri quando hanno scritto quell'articolo, o quali erano i loro requisiti.
Non è che non devi seguire gli articoli, ma sii sempre critico e chiediti se quello che viene scritto ha senso nel tuo specifico caso.
Ora ti do un'opinione sul codice che hai postato ma, ancora una volta, è solo una fra tante.
public IHttpActionResult Delete(long id)
{
      using (TransactionScope scope = new TransactionScope(
        TransactionScopeOption.Required, TRANSACTION_OPTIONS))
      {
        //...
      }
}

Il codice che hai messo nella delete, secondo me, è codice applicativo e che perciò non dovrebbe trovarsi lì. Se ti dovessero chiedere di sviluppare anche un sito MVC, ti troveresti a dover duplicare il codice.
Oltretutto, perché un'action dovrebbe sapere che è necessario creare un TransactionScope?
La responsabilità di un'action è semplicemente quella di ricevere l'input dell'utente e passarlo all'opportuno servizio applicativo. Quindi il codice situato nell'action, io direi che dovrebbe essere semplicemente questo:
public IHttpActionResult Delete(long id)
{
  return UoW.Person.DeleteById(id);
}

Personalmente non mi piace fare riferimento ad UoW così, perché il suo riferimento è cablato nell'action e ne riduce la testabilità. *Potresti* riceverlo come parametro del costruttore grazie a un motore di dependency injection.
public PersonController : Controller {
  private readonly IUnitOfWork unitOfWork;
  public PersonController(IUnitOfWork unitOfWork) {
     this.unitOfWork = unitOfWork;
  }
  public IHttpActionResult Delete(long id)
  {
    return unitOfWork.Person.DeleteById(id);
  }
}

Ovviamente questo ha senso se scrivi unit testing (dovresti) e testi i tuoi controller.
Altra cosa più importante: UoW lo riassegni ad ogni richiesta, sì? Come fai? Il DbContext non è thread safe e perciò la stessa istanza non dovrebbe essere usata da più richieste contemporanee.
Secondo me, UoW è un concetto che andrebbe relegato in un servizio applicativo. L'action non dovrebbe avere alcuna cognizione di come vengono persistiti gli oggetti né che sia necessario usare il pattern unit of work. Semplicemente, se devi persistere più entità di tipo diverso con una sola azione, ti crei un servizio applicativo che espone un metodo idoneo che accetta un DTO comprensivo di tutte le informazioni, e poi pensa lui a usare i repository (o EF nudo e crudo) per persistere tutto.
Poi, hai notato come improvvisamente tu sia costretto a usare un TransactionScope? Chiediti: perché è stato necessario farlo? Ne è valsa la pena?
A proposito delle eccezioni, non mettere try..catch solo per rilanciare l'eccezione originale con throw. Il metodo Dispose viene automaticamente invocato se hai usato un blocco using, anche quando si verifica un'eccezione. Caso mai usa try...catch per sollevare nuove eccezioni di altro tipo, di livello più alto.
Cioè, cerca di catturare le eccezioni che arrivano dallo strato infrastrutturale (tipo DbUpdateException) e risollevale di altro tipo, più significativo, che possano aiutare la Web API a dare un messaggio utile all'utente. Ad esempio: se il Person che si vuole eliminare non esiste, allora fai sollevare dal repository una PersonNotFoundException in modo che chi consuma la tua API possa vedere scritto "The person with the id {id} does not exist" e gli dai status code 404.
A proposito di questo:
public interface IPersonRepository : IRepositoryBase<Person>
  {
    PersonDTO CreateWithHobbies(PersonUpdateDto person);
    IEnumerable<PersonDTO> GetAllWithHobbies();
    PersonDTO GetUserWithHobbiesById(long id);
    IEnumerable<PersonDTO> GetUsersWithHobbiesByName(string name);
    void UpdateWithHobbies(PersonUpdateDto person);
  }

Ok, hai fatto bene a definire metodi specifici per l'IPersonRepository però ricordati di aggiungere anche DeleteById e ogni altro metodo abbia a che fare con il recupero o la persistenza delle entità. Ad esempio, "GetVeryImportantPeople" o qualsiasi altro metodo richieda la composizione di una query LINQ.
Abituati a usare async/await. Questo è importante per razionalizzare l'uso dei thread e fare in modo che l'applicazione riesca a gestire più richieste contemporanee senza dover aumentare le caratteristiche hardware del server.
ciao,
Moreno

Enjoy learning and just keep making
13 messaggi dal 13 febbraio 2012
Grazie mille per la risposta, nell'esempio che ho postato ho omesso alcune cose per mettere solo quello che serviva, tutti i parametri che vedi nei costruttori sono passati già con DI, ho utilizzato ninject, in più per la gestione delle eccezioni ho creato un middleware separato che si occupa di scrivere eventuali errori nella tabella log del db e informare l'utente su cosa si è verificato, in più non ho inserito le badrequest appositamente solo perchè un codice di esempio.

Per quanto riguarda la possibilità di passare ad un altro db purtroppo la probabilità è alta, stanno pensando di dismettere sql e passare ad un altro db (ma ancora non si sa quale) :(

Per cui se non ho capito male non è necessario utilizzare il pattern unit of work, potrei utilizzare il repository pattern e basta dipende dal caso specifico.
11.886 messaggi dal 09 febbraio 2002
Contributi
Ciao,


Per quanto riguarda la possibilità di passare ad un altro db purtroppo la probabilità è alta, stanno pensando di dismettere sql e passare ad un altro db (ma ancora non si sa quale) :(

Questo andrebbe chiarito il prima possibile. E se scelgono MongoDb? Devi comunque buttar via Entity Framework e rifare tutto lo strato infrastrutturale. Anche se hai usato un repository, il lavoro da rifare sarà comunque importante.

Se invece scelgono MySql, te la cavi perché puoi riutilizzare Entity Framework (e al massimo cambiare qualche peculiarità nel mapping).


non è necessario utilizzare il pattern unit of work

Diciamo che non va esposto nelle action di Web API. Se tu devi salvare diverse entità correlate fra loro, come ad esempio una Person, i suoi Address e i suoi PaymentType, ti crei un DTO che contenga tutte queste informazioni e poi lo passi a un apposito metodo di PersonService. Sarà lui ad aggiungere tutte le entità e poi a persisterle tutte insieme grazie al DbContext (<- è lui che implementa il pattern unit of work per te, non necessariamente devi reimplementarlo tu).

Scegli la strada che ti richiede di scrivere meno codice: non c'è bisogno di creare pure un AddressService e un PaymentTypeService. Puoi semplicemente fare PersonService, dato che person è la root di tutto questo aggregato di entità.

ciao,
Moreno

Enjoy learning and just keep making
13 messaggi dal 13 febbraio 2012
Ciao,

Questo andrebbe chiarito il prima possibile.


sarebbe bellissimo saperlo per tempo

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.