当我遇到调用多个事件处理程序的事件但是将每个事件传递给同一个实体时,这个问题就出现了。现在,因为实体绑定到EF中的一个上下文,所以将上下文传递给事件处理程序违反了每单位工作原则的一个上下文。
所以我能想到的最好的是一种工厂和自定义代理(在此示例中仅适用于Player
,但我有更多EF实体,例如Human
,Participation
,等):
public class PlayerProxy : Player {
protected SnowbiteContext Context;
protected Player RealPlayer;
private long? _playerId;
private string _name;
public static Func<SnowbiteContext, PlayerProxy> NewProxyFactory(string name) =>
(ctx) => new PlayerProxy(ctx) {Name = name};
public static Func<SnowbiteContext, PlayerProxy> NewProxyFactory(long playerId) =>
(ctx) => new PlayerProxy(ctx) {PlayerId = playerId};
public PlayerProxy(SnowbiteContext ctx) {
Context = ctx ?? throw new ArgumentException();
}
public void Populate() {
if (RealPlayer != null) return;
if (_playerId != null) RealPlayer = Context.Players.Single(p => p.PlayerId == _playerId);
if (_name != null)
RealPlayer =
Context.Players.SingleOrDefault(p =>
p.Name == _name) ?? /* Contact external json API, complicated and expensive */;
}
public override long PlayerId {
get {
if (_playerId == null) Populate();
Debug.Assert(_playerId != null, nameof(_playerId) + " != null");
return _playerId.Value;
}
set {
Populate();
RealPlayer.PlayerId = value;
_playerId = null;
}
}
public override string Name {
get {
if (_name == null) Populate();
return _name;
}
set {
Populate();
RealPlayer.Name = value;
_name = null;
}
}
public override Human Human {
get {
Populate();
return RealPlayer.Human;
}
set {
Populate();
RealPlayer.Human = value;
}
}
public override long HumanId {
get {
Populate();
return RealPlayer.HumanId;
}
set {
Populate();
RealPlayer.HumanId = value;
}
}
public override ICollection<Participation> Participations {
get {
Populate();
return RealPlayer.Participations;
}
set {
Populate();
RealPlayer.Participations = value;
}
}
// Etc... for EaGuid, Tag, Rank.
}
然后事件会像:
private void RCON_event_OnChat(IReadOnlyList<string> words) {
var name = words[1];
if (name == "Server") return;
EvChat?.Invoke(PlayerProxy.NewProxyFactory(name), words[2]);
}
事件处理程序(玩家N:1人类):
void Handler(Func<SnowbiteContext, PlayerProxy> playerProxyFactory, string message) {
using (var ctx = new SnowbiteContext()) {
PlayerProxy playerProxy = playerProxyFactory(ctx);
// use stuff, unit of work being done here, etc.
// but how about relating my proxy to other entities? Will EF handle my derived type correctly?
Human humanFromER = ctx.Humans.Single(/* ... */);
humanFromER.Players.Add(playerProxy);
playerProxy.Human = humanFromER;
// how about 1:N with ICollection<Participation>? Will EF recognize it properly?
playerProxy.Participations.Add(/* some new participation */);
}
}
供参考,这是我的Player
实体:
public class Player {
public Player() {
Participations = new List<Participation>();
}
/// <summary>
/// BalzeId / PersonaId
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual long PlayerId { get; set; }
[Required]
public virtual long HumanId { get; set; }
public virtual Human Human { get; set; }
/// <summary>
/// Not a key. Mostly provided for convenicencet.
/// </summary>
[StringLength(35)]
public virtual string EaGuid { get; set; }
[Index]
[StringLength(60)]
public virtual string Name { get; set; }
[StringLength(4)]
public virtual string Tag { get; set; }
public virtual int Rank { get; set; }
public virtual ICollection<Participation> Participations { get; set; }
}
或者只是将string name
传递给事件处理程序,并且有一个简单的函数从DB获取一个玩家会更简单:D。
更新
根据Gert Arnold的评论,我想出了这个,完全摆脱了代理:
private void RCON_event_OnChat(IReadOnlyList<string> words) {
var name = words[1];
if (name == "Server") return;
if (EvChat != null) {
// additionally, will be easy to convert to async
foreach (var handler in EvChat.GetInvocationList().Cast<Action<SnowbiteContext, Player, string>>()) {
using (var ctx = new SnowbiteContext()) {
var player = ctx.Players.SingleOrDefault(p => p.Name == name) ?? /* expensive resolve */;
handler(ctx, player, words[2]);
}
}
}
}