7
\$\begingroup\$

Continuing with my web-based checkers game, this question is about the actual playing system. I'll ask for a review on my UI system in the next post.

First, my BoardController.cs. Note that I have references to a ComponentGenerator class here and in other files. I consider this part of the UI, and will post it later. In fact, that is part of the reason I want a dedicated UI post--so it is up front and center.

public class BoardController : Controller
{
    private readonly IMediator _mediator;
    private readonly Database.Context _context;
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly ComputerPlayer _computerPlayer;

    public BoardController(Database.Context context,
        IHubContext<GameHub> signalRHub,
        ComputerPlayer computerPlayer,
        IMediator mediator)
    {
        _context = context;
        _signalRHub = signalRHub;
        _computerPlayer = computerPlayer;
        _mediator = mediator;
    }

    private Theme GetThemeOrDefault()
    {
        if (Request.Cookies.Keys.All(a => a != "theme"))
        {
            return Theme.Steel;
        }

        return Enum.Parse(typeof(Theme), Request.Cookies["theme"]) as Theme? ?? Theme.Steel;
    }

    private Guid? GetPlayerID()
    {
        if (Request.Cookies.TryGetValue("playerID", out var id))
        {
            return Guid.Parse(id);
        }

        return null;
    }

    private string GetClientConnection(Guid id)
    {
        return _context.Players.Find(id).ConnectionID;
    }

    public ActionResult MovePiece(Guid id, Coord start, Coord end)
    {
        var playerID = GetPlayerID();
        if (!playerID.HasValue)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var game = _context.Games
                .Include("Turns")
                .Include("Turns.Moves")
                .FirstOrDefault(f => f.ID == id);

        if (game == null ||
            game.GameStatus != (int)Status.InProgress ||
            (game.BlackPlayerID != playerID && game.CurrentPlayer == (int)Player.Black) ||
            (game.WhitePlayerID != playerID && game.CurrentPlayer == (int)Player.White))
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var controller = game.ToGameController();

        if (!controller.IsValidMove(start, end))
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var move = controller.Move(start, end);
        move.ID = game.ID;

        var turn = move.MoveHistory.Last().ToPdnTurn();
        if (game.Turns.Any(t => t.MoveNumber == turn.MoveNumber))
        {
            var recordedTurn = game.Turns.Single(s => s.MoveNumber == turn.MoveNumber);
            Database.PdnMove newMove;
            switch (controller.CurrentPlayer)
            {
                case Player.White:
                    newMove = move.MoveHistory.Last().WhiteMove.ToPdnMove();
                    break;
                case Player.Black:
                    newMove = move.MoveHistory.Last().BlackMove.ToPdnMove();
                    break;
                default:
                    throw new ArgumentException();
            }

            var existingMove = recordedTurn.Moves.FirstOrDefault(a => a.Player == (int)controller.CurrentPlayer);
            if (existingMove != null)
            {
                recordedTurn.Moves.Remove(existingMove);
            }
            recordedTurn.Moves.Add(newMove);

            game.Fen = newMove.ResultingFen;
        }
        else
        {
            game.Turns.Add(move.MoveHistory.Last().ToPdnTurn());
            game.Fen = turn.Moves.Single().ResultingFen;
        }

        game.CurrentPosition = move.GetCurrentPosition();
        game.CurrentPlayer = (int)move.CurrentPlayer;
        game.GameStatus = (int)move.GetGameStatus();

        game.RowVersion = DateTime.Now;
        _context.SaveChanges();

        var viewModel = game.ToGameViewModel();
        _mediator.Publish(new OnMoveNotification(viewModel)).Wait();

        return Content("");
    }

