ace/Source/ACE.Database/ShardDatabase.cs
Mag-nus 118c890285 More profiling based improvements (#1067)
* RateMonitor created

* NetworkSession Update foreach change to help profiling

This just removes the lambda pattern for the foreach so profilers can monitor each line individually.

Functionality is unchanged.

* Reduce Landblock ctor async work to a single thread

This helps reduce ACE thread starvation

* NetworkSession cosmetic

* NetworkSession cosmetic

* NetworkSession currentBundles ConcurrentDictionary to Array

This significantly improves the performance of NetworkSession.

Arrays are faster than Dictionaries, and we manage concurrency to the elements via currentBundleLocks.

We're also able to get away with using an array because the number of elements is only 12.

Accessing the indexes of currentBundles should be atomic as it's an array of reference values, compiled as 64 bit.

* Add InParallel to database functions that do their underlying work in parallel

* DoSessionWork tick outbound messages in series, not in parallel.

* NetworkSession should not be interfacing with ActionChains

* Don't load landblock ctor resources in parallel

Save the threads in the pool for more important work

* LandblockManager removed check/recheck

This is legacy code from a pattern no longer used. It is no longer required.

* Cosmetic
2018-10-15 23:06:54 -04:00

599 lines
26 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using log4net;
using ACE.Database.Entity;
using ACE.Database.Models.Shard;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
namespace ACE.Database
{
public class ShardDatabase
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public bool Exists(bool retryUntilFound)
{
var config = Common.ConfigManager.Config.MySql.World;
for (; ; )
{
using (var context = new ShardDbContext())
{
if (((RelationalDatabaseCreator)context.Database.GetService<IDatabaseCreator>()).Exists())
{
log.Debug($"Successfully connected to {config.Database} database on {config.Host}:{config.Port}.");
return true;
}
}
log.Error($"Attempting to reconnect to {config.Database} database on {config.Host}:{config.Port} in 5 seconds...");
if (retryUntilFound)
Thread.Sleep(5000);
else
return false;
}
}
/// <summary>
/// Will return uint.MaxValue if no records were found within the range provided.
/// </summary>
public uint GetMaxGuidFoundInRange(uint min, uint max)
{
using (var context = new ShardDbContext())
{
var results = context.Biota
.AsNoTracking()
.Where(r => r.Id >= min && r.Id <= max)
.ToList();
if (!results.Any())
return uint.MaxValue;
var maxId = min;
foreach (var result in results)
{
if (result.Id > maxId)
maxId = result.Id;
}
return maxId;
}
}
[Flags]
enum PopulatedCollectionFlags
{
BiotaPropertiesAnimPart = 0x1,
BiotaPropertiesAttribute = 0x2,
BiotaPropertiesAttribute2nd = 0x4,
BiotaPropertiesBodyPart = 0x8,
BiotaPropertiesBook = 0x10,
BiotaPropertiesBookPageData = 0x20,
BiotaPropertiesBool = 0x40,
BiotaPropertiesCreateList = 0x80,
BiotaPropertiesDID = 0x100,
BiotaPropertiesEmote = 0x200,
BiotaPropertiesEnchantmentRegistry = 0x400,
BiotaPropertiesEventFilter = 0x800,
BiotaPropertiesFloat = 0x1000,
BiotaPropertiesGenerator = 0x2000,
BiotaPropertiesIID = 0x4000,
BiotaPropertiesInt = 0x8000,
BiotaPropertiesInt64 = 0x10000,
BiotaPropertiesPalette = 0x20000,
BiotaPropertiesPosition = 0x40000,
BiotaPropertiesSkill = 0x80000,
BiotaPropertiesSpellBook = 0x100000,
BiotaPropertiesString = 0x200000,
BiotaPropertiesTextureMap = 0x400000
}
private static void SetBiotaPopulatedCollections(Biota biota)
{
PopulatedCollectionFlags populatedCollectionFlags = 0;
if (biota.BiotaPropertiesAnimPart != null && biota.BiotaPropertiesAnimPart.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesAnimPart;
if (biota.BiotaPropertiesAttribute != null && biota.BiotaPropertiesAttribute.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesAttribute;
if (biota.BiotaPropertiesAttribute2nd != null && biota.BiotaPropertiesAttribute2nd.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesAttribute2nd;
if (biota.BiotaPropertiesBodyPart != null && biota.BiotaPropertiesBodyPart.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesBodyPart;
if (biota.BiotaPropertiesBook != null) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesBook;
if (biota.BiotaPropertiesBookPageData != null && biota.BiotaPropertiesBookPageData.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesBookPageData;
if (biota.BiotaPropertiesBool != null && biota.BiotaPropertiesBool.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesBool;
if (biota.BiotaPropertiesCreateList != null && biota.BiotaPropertiesCreateList.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesCreateList;
if (biota.BiotaPropertiesDID != null && biota.BiotaPropertiesDID.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesDID;
if (biota.BiotaPropertiesEmote != null && biota.BiotaPropertiesEmote.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesEmote;
if (biota.BiotaPropertiesEnchantmentRegistry != null && biota.BiotaPropertiesEnchantmentRegistry.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesEnchantmentRegistry;
if (biota.BiotaPropertiesEventFilter != null && biota.BiotaPropertiesEventFilter.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesEventFilter;
if (biota.BiotaPropertiesFloat != null && biota.BiotaPropertiesFloat.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesFloat;
if (biota.BiotaPropertiesGenerator != null && biota.BiotaPropertiesGenerator.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesGenerator;
if (biota.BiotaPropertiesIID != null && biota.BiotaPropertiesIID.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesIID;
if (biota.BiotaPropertiesInt != null && biota.BiotaPropertiesInt.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesInt;
if (biota.BiotaPropertiesInt64 != null && biota.BiotaPropertiesInt64.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesInt64;
if (biota.BiotaPropertiesPalette != null && biota.BiotaPropertiesPalette.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesPalette;
if (biota.BiotaPropertiesPosition != null && biota.BiotaPropertiesPosition.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesPosition;
if (biota.BiotaPropertiesSkill != null && biota.BiotaPropertiesSkill.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesSkill;
if (biota.BiotaPropertiesSpellBook != null && biota.BiotaPropertiesSpellBook.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesSpellBook;
if (biota.BiotaPropertiesString != null && biota.BiotaPropertiesString.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesString;
if (biota.BiotaPropertiesTextureMap != null && biota.BiotaPropertiesTextureMap.Count > 0) populatedCollectionFlags |= PopulatedCollectionFlags.BiotaPropertiesTextureMap;
biota.PopulatedCollectionFlags = (uint)populatedCollectionFlags;
}
private static readonly ConditionalWeakTable<Biota, ShardDbContext> BiotaContexts = new ConditionalWeakTable<Biota, ShardDbContext>();
private static Biota GetBiota(ShardDbContext context, uint id)
{
var biota = context.Biota
.FirstOrDefault(r => r.Id == id);
if (biota == null)
return null;
PopulatedCollectionFlags populatedCollectionFlags = (PopulatedCollectionFlags)biota.PopulatedCollectionFlags;
// todo: There are gains to be had here if we can conditionally perform mulitple .Include (.Where) statements in a single query.
// todo: Until I figure out how to do that, this is still pretty good. Mag-nus 2018-08-10
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesAnimPart)) biota.BiotaPropertiesAnimPart = context.BiotaPropertiesAnimPart.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesAttribute)) biota.BiotaPropertiesAttribute = context.BiotaPropertiesAttribute.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesAttribute2nd)) biota.BiotaPropertiesAttribute2nd = context.BiotaPropertiesAttribute2nd.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesBodyPart)) biota.BiotaPropertiesBodyPart = context.BiotaPropertiesBodyPart.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesBook)) biota.BiotaPropertiesBook = context.BiotaPropertiesBook.FirstOrDefault(r => r.ObjectId == biota.Id);
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesBookPageData)) biota.BiotaPropertiesBookPageData = context.BiotaPropertiesBookPageData.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesBool)) biota.BiotaPropertiesBool = context.BiotaPropertiesBool.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesCreateList)) biota.BiotaPropertiesCreateList = context.BiotaPropertiesCreateList.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesDID)) biota.BiotaPropertiesDID = context.BiotaPropertiesDID.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesEmote)) biota.BiotaPropertiesEmote = context.BiotaPropertiesEmote.Include(r => r.BiotaPropertiesEmoteAction).Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesEnchantmentRegistry)) biota.BiotaPropertiesEnchantmentRegistry = context.BiotaPropertiesEnchantmentRegistry.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesEventFilter)) biota.BiotaPropertiesEventFilter = context.BiotaPropertiesEventFilter.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesFloat)) biota.BiotaPropertiesFloat = context.BiotaPropertiesFloat.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesGenerator)) biota.BiotaPropertiesGenerator = context.BiotaPropertiesGenerator.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesIID)) biota.BiotaPropertiesIID = context.BiotaPropertiesIID.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesInt)) biota.BiotaPropertiesInt = context.BiotaPropertiesInt.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesInt64)) biota.BiotaPropertiesInt64 = context.BiotaPropertiesInt64.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesPalette)) biota.BiotaPropertiesPalette = context.BiotaPropertiesPalette.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesPosition)) biota.BiotaPropertiesPosition = context.BiotaPropertiesPosition.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesSkill)) biota.BiotaPropertiesSkill = context.BiotaPropertiesSkill.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesSpellBook)) biota.BiotaPropertiesSpellBook = context.BiotaPropertiesSpellBook.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesString)) biota.BiotaPropertiesString = context.BiotaPropertiesString.Where(r => r.ObjectId == biota.Id).ToList();
if (populatedCollectionFlags.HasFlag(PopulatedCollectionFlags.BiotaPropertiesTextureMap)) biota.BiotaPropertiesTextureMap = context.BiotaPropertiesTextureMap.Where(r => r.ObjectId == biota.Id).ToList();
return biota;
}
public Biota GetBiota(uint id)
{
var context = new ShardDbContext();
var biota = GetBiota(context, id);
if (biota != null)
BiotaContexts.Add(biota, context);
return biota;
}
public bool SaveBiota(Biota biota, ReaderWriterLockSlim rwLock)
{
if (BiotaContexts.TryGetValue(biota, out var cachedContext))
{
rwLock.EnterWriteLock();
try
{
SetBiotaPopulatedCollections(biota);
try
{
cachedContext.SaveChanges();
return true;
}
catch (Exception ex)
{
// Character name might be in use or some other fault
log.Error($"SaveBiota failed with exception: {ex}");
return false;
}
}
finally
{
rwLock.ExitWriteLock();
}
}
var context = new ShardDbContext();
BiotaContexts.Add(biota, context);
rwLock.EnterWriteLock();
try
{
SetBiotaPopulatedCollections(biota);
context.Biota.Add(biota);
try
{
context.SaveChanges();
return true;
}
catch (Exception ex)
{
// Character name might be in use or some other fault
log.Error($"SaveBiota failed with exception: {ex}");
return false;
}
}
finally
{
rwLock.ExitWriteLock();
}
}
public bool SaveBiotasInParallel(IEnumerable<(Biota biota, ReaderWriterLockSlim rwLock)> biotas)
{
var result = true;
Parallel.ForEach(biotas, biota =>
{
if (!SaveBiota(biota.biota, biota.rwLock))
result = false;
});
return result;
}
public bool RemoveBiota(Biota biota, ReaderWriterLockSlim rwLock)
{
if (BiotaContexts.TryGetValue(biota, out var cachedContext))
{
rwLock.EnterWriteLock();
try
{
cachedContext.Biota.Remove(biota);
try
{
cachedContext.SaveChanges();
return true;
}
catch (Exception ex)
{
// Character name might be in use or some other fault
log.Error($"RemoveBiota failed with exception: {ex}");
return false;
}
}
finally
{
rwLock.ExitWriteLock();
}
}
// If we got here, the biota didn't come from the database through this class.
// Most likely, it doesn't exist in the database, so, no need to remove.
return true;
}
public bool RemoveBiotasInParallel(IEnumerable<(Biota biota, ReaderWriterLockSlim rwLock)> biotas)
{
var result = true;
Parallel.ForEach(biotas, biota =>
{
if (!RemoveBiota(biota.biota, biota.rwLock))
result = false;
});
return result;
}
public void FreeBiotaAndDisposeContext(Biota biota)
{
if (BiotaContexts.TryGetValue(biota, out var context))
{
BiotaContexts.Remove(biota);
context.Dispose();
}
}
public void FreeBiotaAndDisposeContexts(IEnumerable<Biota> biotas)
{
foreach (var biota in biotas)
FreeBiotaAndDisposeContext(biota);
}
public PlayerBiotas GetPlayerBiotasInParallel(uint id)
{
var biota = GetBiota(id);
var inventory = GetInventoryInParallel(id, true);
var wieldedItems = GetWieldedItemsInParallel(id);
return new PlayerBiotas(biota, inventory, wieldedItems);
}
public List<Biota> GetInventoryInParallel(uint parentId, bool includedNestedItems)
{
var inventory = new ConcurrentBag<Biota>();
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.BiotaPropertiesIID
.Where(r => r.Type == (ushort)PropertyInstanceId.Container && r.Value == parentId)
.ToList();
Parallel.ForEach(results, result =>
{
var biota = GetBiota(result.ObjectId);
if (biota != null)
{
inventory.Add(biota);
if (includedNestedItems && biota.WeenieType == (int)WeenieType.Container)
{
var subItems = GetInventoryInParallel(biota.Id, false);
foreach (var subItem in subItems)
inventory.Add(subItem);
}
}
});
}
return inventory.ToList();
}
public List<Biota> GetWieldedItemsInParallel(uint parentId)
{
var wieldedItems = new ConcurrentBag<Biota>();
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.BiotaPropertiesIID
.Where(r => r.Type == (ushort)PropertyInstanceId.Wielder && r.Value == parentId)
.ToList();
Parallel.ForEach(results, result =>
{
var biota = GetBiota(result.ObjectId);
if (biota != null)
wieldedItems.Add(biota);
});
}
return wieldedItems.ToList();
}
public List<Biota> GetDecayableObjectsByLandblock(ushort landblockId)
{
var decayables = new List<Biota>();
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.BiotaPropertiesPosition
.Where(p => p.ObjCellId >> 16 == landblockId)
.ToList();
foreach (var result in results)
{
var biota = GetBiota(context, result.ObjectId);
if (biota != null && biota.WeenieType == (int)WeenieType.Corpse)
decayables.Add(biota);
}
}
return decayables;
}
public List<Biota> GetDecayableObjectsByLandblockInParallel(ushort landblockId)
{
var decayables = new ConcurrentBag<Biota>();
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.BiotaPropertiesPosition
.Where(p => p.ObjCellId >> 16 == landblockId)
.ToList();
Parallel.ForEach(results, result =>
{
var biota = GetBiota(result.ObjectId);
if (biota != null && biota.WeenieType == (int)WeenieType.Corpse)
decayables.Add(biota);
});
}
return decayables.ToList();
}
public List<Biota> GetStaticObjectsByLandblock(ushort landblockId)
{
var staticObjects = new List<Biota>();
var staticLandblockId = 0x70000 | landblockId;
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.Biota.Where(b => b.Id >> 12 == staticLandblockId).ToList();
foreach (var result in results)
{
var biota = GetBiota(context, result.Id);
staticObjects.Add(biota);
}
}
return staticObjects;
}
public List<Biota> GetStaticObjectsByLandblockInParallel(ushort landblockId)
{
var staticObjects = new ConcurrentBag<Biota>();
var staticLandblockId = 0x70000 | landblockId;
using (var context = new ShardDbContext())
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var results = context.Biota.Where(b => b.Id >> 12 == staticLandblockId).ToList();
Parallel.ForEach(results, result =>
{
var biota = GetBiota(result.Id);
staticObjects.Add(biota);
});
}
return staticObjects.ToList();
}
public bool IsCharacterNameAvailable(string name)
{
using (var context = new ShardDbContext())
{
var result = context.Character
.AsNoTracking()
.Where(r => !r.IsDeleted)
.Where(r => !(r.DeleteTime > 0))
.FirstOrDefault(r => r.Name == name);
return result == null;
}
}
private static readonly ConditionalWeakTable<Character, ShardDbContext> CharacterContexts = new ConditionalWeakTable<Character, ShardDbContext>();
public List<Character> GetCharacters(uint accountId, bool includeDeleted)
{
var context = new ShardDbContext();
var results = context.Character
.Include(r => r.CharacterPropertiesContract)
.Include(r => r.CharacterPropertiesFillCompBook)
.Include(r => r.CharacterPropertiesFriendList)
.Include(r => r.CharacterPropertiesQuestRegistry)
.Include(r => r.CharacterPropertiesShortcutBar)
.Include(r => r.CharacterPropertiesSpellBar)
.Include(r => r.CharacterPropertiesTitleBook)
.Where(r => r.AccountId == accountId && (includeDeleted || !r.IsDeleted))
.ToList();
foreach (var result in results)
CharacterContexts.Add(result, context);
return results;
}
public bool SaveCharacter(Character character, ReaderWriterLockSlim rwLock)
{
if (CharacterContexts.TryGetValue(character, out var cachedContext))
{
rwLock.EnterWriteLock();
try
{
try
{
cachedContext.SaveChanges();
return true;
}
catch (Exception ex)
{
// Character name might be in use or some other fault
log.Error($"SaveCharacter failed with exception: {ex}");
return false;
}
}
finally
{
rwLock.ExitWriteLock();
}
}
var context = new ShardDbContext();
CharacterContexts.Add(character, context);
rwLock.EnterWriteLock();
try
{
context.Character.Add(character);
try
{
context.SaveChanges();
return true;
}
catch (Exception ex)
{
// Character name might be in use or some other fault
log.Error($"SaveCharacter failed with exception: {ex}");
return false;
}
}
finally
{
rwLock.ExitWriteLock();
}
}
public bool AddCharacterInParallel(Biota biota, ReaderWriterLockSlim biotaLock, IEnumerable<(Biota biota, ReaderWriterLockSlim rwLock)> possessions, Character character, ReaderWriterLockSlim characterLock)
{
if (!SaveBiota(biota, biotaLock))
return false; // Biota save failed which mean Character fails.
if (!SaveBiotasInParallel(possessions))
return false;
if (!SaveCharacter(character, characterLock))
return false;
return true;
}
public List<Character> GetAllCharacters()
{
using (var context = new ShardDbContext())
{
var results = context.Character
.AsNoTracking()
.ToList();
return results;
}
}
}
}