/// <inheritdoc />
        public async Task <List <MatchListing> > ReadMatchListings(MatchFilter filter, MatchSortOrder sortOrder)
        {
            filter = filter ?? new MatchFilter();
            var cachePolicy = _policyRegistry.Get <IAsyncPolicy>(CacheConstants.MatchesPolicy);
            var cacheKey    = CacheConstants.MatchListingsCacheKeyPrefix + _matchFilterSerializer.Serialize(filter) + sortOrder.ToString();

            return(await cachePolicy.ExecuteAsync(async context => await _matchListingDataSource.ReadMatchListings(filter, sortOrder), new Context(cacheKey)));
        }
        /// <summary>
        /// Gets a list of matches and tournaments based on a query
        /// </summary>
        /// <returns>A list of <see cref="MatchListing"/> objects. An empty list if no matches or tournaments are found.</returns>
        public async virtual Task <List <MatchListing> > ReadMatchListings(MatchFilter filter, MatchSortOrder sortOrder)
        {
            if (filter is null)
            {
                filter = new MatchFilter();
            }

            if (!filter.IncludeMatches && !filter.IncludeTournaments)
            {
                return(new List <MatchListing>());
            }

            if (ExcludeTournamentsDueToMatchTypeFilter(filter.MatchResultTypes))
            {
                filter.IncludeTournaments = false;
            }

            using (var connection = _databaseConnectionFactory.CreateDatabaseConnection())
            {
                var selectSql  = new StringBuilder();
                var whereSql   = new StringBuilder();
                var parameters = new Dictionary <string, object>();
                var orderBy    = new List <string>();

                if (filter.IncludeMatches)
                {
                    // Join to MatchInnings only happens if there's a batting team, because otherwise all you get from it is extra rows to process with just a MatchInningsId
                    var matchSelectSql = $@"SELECT m.MatchId, m.MatchName, m.MatchRoute, m.StartTime, m.StartTimeIsKnown, m.MatchType, m.PlayerType, m.PlayersPerTeam, m.MatchResultType,
                                NULL AS TournamentQualificationType, NULL AS SpacesInTournament, m.OrderInTournament,
                                (SELECT TOP 1 AuditDate FROM {Tables.Audit} WHERE EntityUri = CONCAT('{Constants.EntityUriPrefixes.Match}', m.MatchId) ORDER BY AuditDate ASC) AS FirstAuditDate,
                                (SELECT TOP 1 AuditDate FROM {Tables.Audit} WHERE EntityUri = CONCAT('{Constants.EntityUriPrefixes.Match}', m.MatchId) ORDER BY AuditDate DESC) AS LastAuditDate,
                                mt.TeamRole, mt.MatchTeamId,
                                mt.TeamId,
                                i.MatchInningsId, i.Runs, i.Wickets,
                                ml.MatchLocationId, ml.SecondaryAddressableObjectName, ml.PrimaryAddressableObjectName, ml.Locality, ml.Town, ml.Latitude, ml.Longitude
                                FROM { Tables.Match } AS m
                                LEFT JOIN {Tables.MatchTeam} AS mt ON m.MatchId = mt.MatchId
                                LEFT JOIN {Tables.MatchInnings} AS i ON m.MatchId = i.MatchId AND i.BattingMatchTeamId = mt.MatchTeamId
                                LEFT JOIN {Tables.MatchLocation} AS ml ON m.MatchLocationId = ml.MatchLocationId ";

                    selectSql.Append(matchSelectSql);

                    var(matchWhereSql, matchParameters) = BuildMatchQuery(filter,
                                                                          $@"SELECT m.MatchId, m.OrderInTournament, m.StartTime
                           FROM {Tables.Match} AS m
                           <<JOIN>>
                           <<WHERE>> ");
                    whereSql.Append(matchWhereSql);

                    parameters = matchParameters;

                    if (filter.TournamentId != null)
                    {
                        orderBy.Add("OrderInTournament");
                    }
                }

                if (filter.IncludeMatches && filter.IncludeTournaments)
                {
                    selectSql.Append(" UNION ");
                    whereSql.Append(" UNION ");
                }

                if (filter.IncludeTournaments)
                {
                    var tournamentSelectSql = $@"SELECT tourney.TournamentId AS MatchId, tourney.TournamentName AS MatchName, tourney.TournamentRoute AS MatchRoute, tourney.StartTime, tourney.StartTimeIsKnown, 
                                NULL AS MatchType, tourney.PlayerType, tourney.PlayersPerTeam, NULL AS MatchResultType,
                                tourney.QualificationType AS TournamentQualificationType, tourney.SpacesInTournament, NULL AS OrderInTournament, 
                                (SELECT TOP 1 AuditDate FROM {Tables.Audit} WHERE EntityUri = CONCAT('{Constants.EntityUriPrefixes.Tournament}', tourney.TournamentId) ORDER BY AuditDate ASC) AS FirstAuditDate,
                                (SELECT TOP 1 AuditDate FROM {Tables.Audit} WHERE EntityUri = CONCAT('{Constants.EntityUriPrefixes.Tournament}', tourney.TournamentId) ORDER BY AuditDate DESC) AS LastAuditDate,
                                NULL AS TeamRole, NULL AS MatchTeamId,
                                NULL AS TeamId,
                                NULL AS MatchInningsId, NULL AS Runs, NULL AS Wickets,
                                ml.MatchLocationId, ml.SecondaryAddressableObjectName, ml.PrimaryAddressableObjectName, ml.Locality, ml.Town, ml.Latitude, ml.Longitude
                                FROM { Tables.Tournament} AS tourney
                                LEFT JOIN {Tables.MatchLocation} AS ml ON tourney.MatchLocationId = ml.MatchLocationId ";

                    selectSql.Append(tournamentSelectSql);

                    var(tournamentWhereSql, tournamentParameters) = BuildTournamentQuery(filter,
                                                                                         $@"SELECT tourney.TournamentId AS MatchId, NULL AS OrderInTournament, tourney.StartTime
                           FROM {Tables.Tournament} AS tourney
                           <<JOIN>>
                           <<WHERE>> ");

                    whereSql.Append(tournamentWhereSql);

                    foreach (var key in tournamentParameters.Keys)
                    {
                        if (!parameters.ContainsKey(key))
                        {
                            parameters.Add(key, tournamentParameters[key]);
                        }
                    }
                }

                if (sortOrder == MatchSortOrder.MatchDateEarliestFirst)
                {
                    orderBy.Add("StartTime");
                }
                else if (sortOrder == MatchSortOrder.LatestUpdateFirst)
                {
                    orderBy.Add("LastAuditDate DESC");
                }

                var pagedSql = $@"SELECT * FROM ({selectSql}) AS UnfilteredResults 
                                  WHERE MatchId IN 
                                  (
                                      SELECT MatchId FROM ({whereSql}) AS MatchingRecords 
                                      ORDER BY {string.Join(", ", orderBy.ToArray())}
                                      OFFSET @PageOffset ROWS FETCH NEXT @PageSize ROWS ONLY
                                  ) 
                                  ORDER BY {string.Join(", ", orderBy.ToArray())}";

                parameters.Add("@PageOffset", (filter.Paging.PageNumber - 1) * filter.Paging.PageSize);
                parameters.Add("@PageSize", filter.Paging.PageSize);

                var matches = await connection.QueryAsync <MatchListing, TeamInMatch, Team, MatchInnings, MatchLocation, MatchListing>(pagedSql,
                                                                                                                                       (matchListing, teamInMatch, team, matchInnings, location) =>
                {
                    if (teamInMatch != null)
                    {
                        teamInMatch.Team = team;
                        matchListing.Teams.Add(teamInMatch);

                        if (matchInnings != null)
                        {
                            matchInnings.BattingMatchTeamId = teamInMatch.MatchTeamId;
                            matchInnings.BattingTeam        = teamInMatch;
                        }
                        ;
                    }
                    if (matchInnings != null)
                    {
                        matchListing.MatchInnings.Add(matchInnings);
                    }
                    matchListing.MatchLocation = location;
                    return(matchListing);
                },
                                                                                                                                       new DynamicParameters(parameters),
                                                                                                                                       splitOn : "TeamRole, TeamId, MatchInningsId, MatchLocationId").ConfigureAwait(false);

                var listingsToReturn = matches.GroupBy(match => match.MatchRoute).Select(copiesOfMatch =>
                {
                    var matchToReturn          = copiesOfMatch.First();
                    matchToReturn.MatchInnings = copiesOfMatch.Select(match => match.MatchInnings.SingleOrDefault()).OfType <MatchInnings>().ToList();
                    matchToReturn.Teams        = copiesOfMatch.Select(match => match.Teams.SingleOrDefault()).OfType <TeamInMatch>().Distinct(new TeamInMatchEqualityComparer()).ToList();
                    return(matchToReturn);
                }).ToList();

                return(listingsToReturn);
            }
        }