    public ActionResult Undo(Guid id)
    {
        var playerID = GetPlayerID();
        if (!playerID.HasValue)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var game = _context.Games
                .Include("Turns")
                .Include("Turns.Moves")
                .FirstOrDefault(f => f.ID == id);

        if (game == null ||
            game.GameStatus != (int)Status.InProgress ||
            game.BlackPlayerID != playerID && game.WhitePlayerID != playerID ||
            game.BlackPlayerID == ComputerPlayer.ComputerPlayerID ||
            game.WhitePlayerID == ComputerPlayer.ComputerPlayerID)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var lastTurn = game.Turns.OrderBy(a => a.MoveNumber).Last();

        if (lastTurn.Moves.Count == 2)
        {
            // todo: figure out which is the last move based on variant
            lastTurn.Moves.Remove(lastTurn.Moves.Single(s => (Player)s.Player == Player.White));
            game.Fen = game.Turns.Last().Moves.Single(s => (Player)s.Player == Player.Black).ResultingFen;
        }
        else
        {
            if (game.Turns.Count == 1)
            {
                Response.StatusCode = 403;
                return null;
            }

            game.Turns.Remove(lastTurn);
            game.Fen = game.Turns.Last().Moves.Single(s => (Player)s.Player == Player.White).ResultingFen;
        }

        game.CurrentPosition = -1;

        switch ((Player)game.CurrentPlayer)
        {
            case Player.White:
                game.CurrentPlayer = (int)Player.Black;
                break;
            case Player.Black:
                game.CurrentPlayer = (int)Player.White;
                break;
            default:
                break;
        }

        game.RowVersion = DateTime.Now;
        _context.SaveChanges();

        _mediator.Publish(new OnMoveNotification(game.ToGameViewModel())).Wait();
        return Content("");
    }

    public ActionResult Resign(Guid id)
    {
        var playerID = GetPlayerID();
        if (!playerID.HasValue)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var game = _context.Games.FirstOrDefault(f => f.ID == id);
        if (game == null ||
            game.GameStatus != (int) Status.InProgress ||
            game.BlackPlayerID != playerID && game.WhitePlayerID != playerID)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        if (game.BlackPlayerID == Guid.Empty || game.WhitePlayerID == Guid.Empty)
        {
            game.GameStatus = (int)Status.Aborted;
        }
        else
        {
            game.GameStatus = playerID == game.BlackPlayerID ? (int)Status.WhiteWin : (int)Status.BlackWin;
        }

        game.RowVersion = DateTime.Now;
        _context.SaveChanges();

        _mediator.Publish(new OnGameCompletedNotification(game.ToGameViewModel()));

        return Content("");
    }

    public ActionResult DisplayGame(Guid moveID, Player orientation)
    {
        var game = _context.Games
                .Include("Turns")
                .Include("Turns.Moves")
                .FirstOrDefault(f => f.Turns.Any(a => a.Moves.Any(m => m.ID == moveID)));

        if (game == null)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var move = game.Turns.SelectMany(t => t.Moves).First(f => f.ID == moveID);

        var viewData = new Dictionary<string, object>
        {
            ["playerID"] = GetPlayerID(),
            ["orientation"] = orientation
        };

        var controller = GameController.FromPosition(Variant.AmericanCheckers, move.ResultingFen);

        var viewModel = game.ToGameViewModel();
        viewModel.Board.GameBoard = controller.Board.GameBoard;
        viewModel.DisplayingLastMove = false;

        var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
        return Content(board);
    }

