fix memory leak issue related to AddDbContext not working as expected
This commit is contained in:
parent
b2d282d412
commit
bd3f0caf60
@ -48,10 +48,12 @@ namespace IW4MAdmin.Application.Extensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddDatabaseContext(this IServiceCollection services,
|
public static IServiceCollection AddDatabaseContextOptions(this IServiceCollection services,
|
||||||
ApplicationConfiguration appConfig)
|
ApplicationConfiguration appConfig)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(appConfig.ConnectionString) || appConfig.DatabaseProvider == "sqlite")
|
var activeProvider = appConfig.DatabaseProvider?.ToLower();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(appConfig.ConnectionString) || activeProvider == "sqlite")
|
||||||
{
|
{
|
||||||
var currentPath = Utilities.OperatingDirectory;
|
var currentPath = Utilities.OperatingDirectory;
|
||||||
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
currentPath = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
@ -62,31 +64,34 @@ namespace IW4MAdmin.Application.Extensions
|
|||||||
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
|
{DataSource = Path.Join(currentPath, "Database", "Database.db")};
|
||||||
var connectionString = connectionStringBuilder.ToString();
|
var connectionString = connectionStringBuilder.ToString();
|
||||||
|
|
||||||
services.AddDbContext<DatabaseContext, SqliteDatabaseContext>(options =>
|
var builder = new DbContextOptionsBuilder<SqliteDatabaseContext>()
|
||||||
options.UseSqlite(connectionString), ServiceLifetime.Transient);
|
.UseSqlite(connectionString);
|
||||||
|
|
||||||
|
services.AddSingleton((DbContextOptions) builder.Options);
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (appConfig.DatabaseProvider)
|
switch (activeProvider)
|
||||||
{
|
{
|
||||||
case "mysql":
|
case "mysql":
|
||||||
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
|
var appendTimeout = !appConfig.ConnectionString.Contains("default command timeout",
|
||||||
StringComparison.InvariantCultureIgnoreCase);
|
StringComparison.InvariantCultureIgnoreCase);
|
||||||
services.AddDbContext<DatabaseContext, MySqlDatabaseContext>(options =>
|
var mysqlBuilder = new DbContextOptionsBuilder<MySqlDatabaseContext>()
|
||||||
options.UseMySql(
|
.UseMySql(appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
|
||||||
appConfig.ConnectionString + (appendTimeout ? ";default command timeout=0" : ""),
|
mysqlOptions => mysqlOptions.EnableRetryOnFailure());
|
||||||
mysqlOptions => mysqlOptions.EnableRetryOnFailure()), ServiceLifetime.Transient);
|
services.AddSingleton((DbContextOptions) mysqlBuilder.Options);
|
||||||
break;
|
return services;
|
||||||
case "postgresql":
|
case "postgresql":
|
||||||
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
|
appendTimeout = !appConfig.ConnectionString.Contains("Command Timeout",
|
||||||
StringComparison.InvariantCultureIgnoreCase);
|
StringComparison.InvariantCultureIgnoreCase);
|
||||||
services.AddDbContext<DatabaseContext, PostgresqlDatabaseContext>(options =>
|
var postgresqlBuilder = new DbContextOptionsBuilder<PostgresqlDatabaseContext>()
|
||||||
options.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
|
.UseNpgsql(appConfig.ConnectionString + (appendTimeout ? ";Command Timeout=0" : ""),
|
||||||
postgresqlOptions => postgresqlOptions.EnableRetryOnFailure()), ServiceLifetime.Transient);
|
postgresqlOptions => postgresqlOptions.EnableRetryOnFailure());
|
||||||
break;
|
services.AddSingleton((DbContextOptions) postgresqlBuilder.Options);
|
||||||
|
return services;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException($"No context available for {appConfig.DatabaseProvider}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using SharedLibraryCore.Configuration;
|
||||||
using SharedLibraryCore.Database;
|
using SharedLibraryCore.Database;
|
||||||
|
using SharedLibraryCore.Database.MigrationContext;
|
||||||
using SharedLibraryCore.Interfaces;
|
using SharedLibraryCore.Interfaces;
|
||||||
|
|
||||||
namespace IW4MAdmin.Application.Factories
|
namespace IW4MAdmin.Application.Factories
|
||||||
@ -11,13 +12,15 @@ namespace IW4MAdmin.Application.Factories
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DatabaseContextFactory : IDatabaseContextFactory
|
public class DatabaseContextFactory : IDatabaseContextFactory
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly DbContextOptions _contextOptions;
|
||||||
|
private readonly string _activeProvider;
|
||||||
public DatabaseContextFactory(IServiceProvider serviceProvider)
|
|
||||||
|
public DatabaseContextFactory(ApplicationConfiguration appConfig, DbContextOptions contextOptions)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_contextOptions = contextOptions;
|
||||||
|
_activeProvider = appConfig.DatabaseProvider?.ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// creates a new database context
|
/// creates a new database context
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,10 +28,10 @@ namespace IW4MAdmin.Application.Factories
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DatabaseContext CreateContext(bool? enableTracking = true)
|
public DatabaseContext CreateContext(bool? enableTracking = true)
|
||||||
{
|
{
|
||||||
var context = _serviceProvider.GetRequiredService<DatabaseContext>();
|
var context = BuildContext();
|
||||||
|
|
||||||
enableTracking ??= true;
|
enableTracking ??= true;
|
||||||
|
|
||||||
if (enableTracking.Value)
|
if (enableTracking.Value)
|
||||||
{
|
{
|
||||||
context.ChangeTracker.AutoDetectChangesEnabled = true;
|
context.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||||
@ -44,5 +47,16 @@ namespace IW4MAdmin.Application.Factories
|
|||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DatabaseContext BuildContext()
|
||||||
|
{
|
||||||
|
return _activeProvider switch
|
||||||
|
{
|
||||||
|
"sqlite" => new SqliteDatabaseContext(_contextOptions),
|
||||||
|
"mysql" => new MySqlDatabaseContext(_contextOptions),
|
||||||
|
"postgresql" => new PostgresqlDatabaseContext(_contextOptions),
|
||||||
|
_ => throw new ArgumentException($"No context found for {_activeProvider}")
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -361,7 +361,7 @@ namespace IW4MAdmin.Application
|
|||||||
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
|
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
|
||||||
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
|
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
|
||||||
.AddSingleton(translationLookup)
|
.AddSingleton(translationLookup)
|
||||||
.AddDatabaseContext(appConfig);
|
.AddDatabaseContextOptions(appConfig);
|
||||||
|
|
||||||
if (args.Contains("serialevents"))
|
if (args.Contains("serialevents"))
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
|
|||||||
|
|
||||||
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
public async Task<ResourceQueryHelperResult<AdministeredPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
||||||
{
|
{
|
||||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
var iqPenalties = ctx.Penalties.AsNoTracking()
|
var iqPenalties = ctx.Penalties.AsNoTracking()
|
||||||
.Where(_penalty => query.ClientId == _penalty.PunisherId)
|
.Where(_penalty => query.ClientId == _penalty.PunisherId)
|
||||||
|
@ -31,7 +31,7 @@ namespace IW4MAdmin.Application.Meta
|
|||||||
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
public async Task<ResourceQueryHelperResult<ReceivedPenaltyResponse>> QueryResource(ClientPaginationRequest query)
|
||||||
{
|
{
|
||||||
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
|
var linkedPenaltyType = Utilities.LinkedPenaltyTypes();
|
||||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
var linkId = await ctx.Clients.AsNoTracking()
|
var linkId = await ctx.Clients.AsNoTracking()
|
||||||
.Where(_client => _client.ClientId == query.ClientId)
|
.Where(_client => _client.ClientId == query.ClientId)
|
||||||
|
@ -28,7 +28,7 @@ namespace IW4MAdmin.Application.Meta
|
|||||||
|
|
||||||
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
|
public async Task<ResourceQueryHelperResult<UpdatedAliasResponse>> QueryResource(ClientPaginationRequest query)
|
||||||
{
|
{
|
||||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
|
int linkId = ctx.Clients.First(_client => _client.ClientId == query.ClientId).AliasLinkId;
|
||||||
|
|
||||||
var iqAliasUpdates = ctx.Aliases
|
var iqAliasUpdates = ctx.Aliases
|
||||||
|
@ -37,7 +37,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var ctx = _contextFactory.CreateContext();
|
await using var ctx = _contextFactory.CreateContext();
|
||||||
|
|
||||||
var existingMeta = await ctx.EFMeta
|
var existingMeta = await ctx.EFMeta
|
||||||
.Where(_meta => _meta.Key == metaKey)
|
.Where(_meta => _meta.Key == metaKey)
|
||||||
@ -66,7 +66,7 @@ namespace IW4MAdmin.Application.Misc
|
|||||||
|
|
||||||
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
|
public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
|
||||||
{
|
{
|
||||||
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
return await ctx.EFMeta
|
return await ctx.EFMeta
|
||||||
.Where(_meta => _meta.Key == metaKey)
|
.Where(_meta => _meta.Key == metaKey)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using SharedLibraryCore;
|
using SharedLibraryCore;
|
||||||
using IW4MAdmin.Plugins.Stats.Models;
|
using IW4MAdmin.Plugins.Stats.Models;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -19,7 +18,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
private readonly IDatabaseContextFactory _contextFactory;
|
private readonly IDatabaseContextFactory _contextFactory;
|
||||||
private readonly CommandConfiguration _config;
|
private readonly CommandConfiguration _config;
|
||||||
|
|
||||||
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup, IDatabaseContextFactory contextFactory) : base(config, translationLookup)
|
public MostKillsCommand(CommandConfiguration config, ITranslationLookup translationLookup,
|
||||||
|
IDatabaseContextFactory contextFactory) : base(config, translationLookup)
|
||||||
{
|
{
|
||||||
Name = "mostkills";
|
Name = "mostkills";
|
||||||
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"];
|
Description = translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_DESC"];
|
||||||
@ -32,7 +32,8 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
|
|
||||||
public override async Task ExecuteAsync(GameEvent E)
|
public override async Task ExecuteAsync(GameEvent E)
|
||||||
{
|
{
|
||||||
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(), _contextFactory, _translationLookup);
|
var mostKills = await GetMostKills(StatManager.GetIdForServer(E.Owner), Plugin.Config.Configuration(),
|
||||||
|
_contextFactory, _translationLookup);
|
||||||
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
|
if (!E.Message.IsBroadcastCommand(_config.BroadcastCommandPrefix))
|
||||||
{
|
{
|
||||||
foreach (var stat in mostKills)
|
foreach (var stat in mostKills)
|
||||||
@ -50,33 +51,33 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config, IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
|
public static async Task<IEnumerable<string>> GetMostKills(long? serverId, StatsConfiguration config,
|
||||||
|
IDatabaseContextFactory contextFactory, ITranslationLookup translationLookup)
|
||||||
{
|
{
|
||||||
using (var ctx = contextFactory.CreateContext(enableTracking: false))
|
await using var ctx = contextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
|
||||||
var dayInPast = DateTime.UtcNow.AddDays(-config.MostKillsMaxInactivityDays);
|
|
||||||
|
|
||||||
var iqStats = (from stats in ctx.Set<EFClientStatistics>()
|
var iqStats = (from stats in ctx.Set<EFClientStatistics>()
|
||||||
join client in ctx.Clients
|
join client in ctx.Clients
|
||||||
on stats.ClientId equals client.ClientId
|
on stats.ClientId equals client.ClientId
|
||||||
join alias in ctx.Aliases
|
join alias in ctx.Aliases
|
||||||
on client.CurrentAliasId equals alias.AliasId
|
on client.CurrentAliasId equals alias.AliasId
|
||||||
where stats.ServerId == serverId
|
where stats.ServerId == serverId
|
||||||
where client.Level != EFClient.Permission.Banned
|
where client.Level != EFClient.Permission.Banned
|
||||||
where client.LastConnection >= dayInPast
|
where client.LastConnection >= dayInPast
|
||||||
orderby stats.Kills descending
|
orderby stats.Kills descending
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
alias.Name,
|
alias.Name,
|
||||||
stats.Kills
|
stats.Kills
|
||||||
})
|
})
|
||||||
.Take(config.MostKillsClientLimit);
|
.Take(config.MostKillsClientLimit);
|
||||||
|
|
||||||
var iqList = await iqStats.ToListAsync();
|
var iqList = await iqStats.ToListAsync();
|
||||||
|
|
||||||
return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"].FormatExt(index + 1, stats.Name, stats.Kills))
|
return iqList.Select((stats, index) => translationLookup["PLUGINS_STATS_COMMANDS_MOSTKILLS_FORMAT"]
|
||||||
.Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]);
|
.FormatExt(index + 1, stats.Name, stats.Kills))
|
||||||
}
|
.Prepend(Utilities.CurrentLocalization.LocalizationIndex["PLUGINS_STATS_COMMANDS_MOSTKILLS_HEADER"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -68,121 +68,118 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<int> GetClientOverallRanking(int clientId)
|
public async Task<int> GetClientOverallRanking(int clientId)
|
||||||
{
|
{
|
||||||
using (var context = _contextFactory.CreateContext(enableTracking: false))
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
|
var clientPerformance = await context.Set<EFRating>()
|
||||||
|
.Where(r => r.RatingHistory.ClientId == clientId)
|
||||||
|
.Where(r => r.ServerId == null)
|
||||||
|
.Where(r => r.Newest)
|
||||||
|
.Select(r => r.Performance)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (clientPerformance != 0)
|
||||||
{
|
{
|
||||||
var clientPerformance = await context.Set<EFRating>()
|
var iqClientRanking = context.Set<EFRating>()
|
||||||
.Where(r => r.RatingHistory.ClientId == clientId)
|
.Where(r => r.RatingHistory.ClientId != clientId)
|
||||||
.Where(r => r.ServerId == null)
|
.Where(r => r.Performance > clientPerformance)
|
||||||
.Where(r => r.Newest)
|
.Where(GetRankingFunc());
|
||||||
.Select(r => r.Performance)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if (clientPerformance != 0)
|
return await iqClientRanking.CountAsync() + 1;
|
||||||
{
|
|
||||||
var iqClientRanking = context.Set<EFRating>()
|
|
||||||
.Where(r => r.RatingHistory.ClientId != clientId)
|
|
||||||
.Where(r => r.Performance > clientPerformance)
|
|
||||||
.Where(GetRankingFunc());
|
|
||||||
|
|
||||||
return await iqClientRanking.CountAsync() + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
||||||
{
|
{
|
||||||
using (var context = _contextFactory.CreateContext(enableTracking: false))
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
// setup the query for the clients within the given rating range
|
||||||
// setup the query for the clients within the given rating range
|
var iqClientRatings = (from rating in context.Set<EFRating>()
|
||||||
var iqClientRatings = (from rating in context.Set<EFRating>()
|
.Where(GetRankingFunc(serverId))
|
||||||
.Where(GetRankingFunc(serverId))
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
rating.RatingHistory.ClientId,
|
|
||||||
rating.RatingHistory.Client.CurrentAlias.Name,
|
|
||||||
rating.RatingHistory.Client.LastConnection,
|
|
||||||
rating.Performance,
|
|
||||||
})
|
|
||||||
.OrderByDescending(c => c.Performance)
|
|
||||||
.Skip(start)
|
|
||||||
.Take(count);
|
|
||||||
|
|
||||||
// materialized list
|
|
||||||
var clientRatings = await iqClientRatings.ToListAsync();
|
|
||||||
|
|
||||||
// get all the unique client ids that are in the top stats
|
|
||||||
var clientIds = clientRatings
|
|
||||||
.GroupBy(r => r.ClientId)
|
|
||||||
.Select(r => r.First().ClientId)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var iqRatingInfo = from rating in context.Set<EFRating>()
|
|
||||||
where clientIds.Contains(rating.RatingHistory.ClientId)
|
|
||||||
where rating.ServerId == serverId
|
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
rating.Ranking,
|
|
||||||
rating.Performance,
|
|
||||||
rating.RatingHistory.ClientId,
|
rating.RatingHistory.ClientId,
|
||||||
rating.When
|
rating.RatingHistory.Client.CurrentAlias.Name,
|
||||||
};
|
rating.RatingHistory.Client.LastConnection,
|
||||||
|
rating.Performance,
|
||||||
|
})
|
||||||
|
.OrderByDescending(c => c.Performance)
|
||||||
|
.Skip(start)
|
||||||
|
.Take(count);
|
||||||
|
|
||||||
var ratingInfo = (await iqRatingInfo.ToListAsync())
|
// materialized list
|
||||||
.GroupBy(r => r.ClientId)
|
var clientRatings = await iqClientRatings.ToListAsync();
|
||||||
.Select(grp => new
|
|
||||||
{
|
|
||||||
grp.Key,
|
|
||||||
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
|
|
||||||
});
|
|
||||||
|
|
||||||
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
// get all the unique client ids that are in the top stats
|
||||||
where clientIds.Contains(stat.ClientId)
|
var clientIds = clientRatings
|
||||||
where stat.Kills > 0 || stat.Deaths > 0
|
.GroupBy(r => r.ClientId)
|
||||||
where serverId == null ? true : stat.ServerId == serverId
|
.Select(r => r.First().ClientId)
|
||||||
group stat by stat.ClientId into s
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
ClientId = s.Key,
|
|
||||||
Kills = s.Sum(c => c.Kills),
|
|
||||||
Deaths = s.Sum(c => c.Deaths),
|
|
||||||
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
|
|
||||||
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
|
||||||
});
|
|
||||||
|
|
||||||
var topPlayers = await iqStatsInfo.ToListAsync();
|
|
||||||
|
|
||||||
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
|
|
||||||
var finished = topPlayers.Select(s => new TopStatsInfo()
|
|
||||||
{
|
|
||||||
ClientId = s.ClientId,
|
|
||||||
Id = (int?)serverId ?? 0,
|
|
||||||
Deaths = s.Deaths,
|
|
||||||
Kills = s.Kills,
|
|
||||||
KDR = Math.Round(s.KDR, 2),
|
|
||||||
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
|
|
||||||
Name = clientRatingsDict[s.ClientId].Name,
|
|
||||||
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
|
|
||||||
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
|
|
||||||
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ?
|
|
||||||
ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() :
|
|
||||||
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
|
|
||||||
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
|
||||||
})
|
|
||||||
.OrderByDescending(r => r.Performance)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// set the ranking numerically
|
var iqRatingInfo = from rating in context.Set<EFRating>()
|
||||||
int i = start + 1;
|
where clientIds.Contains(rating.RatingHistory.ClientId)
|
||||||
foreach (var stat in finished)
|
where rating.ServerId == serverId
|
||||||
{
|
select new
|
||||||
stat.Ranking = i;
|
{
|
||||||
i++;
|
rating.Ranking,
|
||||||
}
|
rating.Performance,
|
||||||
|
rating.RatingHistory.ClientId,
|
||||||
|
rating.When
|
||||||
|
};
|
||||||
|
|
||||||
return finished;
|
var ratingInfo = (await iqRatingInfo.ToListAsync())
|
||||||
|
.GroupBy(r => r.ClientId)
|
||||||
|
.Select(grp => new
|
||||||
|
{
|
||||||
|
grp.Key,
|
||||||
|
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
|
||||||
|
});
|
||||||
|
|
||||||
|
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
|
||||||
|
where clientIds.Contains(stat.ClientId)
|
||||||
|
where stat.Kills > 0 || stat.Deaths > 0
|
||||||
|
where serverId == null ? true : stat.ServerId == serverId
|
||||||
|
group stat by stat.ClientId into s
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
ClientId = s.Key,
|
||||||
|
Kills = s.Sum(c => c.Kills),
|
||||||
|
Deaths = s.Sum(c => c.Deaths),
|
||||||
|
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) / s.Sum(c => c.TimePlayed),
|
||||||
|
TotalTimePlayed = s.Sum(c => c.TimePlayed),
|
||||||
|
});
|
||||||
|
|
||||||
|
var topPlayers = await iqStatsInfo.ToListAsync();
|
||||||
|
|
||||||
|
var clientRatingsDict = clientRatings.ToDictionary(r => r.ClientId);
|
||||||
|
var finished = topPlayers.Select(s => new TopStatsInfo()
|
||||||
|
{
|
||||||
|
ClientId = s.ClientId,
|
||||||
|
Id = (int?)serverId ?? 0,
|
||||||
|
Deaths = s.Deaths,
|
||||||
|
Kills = s.Kills,
|
||||||
|
KDR = Math.Round(s.KDR, 2),
|
||||||
|
LastSeen = (DateTime.UtcNow - clientRatingsDict[s.ClientId].LastConnection).HumanizeForCurrentCulture(),
|
||||||
|
Name = clientRatingsDict[s.ClientId].Name,
|
||||||
|
Performance = Math.Round(clientRatingsDict[s.ClientId].Performance, 2),
|
||||||
|
RatingChange = ratingInfo.First(r => r.Key == s.ClientId).Ratings.First().Ranking - ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
|
||||||
|
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 ?
|
||||||
|
ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When).Select(r => r.Performance).ToList() :
|
||||||
|
new List<double>() { clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance },
|
||||||
|
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
|
||||||
|
})
|
||||||
|
.OrderByDescending(r => r.Performance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// set the ranking numerically
|
||||||
|
int i = start + 1;
|
||||||
|
foreach (var stat in finished)
|
||||||
|
{
|
||||||
|
stat.Ranking = i;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -202,63 +199,61 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
long serverId = GetIdForServer(sv);
|
long serverId = GetIdForServer(sv);
|
||||||
EFServer server;
|
EFServer server;
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
var serverSet = ctx.Set<EFServer>();
|
||||||
|
// get the server from the database if it exists, otherwise create and insert a new one
|
||||||
|
server = serverSet.FirstOrDefault(s => s.ServerId == serverId);
|
||||||
|
|
||||||
|
// the server might be using legacy server id
|
||||||
|
if (server == null)
|
||||||
{
|
{
|
||||||
var serverSet = ctx.Set<EFServer>();
|
server = serverSet.FirstOrDefault(s => s.EndPoint == sv.ToString());
|
||||||
// get the server from the database if it exists, otherwise create and insert a new one
|
|
||||||
server = serverSet.FirstOrDefault(s => s.ServerId == serverId);
|
|
||||||
|
|
||||||
// the server might be using legacy server id
|
if (server != null)
|
||||||
if (server == null)
|
|
||||||
{
|
{
|
||||||
server = serverSet.FirstOrDefault(s => s.EndPoint == sv.ToString());
|
// this provides a way to identify legacy server entries
|
||||||
|
server.EndPoint = sv.ToString();
|
||||||
if (server != null)
|
ctx.Update(server);
|
||||||
{
|
|
||||||
// this provides a way to identify legacy server entries
|
|
||||||
server.EndPoint = sv.ToString();
|
|
||||||
ctx.Update(server);
|
|
||||||
ctx.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// server has never been added before
|
|
||||||
if (server == null)
|
|
||||||
{
|
|
||||||
server = new EFServer()
|
|
||||||
{
|
|
||||||
Port = sv.Port,
|
|
||||||
EndPoint = sv.ToString(),
|
|
||||||
ServerId = serverId,
|
|
||||||
GameName = sv.GameName,
|
|
||||||
HostName = sv.Hostname
|
|
||||||
};
|
|
||||||
|
|
||||||
server = serverSet.Add(server).Entity;
|
|
||||||
// this doesn't need to be async as it's during initialization
|
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// we want to set the gamename up if it's never been set, or it changed
|
// server has never been added before
|
||||||
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
|
if (server == null)
|
||||||
|
{
|
||||||
|
server = new EFServer()
|
||||||
{
|
{
|
||||||
server.GameName = sv.GameName;
|
Port = sv.Port,
|
||||||
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
EndPoint = sv.ToString(),
|
||||||
ctx.SaveChanges();
|
ServerId = serverId,
|
||||||
}
|
GameName = sv.GameName,
|
||||||
|
HostName = sv.Hostname
|
||||||
|
};
|
||||||
|
|
||||||
if (server.HostName == null || server.HostName != sv.Hostname)
|
server = serverSet.Add(server).Entity;
|
||||||
{
|
// this doesn't need to be async as it's during initialization
|
||||||
server.HostName = sv.Hostname;
|
|
||||||
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
|
|
||||||
ctx.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
|
|
||||||
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
|
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we want to set the gamename up if it's never been set, or it changed
|
||||||
|
else if (!server.GameName.HasValue || server.GameName.HasValue && server.GameName.Value != sv.GameName)
|
||||||
|
{
|
||||||
|
server.GameName = sv.GameName;
|
||||||
|
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.HostName == null || server.HostName != sv.Hostname)
|
||||||
|
{
|
||||||
|
server.HostName = sv.Hostname;
|
||||||
|
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Entry(server).Property(_prop => _prop.IsPasswordProtected).IsModified = true;
|
||||||
|
server.IsPasswordProtected = !string.IsNullOrEmpty(sv.GamePassword);
|
||||||
|
ctx.SaveChanges();
|
||||||
|
|
||||||
// check to see if the stats have ever been initialized
|
// check to see if the stats have ever been initialized
|
||||||
var serverStats = InitializeServerStats(server.ServerId);
|
var serverStats = InitializeServerStats(server.ServerId);
|
||||||
|
|
||||||
@ -304,79 +299,77 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
EFClientStatistics clientStats;
|
EFClientStatistics clientStats;
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
var clientStatsSet = ctx.Set<EFClientStatistics>();
|
||||||
|
clientStats = clientStatsSet
|
||||||
|
.Include(cl => cl.HitLocations)
|
||||||
|
.FirstOrDefault(c => c.ClientId == pl.ClientId && c.ServerId == serverId);
|
||||||
|
|
||||||
|
if (clientStats == null)
|
||||||
{
|
{
|
||||||
var clientStatsSet = ctx.Set<EFClientStatistics>();
|
clientStats = new EFClientStatistics()
|
||||||
clientStats = clientStatsSet
|
|
||||||
.Include(cl => cl.HitLocations)
|
|
||||||
.FirstOrDefault(c => c.ClientId == pl.ClientId && c.ServerId == serverId);
|
|
||||||
|
|
||||||
if (clientStats == null)
|
|
||||||
{
|
{
|
||||||
clientStats = new EFClientStatistics()
|
Active = true,
|
||||||
{
|
ClientId = pl.ClientId,
|
||||||
Active = true,
|
Deaths = 0,
|
||||||
ClientId = pl.ClientId,
|
Kills = 0,
|
||||||
Deaths = 0,
|
ServerId = serverId,
|
||||||
Kills = 0,
|
Skill = 0.0,
|
||||||
ServerId = serverId,
|
SPM = 0.0,
|
||||||
Skill = 0.0,
|
EloRating = 200.0,
|
||||||
SPM = 0.0,
|
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>()
|
||||||
EloRating = 200.0,
|
|
||||||
HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation)).OfType<IW4Info.HitLocation>()
|
|
||||||
.Select(hl => new EFHitLocationCount()
|
|
||||||
{
|
|
||||||
Active = true,
|
|
||||||
HitCount = 0,
|
|
||||||
Location = hl
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
|
|
||||||
// insert if they've not been added
|
|
||||||
clientStats = clientStatsSet.Add(clientStats).Entity;
|
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
pl.SetAdditionalProperty(CLIENT_STATS_KEY, clientStats);
|
|
||||||
|
|
||||||
// migration for previous existing stats
|
|
||||||
if (clientStats.HitLocations.Count == 0)
|
|
||||||
{
|
|
||||||
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation))
|
|
||||||
.OfType<IW4Info.HitLocation>()
|
|
||||||
.Select(hl => new EFHitLocationCount()
|
.Select(hl => new EFHitLocationCount()
|
||||||
{
|
{
|
||||||
Active = true,
|
Active = true,
|
||||||
HitCount = 0,
|
HitCount = 0,
|
||||||
Location = hl
|
Location = hl
|
||||||
})
|
}).ToList()
|
||||||
.ToList();
|
};
|
||||||
|
|
||||||
ctx.Update(clientStats);
|
// insert if they've not been added
|
||||||
await ctx.SaveChangesAsync();
|
clientStats = clientStatsSet.Add(clientStats).Entity;
|
||||||
}
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
// for stats before rating
|
|
||||||
if (clientStats.EloRating == 0.0)
|
|
||||||
{
|
|
||||||
clientStats.EloRating = clientStats.Skill;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientStats.RollingWeightedKDR == 0)
|
|
||||||
{
|
|
||||||
clientStats.RollingWeightedKDR = clientStats.KDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set these on connecting
|
|
||||||
clientStats.LastActive = DateTime.UtcNow;
|
|
||||||
clientStats.LastStatCalculation = DateTime.UtcNow;
|
|
||||||
clientStats.SessionScore = pl.Score;
|
|
||||||
clientStats.LastScore = pl.Score;
|
|
||||||
|
|
||||||
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
|
|
||||||
_log.LogDebug("Added {client} to stats", pl.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pl.SetAdditionalProperty(CLIENT_STATS_KEY, clientStats);
|
||||||
|
|
||||||
|
// migration for previous existing stats
|
||||||
|
if (clientStats.HitLocations.Count == 0)
|
||||||
|
{
|
||||||
|
clientStats.HitLocations = Enum.GetValues(typeof(IW4Info.HitLocation))
|
||||||
|
.OfType<IW4Info.HitLocation>()
|
||||||
|
.Select(hl => new EFHitLocationCount()
|
||||||
|
{
|
||||||
|
Active = true,
|
||||||
|
HitCount = 0,
|
||||||
|
Location = hl
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ctx.Update(clientStats);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// for stats before rating
|
||||||
|
if (clientStats.EloRating == 0.0)
|
||||||
|
{
|
||||||
|
clientStats.EloRating = clientStats.Skill;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientStats.RollingWeightedKDR == 0)
|
||||||
|
{
|
||||||
|
clientStats.RollingWeightedKDR = clientStats.KDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set these on connecting
|
||||||
|
clientStats.LastActive = DateTime.UtcNow;
|
||||||
|
clientStats.LastStatCalculation = DateTime.UtcNow;
|
||||||
|
clientStats.SessionScore = pl.Score;
|
||||||
|
clientStats.LastScore = pl.Score;
|
||||||
|
|
||||||
|
pl.SetAdditionalProperty(CLIENT_DETECTIONS_KEY, new Detection(_log, clientStats));
|
||||||
|
_log.LogDebug("Added {client} to stats", pl.ToString());
|
||||||
|
|
||||||
return clientStats;
|
return clientStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,11 +427,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
private async Task SaveClientStats(EFClientStatistics clientStats)
|
private async Task SaveClientStats(EFClientStatistics clientStats)
|
||||||
{
|
{
|
||||||
using (var ctx = _contextFactory.CreateContext())
|
await using var ctx = _contextFactory.CreateContext();
|
||||||
{
|
ctx.Update(clientStats);
|
||||||
ctx.Update(clientStats);
|
await ctx.SaveChangesAsync();
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
|
public void AddDamageEvent(string eventLine, int attackerClientId, int victimClientId, long serverId)
|
||||||
@ -628,13 +619,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
public async Task SaveHitCache(long serverId)
|
public async Task SaveHitCache(long serverId)
|
||||||
{
|
{
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
var server = _servers[serverId];
|
||||||
var server = _servers[serverId];
|
ctx.AddRange(server.HitCache.ToList());
|
||||||
ctx.AddRange(server.HitCache.ToList());
|
await ctx.SaveChangesAsync();
|
||||||
await ctx.SaveChangesAsync();
|
server.HitCache.Clear();
|
||||||
server.HitCache.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
|
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
|
||||||
@ -714,14 +703,12 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
EFACSnapshot change;
|
EFACSnapshot change;
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _contextFactory.CreateContext();
|
||||||
|
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
|
||||||
{
|
{
|
||||||
while ((change = clientDetection.Tracker.GetNextChange()) != default(EFACSnapshot))
|
ctx.Add(change);
|
||||||
{
|
|
||||||
ctx.Add(change);
|
|
||||||
}
|
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddStandardKill(EFClient attacker, EFClient victim)
|
public async Task AddStandardKill(EFClient attacker, EFClient victim)
|
||||||
@ -826,160 +813,158 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
|
|
||||||
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
|
int currentServerTotalPlaytime = clientStats.TimePlayed + currentSessionTime;
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: true))
|
await using var ctx = _contextFactory.CreateContext(enableTracking: true);
|
||||||
|
// select the rating history for client
|
||||||
|
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
|
||||||
|
.Include(h => h.Ratings)
|
||||||
|
where history.ClientId == client.ClientId
|
||||||
|
select history;
|
||||||
|
|
||||||
|
// get the client ratings
|
||||||
|
var clientHistory = await iqHistoryLink
|
||||||
|
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
|
||||||
|
{
|
||||||
|
Active = true,
|
||||||
|
ClientId = client.ClientId,
|
||||||
|
Ratings = new List<EFRating>()
|
||||||
|
};
|
||||||
|
|
||||||
|
// it's the first time they've played
|
||||||
|
if (clientHistory.RatingHistoryId == 0)
|
||||||
{
|
{
|
||||||
// select the rating history for client
|
ctx.Add(clientHistory);
|
||||||
var iqHistoryLink = from history in ctx.Set<EFClientRatingHistory>()
|
|
||||||
.Include(h => h.Ratings)
|
|
||||||
where history.ClientId == client.ClientId
|
|
||||||
select history;
|
|
||||||
|
|
||||||
// get the client ratings
|
|
||||||
var clientHistory = await iqHistoryLink
|
|
||||||
.FirstOrDefaultAsync() ?? new EFClientRatingHistory()
|
|
||||||
{
|
|
||||||
Active = true,
|
|
||||||
ClientId = client.ClientId,
|
|
||||||
Ratings = new List<EFRating>()
|
|
||||||
};
|
|
||||||
|
|
||||||
// it's the first time they've played
|
|
||||||
if (clientHistory.RatingHistoryId == 0)
|
|
||||||
{
|
|
||||||
ctx.Add(clientHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region INDIVIDUAL_SERVER_PERFORMANCE
|
|
||||||
// get the client ranking for the current server
|
|
||||||
int individualClientRanking = await ctx.Set<EFRating>()
|
|
||||||
.Where(GetRankingFunc(clientStats.ServerId))
|
|
||||||
// ignore themselves in the query
|
|
||||||
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
|
||||||
.Where(c => c.Performance > clientStats.Performance)
|
|
||||||
.CountAsync() + 1;
|
|
||||||
|
|
||||||
// limit max history per server to 40
|
|
||||||
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
|
|
||||||
{
|
|
||||||
// select the oldest one
|
|
||||||
var ratingToRemove = clientHistory.Ratings
|
|
||||||
.Where(r => r.ServerId == clientStats.ServerId)
|
|
||||||
.OrderBy(r => r.When)
|
|
||||||
.First();
|
|
||||||
|
|
||||||
ctx.Remove(ratingToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the previous newest to false
|
|
||||||
var ratingToUnsetNewest = clientHistory.Ratings
|
|
||||||
.Where(r => r.ServerId == clientStats.ServerId)
|
|
||||||
.OrderByDescending(r => r.When)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (ratingToUnsetNewest != null)
|
|
||||||
{
|
|
||||||
if (ratingToUnsetNewest.Newest)
|
|
||||||
{
|
|
||||||
ctx.Update(ratingToUnsetNewest);
|
|
||||||
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
|
|
||||||
ratingToUnsetNewest.Newest = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newServerRating = new EFRating()
|
|
||||||
{
|
|
||||||
Performance = clientStats.Performance,
|
|
||||||
Ranking = individualClientRanking,
|
|
||||||
Active = true,
|
|
||||||
Newest = true,
|
|
||||||
ServerId = clientStats.ServerId,
|
|
||||||
RatingHistory = clientHistory,
|
|
||||||
ActivityAmount = currentServerTotalPlaytime,
|
|
||||||
};
|
|
||||||
|
|
||||||
// add new rating for current server
|
|
||||||
ctx.Add(newServerRating);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
#region OVERALL_RATING
|
|
||||||
// select all performance & time played for current client
|
|
||||||
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
|
|
||||||
where stats.ClientId == client.ClientId
|
|
||||||
where stats.ServerId != clientStats.ServerId
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
stats.Performance,
|
|
||||||
stats.TimePlayed
|
|
||||||
};
|
|
||||||
|
|
||||||
var clientStatsList = await iqClientStats.ToListAsync();
|
|
||||||
|
|
||||||
// add the current server's so we don't have to pull it from the database
|
|
||||||
clientStatsList.Add(new
|
|
||||||
{
|
|
||||||
clientStats.Performance,
|
|
||||||
TimePlayed = currentServerTotalPlaytime
|
|
||||||
});
|
|
||||||
|
|
||||||
// weight the overall performance based on play time
|
|
||||||
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
|
|
||||||
|
|
||||||
// shouldn't happen but just in case the sum of time played is 0
|
|
||||||
if (double.IsNaN(performanceAverage))
|
|
||||||
{
|
|
||||||
performanceAverage = clientStatsList.Average(p => p.Performance);
|
|
||||||
}
|
|
||||||
|
|
||||||
int overallClientRanking = await ctx.Set<EFRating>()
|
|
||||||
.Where(GetRankingFunc())
|
|
||||||
.Where(r => r.RatingHistory.ClientId != client.ClientId)
|
|
||||||
.Where(r => r.Performance > performanceAverage)
|
|
||||||
.CountAsync() + 1;
|
|
||||||
|
|
||||||
// limit max average history to 40
|
|
||||||
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
|
|
||||||
{
|
|
||||||
var ratingToRemove = clientHistory.Ratings
|
|
||||||
.Where(r => r.ServerId == null)
|
|
||||||
.OrderBy(r => r.When)
|
|
||||||
.First();
|
|
||||||
|
|
||||||
ctx.Remove(ratingToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the previous average newest to false
|
|
||||||
ratingToUnsetNewest = clientHistory.Ratings
|
|
||||||
.Where(r => r.ServerId == null)
|
|
||||||
.OrderByDescending(r => r.When)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
if (ratingToUnsetNewest != null)
|
|
||||||
{
|
|
||||||
if (ratingToUnsetNewest.Newest)
|
|
||||||
{
|
|
||||||
ctx.Update(ratingToUnsetNewest);
|
|
||||||
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
|
|
||||||
ratingToUnsetNewest.Newest = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new average rating
|
|
||||||
var averageRating = new EFRating()
|
|
||||||
{
|
|
||||||
Active = true,
|
|
||||||
Newest = true,
|
|
||||||
Performance = performanceAverage,
|
|
||||||
Ranking = overallClientRanking,
|
|
||||||
ServerId = null,
|
|
||||||
RatingHistory = clientHistory,
|
|
||||||
ActivityAmount = clientStatsList.Sum(s => s.TimePlayed)
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.Add(averageRating);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region INDIVIDUAL_SERVER_PERFORMANCE
|
||||||
|
// get the client ranking for the current server
|
||||||
|
int individualClientRanking = await ctx.Set<EFRating>()
|
||||||
|
.Where(GetRankingFunc(clientStats.ServerId))
|
||||||
|
// ignore themselves in the query
|
||||||
|
.Where(c => c.RatingHistory.ClientId != client.ClientId)
|
||||||
|
.Where(c => c.Performance > clientStats.Performance)
|
||||||
|
.CountAsync() + 1;
|
||||||
|
|
||||||
|
// limit max history per server to 40
|
||||||
|
if (clientHistory.Ratings.Count(r => r.ServerId == clientStats.ServerId) >= 40)
|
||||||
|
{
|
||||||
|
// select the oldest one
|
||||||
|
var ratingToRemove = clientHistory.Ratings
|
||||||
|
.Where(r => r.ServerId == clientStats.ServerId)
|
||||||
|
.OrderBy(r => r.When)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
ctx.Remove(ratingToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the previous newest to false
|
||||||
|
var ratingToUnsetNewest = clientHistory.Ratings
|
||||||
|
.Where(r => r.ServerId == clientStats.ServerId)
|
||||||
|
.OrderByDescending(r => r.When)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (ratingToUnsetNewest != null)
|
||||||
|
{
|
||||||
|
if (ratingToUnsetNewest.Newest)
|
||||||
|
{
|
||||||
|
ctx.Update(ratingToUnsetNewest);
|
||||||
|
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
|
||||||
|
ratingToUnsetNewest.Newest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newServerRating = new EFRating()
|
||||||
|
{
|
||||||
|
Performance = clientStats.Performance,
|
||||||
|
Ranking = individualClientRanking,
|
||||||
|
Active = true,
|
||||||
|
Newest = true,
|
||||||
|
ServerId = clientStats.ServerId,
|
||||||
|
RatingHistory = clientHistory,
|
||||||
|
ActivityAmount = currentServerTotalPlaytime,
|
||||||
|
};
|
||||||
|
|
||||||
|
// add new rating for current server
|
||||||
|
ctx.Add(newServerRating);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
#region OVERALL_RATING
|
||||||
|
// select all performance & time played for current client
|
||||||
|
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
|
||||||
|
where stats.ClientId == client.ClientId
|
||||||
|
where stats.ServerId != clientStats.ServerId
|
||||||
|
select new
|
||||||
|
{
|
||||||
|
stats.Performance,
|
||||||
|
stats.TimePlayed
|
||||||
|
};
|
||||||
|
|
||||||
|
var clientStatsList = await iqClientStats.ToListAsync();
|
||||||
|
|
||||||
|
// add the current server's so we don't have to pull it from the database
|
||||||
|
clientStatsList.Add(new
|
||||||
|
{
|
||||||
|
clientStats.Performance,
|
||||||
|
TimePlayed = currentServerTotalPlaytime
|
||||||
|
});
|
||||||
|
|
||||||
|
// weight the overall performance based on play time
|
||||||
|
double performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
|
||||||
|
|
||||||
|
// shouldn't happen but just in case the sum of time played is 0
|
||||||
|
if (double.IsNaN(performanceAverage))
|
||||||
|
{
|
||||||
|
performanceAverage = clientStatsList.Average(p => p.Performance);
|
||||||
|
}
|
||||||
|
|
||||||
|
int overallClientRanking = await ctx.Set<EFRating>()
|
||||||
|
.Where(GetRankingFunc())
|
||||||
|
.Where(r => r.RatingHistory.ClientId != client.ClientId)
|
||||||
|
.Where(r => r.Performance > performanceAverage)
|
||||||
|
.CountAsync() + 1;
|
||||||
|
|
||||||
|
// limit max average history to 40
|
||||||
|
if (clientHistory.Ratings.Count(r => r.ServerId == null) >= 40)
|
||||||
|
{
|
||||||
|
var ratingToRemove = clientHistory.Ratings
|
||||||
|
.Where(r => r.ServerId == null)
|
||||||
|
.OrderBy(r => r.When)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
ctx.Remove(ratingToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the previous average newest to false
|
||||||
|
ratingToUnsetNewest = clientHistory.Ratings
|
||||||
|
.Where(r => r.ServerId == null)
|
||||||
|
.OrderByDescending(r => r.When)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (ratingToUnsetNewest != null)
|
||||||
|
{
|
||||||
|
if (ratingToUnsetNewest.Newest)
|
||||||
|
{
|
||||||
|
ctx.Update(ratingToUnsetNewest);
|
||||||
|
ctx.Entry(ratingToUnsetNewest).Property(r => r.Newest).IsModified = true;
|
||||||
|
ratingToUnsetNewest.Newest = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new average rating
|
||||||
|
var averageRating = new EFRating()
|
||||||
|
{
|
||||||
|
Active = true,
|
||||||
|
Newest = true,
|
||||||
|
Performance = performanceAverage,
|
||||||
|
Ranking = overallClientRanking,
|
||||||
|
ServerId = null,
|
||||||
|
RatingHistory = clientHistory,
|
||||||
|
ActivityAmount = clientStatsList.Sum(s => s.TimePlayed)
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.Add(averageRating);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1137,25 +1122,23 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
EFServerStatistics serverStats;
|
EFServerStatistics serverStats;
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
||||||
|
serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId);
|
||||||
|
|
||||||
|
if (serverStats == null)
|
||||||
{
|
{
|
||||||
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
_log.LogDebug("Initializing server stats for {serverId}", serverId);
|
||||||
serverStats = serverStatsSet.FirstOrDefault(s => s.ServerId == serverId);
|
// server stats have never been generated before
|
||||||
|
serverStats = new EFServerStatistics()
|
||||||
if (serverStats == null)
|
|
||||||
{
|
{
|
||||||
_log.LogDebug("Initializing server stats for {serverId}", serverId);
|
ServerId = serverId,
|
||||||
// server stats have never been generated before
|
TotalKills = 0,
|
||||||
serverStats = new EFServerStatistics()
|
TotalPlayTime = 0,
|
||||||
{
|
};
|
||||||
ServerId = serverId,
|
|
||||||
TotalKills = 0,
|
|
||||||
TotalPlayTime = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
serverStats = serverStatsSet.Add(serverStats).Entity;
|
serverStats = serverStatsSet.Add(serverStats).Entity;
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverStats;
|
return serverStats;
|
||||||
@ -1216,12 +1199,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
|||||||
{
|
{
|
||||||
await waiter.WaitAsync();
|
await waiter.WaitAsync();
|
||||||
|
|
||||||
using (var ctx = _contextFactory.CreateContext())
|
await using var ctx = _contextFactory.CreateContext();
|
||||||
{
|
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
||||||
var serverStatsSet = ctx.Set<EFServerStatistics>();
|
serverStatsSet.Update(_servers[serverId].ServerStatistics);
|
||||||
serverStatsSet.Update(_servers[serverId].ServerStatistics);
|
await ctx.SaveChangesAsync();
|
||||||
await ctx.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var stats in sv.GetClientsAsList()
|
foreach (var stats in sv.GetClientsAsList()
|
||||||
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
|
||||||
|
@ -26,7 +26,7 @@ namespace Stats.Helpers
|
|||||||
public async Task<ResourceQueryHelperResult<StatsInfoResult>> QueryResource(StatsInfoRequest query)
|
public async Task<ResourceQueryHelperResult<StatsInfoResult>> QueryResource(StatsInfoRequest query)
|
||||||
{
|
{
|
||||||
var result = new ResourceQueryHelperResult<StatsInfoResult>();
|
var result = new ResourceQueryHelperResult<StatsInfoResult>();
|
||||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
// we need to get the ratings separately because there's not explicit FK
|
// we need to get the ratings separately because there's not explicit FK
|
||||||
var ratings = await context.Set<EFClientRatingHistory>()
|
var ratings = await context.Set<EFClientRatingHistory>()
|
||||||
|
@ -173,11 +173,9 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
{
|
{
|
||||||
IList<EFClientStatistics> clientStats;
|
IList<EFClientStatistics> clientStats;
|
||||||
int messageCount = 0;
|
int messageCount = 0;
|
||||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
|
||||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
|
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
|
||||||
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
|
|
||||||
}
|
|
||||||
|
|
||||||
int kills = clientStats.Sum(c => c.Kills);
|
int kills = clientStats.Sum(c => c.Kills);
|
||||||
int deaths = clientStats.Sum(c => c.Deaths);
|
int deaths = clientStats.Sum(c => c.Deaths);
|
||||||
@ -252,13 +250,11 @@ namespace IW4MAdmin.Plugins.Stats
|
|||||||
{
|
{
|
||||||
IList<EFClientStatistics> clientStats;
|
IList<EFClientStatistics> clientStats;
|
||||||
|
|
||||||
using (var ctx = _databaseContextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
clientStats = await ctx.Set<EFClientStatistics>()
|
||||||
clientStats = await ctx.Set<EFClientStatistics>()
|
.Include(c => c.HitLocations)
|
||||||
.Include(c => c.HitLocations)
|
.Where(c => c.ClientId == request.ClientId)
|
||||||
.Where(c => c.ClientId == request.ClientId)
|
.ToListAsync();
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
double headRatio = 0;
|
double headRatio = 0;
|
||||||
double chestRatio = 0;
|
double chestRatio = 0;
|
||||||
|
@ -41,7 +41,7 @@ namespace StatsWeb
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result = new ResourceQueryHelperResult<MessageResponse>();
|
var result = new ResourceQueryHelperResult<MessageResponse>();
|
||||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||||
|
|
||||||
if (serverCache == null)
|
if (serverCache == null)
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database
|
|||||||
{
|
{
|
||||||
public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
|
public static async Task Seed(IDatabaseContextFactory contextFactory, CancellationToken token)
|
||||||
{
|
{
|
||||||
var context = contextFactory.CreateContext();
|
await using var context = contextFactory.CreateContext();
|
||||||
var strategy = context.Database.CreateExecutionStrategy();
|
var strategy = context.Database.CreateExecutionStrategy();
|
||||||
await strategy.ExecuteAsync(async () =>
|
await strategy.ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> options) : base(options)
|
public MySqlDatabaseContext(DbContextOptions options) : base(options)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PostgresqlDatabaseContext(DbContextOptions<PostgresqlDatabaseContext> options) : base(options)
|
public PostgresqlDatabaseContext(DbContextOptions options) : base(options)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ namespace SharedLibraryCore.Database.MigrationContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> options) : base(options)
|
public SqliteDatabaseContext(DbContextOptions options) : base(options)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -91,12 +91,11 @@ namespace SharedLibraryCore.Database.Models
|
|||||||
SetAdditionalProperty("_reportCount", 0);
|
SetAdditionalProperty("_reportCount", 0);
|
||||||
ReceivedPenalties = new List<EFPenalty>();
|
ReceivedPenalties = new List<EFPenalty>();
|
||||||
_processingEvent = new SemaphoreSlim(1, 1);
|
_processingEvent = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~EFClient()
|
~EFClient()
|
||||||
{
|
{
|
||||||
_processingEvent.Dispose();
|
_processingEvent?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
@ -22,34 +22,32 @@ namespace SharedLibraryCore.Repositories
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo)
|
public async Task<IList<AuditInfo>> ListAuditInformation(PaginationRequest paginationInfo)
|
||||||
{
|
{
|
||||||
using (var ctx = _contextFactory.CreateContext(enableTracking: false))
|
await using var ctx = _contextFactory.CreateContext(enableTracking: false);
|
||||||
{
|
var iqItems = (from change in ctx.EFChangeHistory
|
||||||
var iqItems = (from change in ctx.EFChangeHistory
|
where change.TypeOfChange != Database.Models.EFChangeHistory.ChangeType.Ban
|
||||||
where change.TypeOfChange != Database.Models.EFChangeHistory.ChangeType.Ban
|
orderby change.TimeChanged descending
|
||||||
orderby change.TimeChanged descending
|
join originClient in ctx.Clients
|
||||||
join originClient in ctx.Clients
|
on (change.ImpersonationEntityId ?? change.OriginEntityId) equals originClient.ClientId
|
||||||
on (change.ImpersonationEntityId ?? change.OriginEntityId) equals originClient.ClientId
|
join targetClient in ctx.Clients
|
||||||
join targetClient in ctx.Clients
|
on change.TargetEntityId equals targetClient.ClientId
|
||||||
on change.TargetEntityId equals targetClient.ClientId
|
into targetChange
|
||||||
into targetChange
|
from targetClient in targetChange.DefaultIfEmpty()
|
||||||
from targetClient in targetChange.DefaultIfEmpty()
|
select new AuditInfo()
|
||||||
select new AuditInfo()
|
{
|
||||||
{
|
Action = change.TypeOfChange.ToString(),
|
||||||
Action = change.TypeOfChange.ToString(),
|
OriginName = originClient.CurrentAlias.Name,
|
||||||
OriginName = originClient.CurrentAlias.Name,
|
OriginId = originClient.ClientId,
|
||||||
OriginId = originClient.ClientId,
|
TargetName = targetClient == null ? "" : targetClient.CurrentAlias.Name,
|
||||||
TargetName = targetClient == null ? "" : targetClient.CurrentAlias.Name,
|
TargetId = targetClient == null ? new int?() : targetClient.ClientId,
|
||||||
TargetId = targetClient == null ? new int?() : targetClient.ClientId,
|
When = change.TimeChanged,
|
||||||
When = change.TimeChanged,
|
Data = change.Comment,
|
||||||
Data = change.Comment,
|
OldValue = change.PreviousValue,
|
||||||
OldValue = change.PreviousValue,
|
NewValue = change.CurrentValue
|
||||||
NewValue = change.CurrentValue
|
})
|
||||||
})
|
.Skip(paginationInfo.Offset)
|
||||||
.Skip(paginationInfo.Offset)
|
.Take(paginationInfo.Count);
|
||||||
.Take(paginationInfo.Count);
|
|
||||||
|
|
||||||
return await iqItems.ToListAsync();
|
return await iqItems.ToListAsync();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user