如何在领域驱动设计中跟踪最后修改对象的用户?

5

我正在尝试实施领域驱动设计(DDD),感觉已经有所了解,但是我也遇到了一些问题。

在我的90%领域对象中,我想知道最后一个更改它的用户。我不需要完整的审计跟踪 - 对于我的需求来说,这是过度的。

所有我的类都实现了包含以下内容的抽象基类:

    public abstract class Base
    {
       public User LastChangedBy { get; set; }
       public DateTime LastChangedDate { get; set; }
    }

选项1:遵循DDD原则,但不够优雅

永远不要让对象进入无效状态。

public abstract class Base
{
   public User LastChangedBy { get; protected set; }
   public DateTime LastChangedDate { get; protected set; }
}

public SomeObject
{
   .....
   SomeBehaviorThatChangesObject(User changedBy, ...)
   AnotherBehaviorThatChangesObject(User changedBy, ...)
}

我需要将所有的setter设置为private,基类的setter设置为protected。每次修改对象都必须通过一个带有(User changedBy)参数的方法完成。

虽然非常安全,但由于用户可能会对对象进行六次更改,因此我必须为每个更改提供User对象。实际上,在我的领域模型中,几乎每个方法都需要提供这个对象...

选项2:

引入一个bool IsValid字段。在所有的setter中,我将IsValid设置为false。创建一个AcceptChanges(User changesAcceptedBy)方法来将这个字段设置为true。记得在存储对象之前始终检查对象是否有效。

public abstract class Base
    {
       public bool IsValid {get; protected set;}
       public User LastChangedBy { get; protected set; }
       public DateTime LastChangedDate { get; protected set; }
    }

public SomeObject
{
   public Object Propery{get; set{IsValid = false; ...}}
   .....
   SomeBehaviorThatChangesObject(...)
   {
     //change the object
     IsValid = false;
   }
}

选项3: 在我看来最实用但不是非常DDD

可以在UnitOfWork中的持久层或者像repository.SaveChanges(User changedBy)这样的仓库中完成。但是,我或者其他实现这部分的人可能会忘记这一点,导致对象处于无效状态...

public SomeRepository
    {
       public void Update (User changedBy)
       {...}
    }

“能够看到最后更改实体的人是很正常的,但我还没有看到任何好的例子来实现DDD。你怎么做到的?
更新 回应jgauffins的解决方案: 谢谢,Base确实实现了一个接口,并且我简化了很多内容,以免过多地负担信息。我感觉这不太优雅,因为每个方法都需要我的用户对象作为每次更改的参数,而且我突然间必须将所有的setter设置为私有的……
将它放在repo中可能会起到作用,但上面的论点是正确的,我没有想到这一点,所以这就是我问的原因。我实际上正在计划一个可能需要更改一些对象的服务。
这是一个很大的变化,改变了数百个方法和属性,所以我想要确定。而且我最终将得到相当多的方法来设置一个字段,就像我在这篇文章中提到的那样,我不需要一个方法,因为“set”已经足够描述用户知道改变的内容……然而我的直觉告诉我你是对的,只是想看看是否有其他的方式来做同样的事情。”
“最终更新:”
“阅读所有的评论、问题和建议解决方案,我意识到我可能需要重新思考我如何看待LastChangedBy和LastChangedDate字段。对于某些对象,这实际上与我认为属于域的某些东西有关。对于许多其他对象,我真正寻找的是审计。我没有意识到这种差异。”
“例如,文档对象可以被除创建者以外的人更改,这将与某些行为相关联(通知创建者等)。在这些情况下,我感兴趣的不是最后一个更改文档对象的人,而是最后一个编辑文档内容的人。”
“与其将此与LastChangedBy混合在一起,不如为LastEditedBy创建字段。如果需要跟踪每次更改,这甚至可以是编辑器列表。”
“当然,大部分时间这将是相同的用户作为LastChangedBy,但看看这种情况:创建者进去并更改属性以锁定文档以防止进一步编辑。那么文档就没有被真正编辑,但是我希望出于审计原因跟踪它。如果我用相同的字段来跟踪编辑,那么这样做是错误的。我可以这样做,因为现在的要求是创建者可以进去看到谁最后一次更改了文档,但这不是正确的解决方案。”
“为了实现审计,我做了以下几件事:”
“首先,我将我的基类分成需要实现的接口。LastChangedBy和LastChangedDate在IAuditable接口中定义,需要进行审计的对象必须实现该接口。”
“然后像这样覆盖DbContext.SaveChanges():”
        public override int SaveChanges()
    {
        if(ChangeTracker.Entries<IAuditable>().Any())
            throw new InvalidOperationException
                (
                "Tried to save changes on an object that is needs to be Audited. Please provide the User that makes the changes!"
                );
        return base.SaveChanges();
    }

    public int SaveChanges(User changedBy)
    {
        var entries = ChangeTracker.Entries<IAuditable>().Where(entry=>entry.State == EntityState.Modified);
        foreach (var dbEntityEntry in entries)
        {
            dbEntityEntry.Entity.Audit(DateTime.Now, changedBy);
        }
        return base.SaveChanges();
    }

