Architektura aplikacji oraz mapowanie ViewModel - Entity

0

Witam,
piszę aplikację internetową w ASP.NET MVC 5 przy użyciu Entity Fremowork. Mam problem z jej architekturą.

Mam dwa projekty (w solution)

  1. Projekt z danymi.
  • encje
  • generyczna klasa repozytorium
  • unit of work zawierający instancje DbContextu oraz instancje konkretnych repozytoriów
  1. Projekt MVC z referencją do projektu 1.
  • kontrolery
  • viewmodele
    itd.

Problem
Użytkownik wypełnia formularz. Podaje dane, m.in. nazwę drużyny oraz nazwy graczy biorących udział w meczu. Główny problem polega na tym gdzie wstawić kawałek kodu, który w bazie danych wyszukuje czy dana nazwa drużyny już istnieje, to samo z graczami, a następnie tworzy nowe encje lub używa istniejących. Nazwałbym to mapowaniem ViewModelu do encjji.
Bardzo bym prosił o jakąś poradę jak to powinno wyglądać oraz o ocenę czy sama idea repozytoriów i unit of work jest dobrze zaimplementowana.

Poniżej kawałek kodu, który obecnie znajduje się w akcji kontrolera, która obsługuje dane przychodzące z formularza. Działa, ale pewnie akcja kontrolera to nie miejsce gdzie ten kod powinien się znajdować oraz sam kod jest za długi jak na jedną metodę.

[HttpPost]
        public ActionResult Create(AddMatchViewModel viewModel)
        {
            if (ModelState.IsValid)
            {
                Team teamA = unitOfWork.TeamRepository.Get(m => m.Name == viewModel.TeamAName).FirstOrDefault();
                Team teamB = unitOfWork.TeamRepository.Get(m => m.Name == viewModel.TeamBName).FirstOrDefault();

                var matchup = new Matchup()
                {
                    TeamA = teamA ?? new Team() { Name = viewModel.TeamAName },
                    TeamB = teamB ?? new Team() { Name = viewModel.TeamBName }
                };

                foreach (var result in viewModel.MapResults)
                {
                    Map map = unitOfWork.MapRepository.Get(m => m.Name == result.MapName).FirstOrDefault();

                    var mapResult = new MapResult()
                    {
                        Map = map ?? new Map() { Name = result.MapName },
                        TeamAScore = result.TeamAScore,
                        TeamBScore = result.TeamBScore
                    };

                    matchup.MapResults.Add(mapResult);
                }

                for (int playerCounter = 0; playerCounter < viewModel.PlayerResults.Count; playerCounter++)
                {
                    var result = viewModel.PlayerResults[playerCounter];
                    var player = unitOfWork.PlayerRepository.Get(p => p.Name == result.PlayerName).FirstOrDefault()
                                 ?? new Player() { Name = result.PlayerName };

                    for (int mapCounter = 0; mapCounter < viewModel.NumberOfMaps; mapCounter++)
                    {
                        var playerResult = new PlayerResult()
                        {
                            Team = playerCounter < 5 ? teamA : teamB,
                            Player = player,
                            Rating = result.Ratings[mapCounter]
                        };
   
                        ((List<MapResult>)matchup.MapResults)[mapCounter].PlayerResults.Add(playerResult);
                    }
                }

                unitOfWork.MatchupRepository.Add(matchup);
                unitOfWork.Save();

                return RedirectToAction("Index");
            }

            return View();
        }

Viewmodele

public class AddMatchViewModel
    {
        public DateTime DateTime { get; set; }
        public string TeamAName { get; set; }
        public string TeamBName { get; set; }

        public int NumberOfMaps { get; set; }

        public List<MapResultViewModel> MapResults { get; set; } = new List<MapResultViewModel>(3);
        public List<PlayerResultViewModel> PlayerResults { get; set; }
    }

    public class PlayerResultViewModel
    {
        public string PlayerName { get; set; }
        public List<double> Ratings { get; set; } 
    }

    public class MapResultViewModel
    {
        public string MapName { get; set; }
        public int TeamAScore { get; set; }
        public int TeamBScore { get; set; }
    }

Encje (Team, Map oraz Player zawierają tylko int Id oraz string Name, nie będę zaśmiecał):

public class Matchup
    {
        public int Id { get; set; }
        public DateTime DateTime { get; set; }
        public Team TeamA { get; set; }
        public Team TeamB { get; set; }
        public ICollection<MapResult> MapResults { get; set; }
    }

public class MapResult
    {
        public int Id { get; set; }
        public Matchup Match { get; set; }
        public int TeamAScore { get; set; }
        public int TeamBScore { get; set; }
        public Map Map { get; set; }
        public ICollection<PlayerResult> PlayerResults { get; set; }
    }

public class PlayerResult
    {
        public int Id { get; set; }
        public MapResult MapResult { get; set; }
        public Player Player { get; set; }
        public Team Team { get; set; }
        public double Rating { get; set; }
    }

Repozytorium generyczne

public class GenericRepository<T> where T : class 
    {
        public Database Context { get; set; }

        public GenericRepository(Database context)
        {
            Context = context;
        }

        public virtual IQueryable<T> Get(Expression<Func<T, bool>> predicate)
        {
            return Context.Set<T>().Where(predicate);
        }

        public virtual T GetById(int id)
        {
            return Context.Set<T>().Find(id);
        }

        public virtual IQueryable<T> GetAll()
        {
            IQueryable<T> all = Context.Set<T>();
            return all;
        }

        public virtual void Add(T entity)
        {
            Context.Set<T>().Add(entity);
        }

        public virtual void Delete(T entity)
        {
            Context.Set<T>().Remove(entity);
        }
    }