    public ActionResult Join(Guid id)
    {
        var playerID = GetPlayerID();
        if (!playerID.HasValue)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var game = _context.Games.FirstOrDefault(f => f.ID == id);
        if (game.BlackPlayerID != Guid.Empty && game.WhitePlayerID != Guid.Empty ||
            game.BlackPlayerID == playerID ||
            game.WhitePlayerID == playerID)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        if (game.BlackPlayerID == Guid.Empty)
        {
            game.BlackPlayerID = playerID.Value;
        }
        else if (game.WhitePlayerID == Guid.Empty)
        {
            game.WhitePlayerID = playerID.Value;
        }

        game.RowVersion = DateTime.Now;
        try
        {
            _context.SaveChanges();
            var gameViewModel = game.ToGameViewModel();
            _mediator.Publish(new OnGameJoinedNotification(gameViewModel, playerID.Value)).Wait();

            var viewData = new Dictionary<string, object>
            {
                ["playerID"] = playerID,
                ["orientation"] = game.BlackPlayerID == playerID ? Player.Black : Player.White,
            };

            var board = ComponentGenerator.GetBoard(gameViewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
            return Content(board);
        }
        catch (DbUpdateConcurrencyException)
        {
            _signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");

            Response.StatusCode = 403;
            return Content(Resources.Resources.GameJoined);
        }
    }

    public ActionResult Orientate(Guid id, Guid? moveID, Player orientation)
    {
        var game = _context.Games
                .Include("Turns")
                .Include("Turns.Moves")
                .FirstOrDefault(f => f.ID == id);

        if (game == null)
        {
            Response.StatusCode = 403;
            return Content("");
        }

        var move = game.Turns.SelectMany(t => t.Moves).FirstOrDefault(f => f.ID == moveID) ??
            game.Turns.OrderBy(o => o.MoveNumber).LastOrDefault()?.Moves.OrderBy(a => a.CreatedOn).LastOrDefault();

        Dictionary<string, object>
            viewData = new Dictionary<string, object>
            {
                ["playerID"] = GetPlayerID(),
                ["orientation"] = orientation
            };

        var viewModel = game.ToGameViewModel();
        if (moveID != null && moveID.Value != game.Turns.Last().Moves.OrderBy(o => o.CreatedOn).Last().ID)
        {
            var fen = move.ResultingFen;
            var controller = GameController.FromPosition((Variant)game.Variant, fen);

            viewModel.Board.GameBoard = controller.Board.GameBoard;
            viewModel.DisplayingLastMove = false;
        }

        var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
        return Content(board);
    }
}

I set up the concept of an Action to make it easy to perform multiple discrete responses when something happens. Here they are, in sequence of being performed:

The GameCreated actions (actually used in the HomeController, but since I'm getting the rest of them reviewed anyway...):

public class OnGameCreatedNotification : INotification
{
    public GameViewModel ViewModel { get; }

    public OnGameCreatedNotification(GameViewModel viewModel, Guid currentPlayerID)
    {
        ViewModel = viewModel;
    }
}

public class DoComputerMoveAction : INotificationHandler<OnGameCreatedNotification>
{
    private readonly ComputerPlayer _computerPlayer;
    private readonly IHubContext<GameHub> _signalRHub;
    public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
    {
        _signalRHub = signalRHub;
        _computerPlayer = computerPlayer;
    }

    public async Task Handle(OnGameCreatedNotification request, CancellationToken cancellationToken)
    {
        await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
    }
}

public class AddGameToLobbyAction : INotificationHandler<OnGameCreatedNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;

    public AddGameToLobbyAction(IHubContext<GameHub> signalRHub)
    {
        _signalRHub = signalRHub;
    }

    public Task Handle(OnGameCreatedNotification notification, CancellationToken cancellationToken)
    {
        var lobbyEntry =
$@"<tr>
    <td><a href=""/Home/Game/{notification.ViewModel.ID}"">{Resources.Resources.ResourceManager.GetString(notification.ViewModel.Variant.ToString())}</a></td>
    <td>{Resources.Resources.ResourceManager.GetString(notification.ViewModel.GameStatus.ToString())}</td>
</tr>";

        _signalRHub.Clients.Group("home").InvokeAsync("GameCreated", lobbyEntry);
        return Task.CompletedTask;
    }
}

The GameJoined actions:

public class OnGameJoinedNotification : INotification
{
    public GameViewModel ViewModel { get; }
    public Guid CurrentPlayerID { get; }

    public OnGameJoinedNotification(GameViewModel viewModel, Guid currentPlayerID)
    {
        ViewModel = viewModel;
        CurrentPlayerID = currentPlayerID;
    }
}

public class RemoveGameFromLobbyAction : INotificationHandler<OnGameJoinedNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;

    public RemoveGameFromLobbyAction(IHubContext<GameHub> signalRHub)
    {
        _signalRHub = signalRHub;
    }

    public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
    {
        _signalRHub.Clients.Group("home").InvokeAsync("GameJoined", notification.ViewModel.ID);
        return Task.CompletedTask;
    }
}

public class UpdateControlsAction : INotificationHandler<OnGameJoinedNotification>
{
    private readonly IMediator _mediator;
    private readonly IHubContext<GameHub> _signalRHub;

