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\$
1
\$\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.
\$\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\$ – Hosch250 Aug 4 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 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\$ – Hosch250 Aug 5 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 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\$ – Hosch250 Aug 5 at 13:47

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.