当然,还有其他方法可以做到这一点,但这至少是一个开始。

现在,我意识到我的问题可能可以更好地表达,但是老实说,我没有意识到问题的一部分是我想要在不同对象上由于不同原因跟踪更改。上面文档的示例仅是更多对象中的一个,其中LastChangedBy并不是我需要在我的领域中设置或跟踪的真正内容,但可以按照上面的方式重写以变得更加具体。


您将使用跟踪的数据做什么? - Yves Reynhout
@YvesReynhout 嗯,实际上有不同的原因。对于某些对象,仅是为了跟踪最后一次更改的人(谁停用了一个对象,更改了有关某个人的信息)- 真正是出于审计目的。其中一些是为了共享知识的文档,可以有多个编辑器或可以随时间进行更改的人(主题)。文档的创建者可能会在其他人更改它时收到通知,并且还需要查看谁更改了它。我倾向于后者是领域问题,前者是审计问题... - cfs
2个回答

4
我从Thread.CurrentPrincipal开始。但是,如果您要切换到异步处理(例如使用我在这里描述的命令),则无法使用Thread.CurrentPrincipal
但首先存在一个基本问题:
public abstract class Base
{
   public User LastChangedBy { get; protected set; }
   public DateTime LastChangedDate { get; protected set; }
}

那不是一个基类。它并没有添加任何功能。您应该将其更改为名为ITrackChanges或类似的接口。

public SomeObject
{
   .....
   SomeBehaviorThatChangesObject(User changedBy, ...)
   AnotherBehaviorThatChangesObject(User changedBy, ...)
}

这就是我的做法。你为什么认为它不够优雅呢?使用这种方法,管理员或后台服务可以代表用户执行任务。
更新:
我觉得这不够优雅,因为每个方法都需要我的用户对象作为参数来进行更改。
好吧,这是业务要求,对吧?你必须更改 LastChangedBy 属性。因此必须提供它。
公共 setter 在 DDD 中并不被禁止使用。你可以使用它们,但在这种情况下,每次更改都必须更新 LastChangedBy 和 LastChangedDate。而且这种逻辑应该由模型本身强制执行,而不是由调用代码强制执行。因此,无法使用公共 setter。

+1 只是为了_"管理员或后台服务可以代表用户执行操作"_. 我在旧应用程序中也遇到了这个问题,但解决方案的架构方式并不十分优雅。 - Luiz Damim
我不得不将这个回答设置为被接受的,因为它确实让我朝着正确的方向找到了一个令我满意的解决方案,尽管我在基础设施层实现了审计,但我觉得我遵循了这个答案背后的主要思想。 - cfs

2

...或者,您可以将用户设置为 Thread.CurrentPrincipal,并且不将其作为任何接口的一部分。

这对于任何值得拥有的身份模型都有效,无论是在具有自定义或AD身份验证的网站还是桌面应用程序中。

要扩展功能,如果未设置用户,则您的存储库等可以抛出异常,并且您仍然可以实现基于角色或声明的权限,以根据用户身份防止/允许特定类型的更新,如果您希望如此。


我怎么才能获取到Guid UserId呢?我已经检查过了,但是我只能获取到用户的名称... - cfs
1
你可以创建一个 IIdentity 接口的实现,其中你可以存储关于用户的任何信息。我倾向于尽可能地存储丰富的身份对象,包括显示名称等,以及原始用户 ID。完成后,你可以将其设置为新的 GenericPrincipal 实例,如我所链接主题中的示例所示;然后再将其设置为 CurrenctPrincipal。你在哪里处理这个对象完全取决于你。也许让仓库来处理它是有意义的 - 因为它们可能是“Base”感知的? - Andras Zoltan
嗨,谢谢。是的,我有一个通用存储库,可以接受所有实现Base的对象,所以应该可以正常工作。我需要再仔细研究一下,但看起来这是一个不错的选择,因为它内置于框架中,很可能我不会更改它,所以使用应该是安全的 :) - cfs
我也更喜欢这个解决方案,因为它尽可能地将审计信息移入基础设施中。虽然审计信息可能是业务需求,但它不是您领域模型的核心功能,因此每个行为都传递用户实例的观念并不太优雅,这一点是相当适用的。 - eulerfx
1
我向您提及我曾经关于仓储模式所提出的问题,因为我在一个细节上搞混了自己 - 我接受的答案中的引用“该模式是为了服务于你,而不是相反”的话在这里浮现在我的脑海中 :) - Andras Zoltan
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接