【问题标题】:How to keep track of the last user that made changes to an object in DDD?如何跟踪最后一个对 DDD 中的对象进行更改的用户?
【发布时间】:2012-10-23 22:34:30
【问题描述】:

我正在尝试实现 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, ...)
}

我必须将我所有的设置器设为私有,将基类设置器设为受保护。对对象所做的每一次更改都需要通过一个将 (User changedBy) 作为参数的方法来完成。

非常安全,但是由于用户可能会对对象进行 6 次更改,因此我必须为每个更改提供 User 对象。好吧,实际上,我必须为我的领域模型中的几乎所有方法提供这个......

选项 2:不是最好的,但我读过它

引入一个 bool IsValid 字段。在所有设置器中,我将 IsValid 设置为 false。创建了一个将此字段设置为 true 的方法 AcceptChanges(User changesAcceptedBy)。请记住在持久化对象之前始终检查对象是否有效。

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 中可以完成这项工作,但上面的论点是有效的,我没有想到这一点,所以这正是我问的原因。我实际上正在计划一项可能需要更改某些对象的服务。

这是一个很大的改变,改变了数百个方法和属性,所以我想保持舒尔。而且我最终会用很多方法来设置一个字段,就像我在this post 中提到的那样,我不需要一个方法,因为“set”描述得足以让用户知道更改做了什么......我的直觉但是感觉说你是对的,只是想看看是否有其他方法可以做同样的事情。

最终更新:

阅读所有 cmets、问题和建议的解决方案后,我意识到我可能不得不重新考虑如何看待 LastChangedBy 和 LastChangedDate 字段。对于某些对象,这实际上与我认为属于该领域的事物有关。对于许多其他人来说,我正在寻找真正的审计。我没有意识到区别。

即Document 对象可以由创建者以外的其他人更改,并且会与某些行为(通知创建者等)相关联。在这些情况下,实际上并不是最后一个更改我感兴趣的文档对象的人,而是最后一个编辑文档内容的人。

我没有将它与 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 不是我需要在我的域中设置或跟踪的对象之一真正,但它可以被重写为像上面那样更特别。

【问题讨论】:

  • 您会将跟踪数据用于什么目的?
  • @YvesReynhout 好吧,确实有不同的原因。在某些对象上,它只是为了跟踪谁进行了最后一次更改(谁停用了对象,更改了有关人的信息)——实际上是出于审计原因。其中一些是用于共享知识的文档,其中可以有多个编辑者,或者可以随着时间的推移对文档(主题)进行更改的人。当其他人更改文档时,可能会通知文档的创建者,并且还需要查看谁更改了它。我倾向于后者是一个领域问题,而前者是一个审计问题......

标签: c# design-patterns domain-driven-design n-tier-architecture


【解决方案1】:

我从Thread.CurrentPrincipal 开始。但是,如果您稍后要切换到异步处理(例如使用我描述的 here 之类的命令),则无法使用 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 属性。因此必须提供。

DDD 中不禁止公共 setter。您可以使用它们,但在这种情况下,LastChangedByLastChangedDate 必须针对每次更改进行更新。并且该逻辑应该由模型本身而不是调用代码来执行。因此无法使用公共设置器。

【讨论】:

  • +1 仅用于 “管理员或后台服务可以使用该方法代表用户执行操作。”。我在一个较旧的应用程序中遇到了这个问题,它的架构方式并没有变得非常优雅。
  • 我必须将此设置为可接受的答案,因为这确实让我找到了一个我满意的灵魂,即使我在基础设施层实施了审计,我觉得我遵循这个答案背后的主要思想。
【解决方案2】:

...或者您可以将用户设置为Thread.CurrentPrincipal,而不是使其成为任何界面的一部分。

这适用于任何值得称道的身份模型,无论是在具有自定义或 AD 身份验证的网站或桌面应用程序上。

为了扩展,如果未设置用户,您的存储库等可能会抛出,如果您愿意,您仍然可以实现基于角色或声明的权限,以防止/允许基于用户身份的特定类型的更新.

【讨论】:

  • 好的,如果我理解这一点的话。 Thread.CurrentPrincipal 将通过层工作。 IE。我可以在应用程序中的任何层调用它。你会把它放在哪里,你在基础设施层/DAL中提到存储库?
  • 我如何获得 Guid UserId?检查了一下,我只能得到用户的名字......
  • 您创建了IIdentity 接口的实现,您可以在其中存储您想要的有关用户的任何信息。我倾向于在原始用户 ID 旁边存储尽可能丰富的身份对象(包括显示名称等)。完成此操作后 - 您可以将其设置为新的 GenericPrincipal 实例,如我链接到的主题的示例所示;然后您将其设置为 CurrenctPrincipal。您实际处理此对象的位置完全取决于您。存储库(y|ies)这样做可能有意义 - 因为他们大概知道Base
  • 嗨,谢谢。是的,我有一个 GenericRepository,它接受所有实现 Base 的对象,所以应该可以正常工作。我将不得不对此进行更多研究,但它看起来是一个不错的选择,因为它内置在框架中,而且我不太可能更改它,所以它应该是安全的:)
  • 我推荐你to a question I asked about the repository pattern,因为我在一个特定的小细节上搞砸了——引用,从我接受的答案中,'模式是为你服务的,而不是反过来' 突然想到这里 :)
猜你喜欢
  • 1970-01-01
  • 2012-01-02
  • 1970-01-01
  • 2010-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多