Unit of work


public class UnitOfWork
    {
        private Database context = new Database();
        private GenericRepository<Matchup> matchupRepository;
        private GenericRepository<Team> teamRepository;
        private GenericRepository<Map> mapRepository;
        private GenericRepository<Player> playerRepository;

        public GenericRepository<Matchup> MatchupRepository
        {
            get
            {
                if(this.matchupRepository == null)
                    this.matchupRepository = new GenericRepository<Matchup>(context);

                return this.matchupRepository;
            }
        }

        public GenericRepository<Team> TeamRepository
        {
            get
            {
                if (this.teamRepository == null)
                    this.teamRepository = new GenericRepository<Team>(context);

                return this.teamRepository;
            }
        }

        public GenericRepository<Map> MapRepository
        {
            get
            {
                if (this.mapRepository == null)
                    this.mapRepository = new GenericRepository<Map>(context);

                return this.mapRepository;
            }
        }

        public GenericRepository<Player> PlayerRepository
        {
            get
            {
                if (this.playerRepository == null)
                    this.playerRepository = new GenericRepository<Player>(context);

                return this.playerRepository;
            }
        }

        public void Save()
        {
            context.SaveChanges();
        }
    }
1
gonzono11 napisał(a):

Witam,
piszę aplikację internetową w ASP.NET MVC 5 przy użyciu Entity Fremowork. Mam problem z jej architekturą.

Mam dwa projekty (w solution)

  1. Projekt z danymi.
  • encje
  • generyczna klasa repozytorium
  • unit of work zawierający instancje DbContextu oraz instancje konkretnych repozytoriów
  1. Projekt MVC z referencją do projektu 1.
  • kontrolery
  • viewmodele
    itd.

Problem
Użytkownik wypełnia formularz. Podaje dane, m.in. nazwę drużyny oraz nazwy graczy biorących udział w meczu. Główny problem polega na tym gdzie wstawić kawałek kodu, który w bazie danych wyszukuje czy dana nazwa drużyny już istnieje, to samo z graczami, a następnie tworzy nowe encje lub używa istniejących. Nazwałbym to mapowaniem ViewModelu do encjji.

No to nie jest mapowanie, bo mapowanie to po prostu przepisywanie danych z jednego obiektu w drugi, tylko logika biznesowa. Powinieneś to umieścić w warstwie logiki biznesowej, a warstwę tę oczywiście w oddzielnym projekcie.

Bardzo bym prosił o jakąś poradę jak to powinno wyglądać oraz o ocenę czy sama idea repozytoriów i unit of work jest dobrze zaimplementowana.

Nie, bo:

  1. UoW nie powinien wiedzieć nic o repozytoriach, a na pewno nie powinien mieć w sobie właściwości będących repozytoriami. To złamanie Single Responsibility Principle i Open Closed Principle. Dodając nowe repozytorium musisz aktualizować UoW - jaki to ma sens?
  2. Nie piszesz zgodnie z Domain Driven Design, więc nie Twoje repozytoria to żadne repozytoria tylko zwykłe Data Access Objects.
  3. Generyczne repozytorium to generalnie antywzorzec. Na dodatek całkowicie zbędny.
  4. DbContext sam w sobie jest "generycznym repozytorium" (bo masz DbSety dla poszczególnych typów) oraz UoW (wszystko co zrobisz w ramach jednego kontekstu zatwierdzasz jako jedną transakcję) - jaki jest więc zysk z opakowania go w Twoje struktury?

Poniżej kawałek kodu, który obecnie znajduje się w akcji kontrolera, która obsługuje dane przychodzące z formularza. Działa, ale pewnie akcja kontrolera to nie miejsce gdzie ten kod powinien się znajdować oraz sam kod jest za długi jak na jedną metodę.

Pełna zgoda.

((List<MapResult>)matchup.MapResults)[mapCounter].PlayerResults.Add(playerResult);

A to rzutowanie to po co?

public Database Context { get; set; }

Po co to jest public? To powinno być zwykłe prywatne pole, na dodatek readonly.

public virtual IQueryable<T> GetAll()

A tak naprawdę, to jak często będziesz potrzebował wszystkich rekordów z danej tabeli? Np. jak będziesz miał zapisane 100 tysięcy meczów, użyjesz tego kiedykolwiek?

Unit of work

            get
            {
                if(this.matchupRepository == null)
                    this.matchupRepository = new GenericRepository<Matchup>(context);

                return this.matchupRepository;
            }
        }

Lepiej byłoby już te wszystkie "repozytoria" wstrzykiwać przez konstruktor, a nie łamać Dependency Inversion Principle tworząc te obiekty wewnątrz klasy, która z nich korzysta.

Ogólnie poczytaj o SOLID.
Możesz też rzucić okiem na: http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy/ http://commitandrun.pl/2016/05/30/Brutalne_prawdy_o_MVC/ no i bonusowo http://commitandrun.pl/2016/04/25/Dlaczego_Entity_Framework_nie_jest_dobrym_wyborem/

1 użytkowników online, w tym zalogowanych: 0, gości: 1