    public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _mediator = mediator;
    }

    public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
    {
        _signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");
        _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", notification.ViewModel.BlackPlayerID == notification.CurrentPlayerID ? "black-player-text" : "white-player-text", "bold");

        _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", "new-game", "hide");
        _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("RemoveClass", "resign", "hide");

        var clients = new List<IClientProxy>
        {
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.BlackPlayerID)).Result),
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.WhitePlayerID)).Result)
        };

        foreach (var client in clients)
        {
            client.InvokeAsync("SetAttribute", "resign", "title", "Resign");
            client.InvokeAsync("SetHtml", "#resign .sr-only", "Resign");
        }

        return Task.CompletedTask;
    }
}

The Move actions:

public class OnMoveNotification : INotification
{
    public GameViewModel ViewModel { get; }

    public OnMoveNotification(GameViewModel viewModel)
    {
        ViewModel = viewModel;
    }
}

public class DoComputerMoveAction : INotificationHandler<OnMoveNotification>
{
    private readonly ComputerPlayer _computerPlayer;
    private readonly IHubContext<GameHub> _signalRHub;
    public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
    {
        _signalRHub = signalRHub;
        _computerPlayer = computerPlayer;
    }

    public async Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
    }
}

public class UpdateMoveHistoryAction : INotificationHandler<OnMoveNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly IMediator _mediator;
    public UpdateMoveHistoryAction(IHubContext<GameHub> signalRHub, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _mediator = mediator;
    }

    public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;

        _signalRHub.Clients
            .Group(request.ViewModel.ID.ToString())
            .InvokeAsync("UpdateMoves", request.ViewModel.ID, lastMoveDate, ComponentGenerator.GetMoveControl(request.ViewModel.Turns));
        return Task.CompletedTask;
    }
}

public class UpdateOpponentStateAction : INotificationHandler<OnMoveNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly IMediator _mediator;
    public UpdateOpponentStateAction(IHubContext<GameHub> signalRHub, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _mediator = mediator;
    }

    public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
        _signalRHub.Clients
            .Group(request.ViewModel.ID.ToString())
            .InvokeAsync("UpdateOpponentState", request.ViewModel.ID, lastMoveDate, request.ViewModel.CurrentPlayer.ToString(), request.ViewModel.GameStatus.ToString());
        return Task.CompletedTask;
    }
}

public class UpdateControlsAction : INotificationHandler<OnMoveNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly IMediator _mediator;
    public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _mediator = mediator;
    }

    public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        var clients = new List<IClientProxy>
        {
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
        };

        foreach (var client in clients)
        {
            if (request.ViewModel.Turns.Count == 1 && (request.ViewModel.Turns[0].BlackMove == null || request.ViewModel.Turns[0].WhiteMove == null) || request.ViewModel.GameStatus != Status.InProgress)
            {
                client.InvokeAsync("SetAttribute", "undo", "disabled", "");

            }
            else
            {
                client.InvokeAsync("RemoveAttribute", "undo", "disabled");
            }
            client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "RemoveClass" : "AddClass", "new-game", "hide");
            client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "AddClass" : "RemoveClass", "resign", "hide");
        }

        return Task.CompletedTask;
    }
}

This next action isn't used by the game, but rather publishes the data so another system can hook into mine and receive updates about games.

public class SignalGameStateAction : INotificationHandler<OnMoveNotification>
{
    private readonly IHubContext<APIHub> _signalRHub;
    public SignalGameStateAction(IHubContext<APIHub> signalRHub)
    {
        _signalRHub = signalRHub;
    }

    public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        var data = JsonConvert.SerializeObject(request.ViewModel);
        _signalRHub.Clients.All.InvokeAsync("GameChanged", data);

        return Task.CompletedTask;
    }
}

And finally, I really hate this last one, but I don't see any other way in SignalR Core to publish a message to a group with exceptions by connection ID. They have this functionality in SignalR non-Core, so hopefully soon...

public class UpdateBoardAction : INotificationHandler<OnMoveNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly Database.Context _context;
    private readonly IMediator _mediator;
    public UpdateBoardAction(IHubContext<GameHub> signalRHub, Database.Context context, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _context = context;
        _mediator = mediator;
    }

    private string GetClientConnection(Guid id)
    {
        return _context.Players.Find(id).ConnectionID;
    }

    Dictionary<string, object> GetViewData(Guid localPlayerID, Player orientation)
    {
        return new Dictionary<string, object>
        {
            ["playerID"] = localPlayerID,
            ["orientation"] = orientation
        };
    }

    public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
    {
        var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;

        var blackConnection = GetClientConnection(request.ViewModel.BlackPlayerID);
        var whiteConnection = GetClientConnection(request.ViewModel.WhitePlayerID);

        if (request.ViewModel.BlackPlayerID != ComputerPlayer.ComputerPlayerID)
        {
            _signalRHub.Clients.Client(blackConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.Black)),
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.White)));
        }

        if (request.ViewModel.WhitePlayerID != ComputerPlayer.ComputerPlayerID)
        {
            _signalRHub.Clients.Client(whiteConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.Black)),
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.White)));
        }

        _signalRHub.Groups.RemoveAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
        _signalRHub.Groups.RemoveAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();

        _signalRHub.Clients
            .Group(request.ViewModel.ID.ToString())
            .InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.Black)),
                ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.White)));

        _signalRHub.Groups.AddAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
        _signalRHub.Groups.AddAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();

        return Task.CompletedTask;
    }
}

And the GameCompleted actions:

public class OnGameCompletedNotification : INotification
{
    public GameViewModel ViewModel { get; }

    public OnGameCompletedNotification(GameViewModel viewModel)
    {
        ViewModel = viewModel;
    }
}

public class UpdateControlsAction : INotificationHandler<OnGameCompletedNotification>
{
    private readonly IHubContext<GameHub> _signalRHub;
    private readonly IMediator _mediator;
    public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
    {
        _signalRHub = signalRHub;
        _mediator = mediator;
    }

    public Task Handle(OnGameCompletedNotification request, CancellationToken cancellationToken)
    {
        var clients = new List<IClientProxy>
        {
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
            _signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
        };

        foreach (var client in clients)
        {
            client.InvokeAsync("SetAttribute", "undo", "disabled", "");
            client.InvokeAsync("RemoveClass", "new-game", "hide");
            client.InvokeAsync("AddClass", "resign", "hide");
        }

        return Task.CompletedTask;
    }
}

My main concerns here are:

A) Am I following best web practices, including my MVC structure?
B) Is my choice of MediatR for the action system good?
C) See anything else I could change for the better?

\$\endgroup\$
2
\$\begingroup\$

Code Quality

My main concern with this code is that it mixes game logic, with database retrieval and web operations. The I in SOLID stands for Interface Segregation. It is a very useful concept. It allows you to write reuasble parts of code, without hard dependencies, and easy to test.

Proposed Changes

  • Let BoardController handle web requests, responses and cookies. This class should forward requests to your API, where you would have a service.
  • BoardService should not know it is being used in a web context. No cookies, web request or response should be included. All application flow should be in this class.
  • Do not use Database.Context as a dependency, instead provide an interface IBoardRepository which handles that context internally. The service should have a dependency on the repository.
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I'm using Entity Framework's DbSet<T> as my repository. I hate nesting repositories worse than almost any other code smell, since I see it the most. \$\endgroup\$ – user34073 Aug 4 '19 at 21:52
  • \$\begingroup\$ If you use this tight-coupling where I work, you get fired on the spot :) But I get your sentiment. \$\endgroup\$ – dfhwze Aug 5 '19 at 4:20
  • \$\begingroup\$ If I was the boss and you wrapped a repository in a repository, you'd be fired on the spot. EF is a repository by design. \$\endgroup\$ – user34073 Aug 5 '19 at 13:31
  • \$\begingroup\$ By exposing the context to the business layer, business flow could traverse every possible navigation property in your entities. This level of de-encapsulation is one of the main reasons we wrap the EF data layer. \$\endgroup\$ – dfhwze Aug 5 '19 at 13:38
  • \$\begingroup\$ It's a good reason, but I've never seen it work for a data context provider swap in real life--it always needs an extensive refactoring of the application. Using EF and just switching databases, though, I'd just need to reference the new package, and I'd be set to go. \$\endgroup\$ – user34073 Aug 5 '19 at 13:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy