5

I'm trying to implement DDD and feel I get the hang of it, but I have some problems too.

In 90 % of my domain objects I want to know the last user that made changes to it. I do not need a full audit trail - that is overkill for my needs.

All my classes implement an abstract base class containing:

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

Option 1: Follows DDD principles, but not so elegant

Never let the object enter invalid state

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

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

I have to make all my setters private, the Base class setters as proteced. Every change made to objects need to be done through a method that takes (User changedBy) as a parameter.

Very safe, BUT since the user might do 6 changes to the object, i have to supply the User object for each one of them. Well, actually, I have to supply this for pretty much every method in my domain model...

Option 2: not the best one, but I've read about it

Introduce a bool IsValid field. In all setters I set the IsValid to false. Created a method AcceptChanges(User changesAcceptedBy) that sets this field to true. Remember to always check if object is valid before persisting it.

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;
   }
}

Option 3: I my eyes most practical but not very DDD

Do it in the persistance layer in UnitOfWork or in the repositories like repository.SaveChanges(User changedBy). I, or whoever implements this part, might forget that and it would leave the object in an invalid state...

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

It's quite normal to be able to see who changed an entity last, but I've not seen any good examples for it implementing DDD. How do you do this?

Update In response to jgauffins solution: Thanks, Base does actually implement an interface, and i simlpified very much to not overload with info. I feel it is not elegant becouse every method will need my user object as parameter for every change, and i suddenly have to make absoulutely all setters private...

Putting it in the repo could do the job, but the argument above is valid and I haven't thought of that so that's exactly why I asked. I'm actually planning a service that might need to change some objects.

It's a big change, changing hundreds of methods and properties so I want to be shure. And also I will end up with quite a few methods just to set one field like I mentioned in this post, where I dont need a method becouse "set" describes enough for the user to know what the change does... My gut feeling however says you're right, just wanted see if there was alterantive ways to do the same thing.

Final update:

Reading all the comments, questions and suggested solutions I realized that I probably had to rethink how I looked at the LastChangedBy and LastChangedDate fields. For some of the objects this actually relates to something that I feel belong to the domain. For many others it's really auditing I'm looking for. I didn't realize the difference.

I.e. the Document object can be changed by other than the creator and that would be assioiated with some behavior (notifying creator etc). In these cases it is not really the person that last changed the document object I'm interested in, but it is the last one who edited the content of the document.

Instead of mixing this with LastChangedBy, I create fields for LastEditedBy. This could even be a list of Editors if I need to track every change.

Of course much of the time this will be the same user as the LastChangedBy, but look at the scenario where the creator goes in and changes a property to lock the document for further editing. Then the document isn't really edited, but changed and something I would like to track for audit reasons. it would be wrong then if I used the same field to track edits. i could do that, since right now the requirement is that the creator can go in and see who changed the document the last time, but it is not really the right solution.

To implement the audit i did the following:

First of all I divided my Base Class into interfaces that needs to be implemented. The lastChangedBy and LastChangedDate is defined in IAuditable interface which objects that needs auditing has to implement.

Then overriding DbContext.SaveChanges() like this:

        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();
    }

There is of course other ways to do this, but this seemed like a start at least.

Now, I realize that my question could have been better, but honestly I did not realize that part of the problem was that I wanted to track changes for different reasons on different objects. The example with the document above is only one of more objects where LastChangedBy is not really what I need to set or track in my domain, but it can be rewritten to be more spesific like above.

Community
  • 1
  • 1
cfs
  • 1,304
  • 12
  • 30
  • What will you be using the tracked data for? – Yves Reynhout Oct 24 '12 at 20:05
  • @YvesReynhout Well, for different reasons really. On some objects it is just to keep track on who made the last change (who deactivated an object, changed information about a person) - for audit reasons really. Some of it is for documents to share knowledge, where there can be several editors, or persons who can make changes over time on a document (subject). The creator of the document might be notified when someone else changes it, and also need to see who changed it. I'm leaning towards the latter is a domain concern and the former is an audit concern... – cfs Oct 24 '12 at 22:21

2 Answers2

4

I started with Thread.CurrentPrincipal. But if you are going to switch to asynchronous processing (for instance using commands like I describe here) later it's not possible to use Thread.CurrentPrincipal.

But there is an fundamental issue first:

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

That's not a base class. It doesn't add any functionality. You should instead change it do an interface named something like ITrackChanges or similar.

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

That's how I do things. Why do you think that it's not elegant? An administrator or a background service can do things on behalf of a user with that approach.

Update

I feel it is not elegant because every method will need my user object as parameter for every change

Well. It's a business requirement, right? You must change the LastChangedBy property. Hence it must be provided.

Public setters is not forbidden in DDD. You can use them, but in this case both the LastChangedBy and LastChangedDate must be updated for every change. And that logic should be enforced by the model itself and not by the calling code. Hence it's not possible to use a public setter.

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • +1 just for _"An administrator or a background service can do things on behalf of a user with that approach."_. I had this exactly problem in an older application and the way it was architectured the solution didn't turn very elegant. – Luiz Damim Oct 24 '12 at 10:04
  • I had to set this as the accepted answer since this really put me in the right direction to find a soultion that I'm happy with and even though i implemented auditing in the infrastructure layer I feel I follow the main idea behind this answer. – cfs Nov 02 '12 at 23:47
2

...Or you can set the user into Thread.CurrentPrincipal and not make it part of any interface.

This works well with any identity model worth it's salt, be it on a website or desktop app with either custom or AD authentication.

To expand, your repository etc can throw if the user isn't set, and also you can still implement role or claims-based permissions to prevent/allow specific types of updates based on the identity of the user should you so wish.

Andras Zoltan
  • 41,961
  • 13
  • 104
  • 160
  • OK, so if I understand this right. Thread.CurrentPrincipal would work through layers. I.e. I can call this in whatever layer I want in the application. Where would you put it, you mention repository so in the infrastrucure layer/DAL? – cfs Oct 23 '12 at 22:49
  • And how do I get to the Guid UserId? checked it out and I could only get to the Name of the user... – cfs Oct 23 '12 at 23:14
  • 1
    You create an implementation of the `IIdentity` interface in which you can store whatever you want about the user. What I tend to do is to store as rich an identity object as possible - including display name etc - alongside the raw user ID. Once you've done that - you can set it into a new `GenericPrincipal` instance as shown in the example on the topic I've linked to; which, in turn you then set to the CurrenctPrincipal. Where you actually process this object is entirely up to you. Might make sense for the repositor(y|ies) to do it - since presumably they are `Base` aware? – Andras Zoltan Oct 23 '12 at 23:33
  • Hi, thanks. Yes I have a GenericRepository that takes all objects implementing the Base, so that should work fine. I'll have to look a bit more into this, but it looks like a good option since it is built into the framework and very unlikely that I will change that so it should be safe to do :) – cfs Oct 23 '12 at 23:40
  • I prefer this solution also, because it moves audit information into the infrastructure as much as possible. While audit information may be a business requirement, it is not a core function of your domain model, therefore the notion that passing a user instance for each behavior isn't elegant, is quite applicable. – eulerfx Oct 24 '12 at 16:00
  • Hi. Well I guess it depends. On some objects tracking who was the last one to change it can be seen as a domain concern. It's a difficult one. The two solutions suggested at the moment does represent some of my problems implementing DDD. Either solution could convince me of beeing the right one really. I'll wait a bit and see if someone comes with a different angle on this. I'm in doubt myself at the moment. – cfs Oct 24 '12 at 22:02
  • 1
    I refer you [to a question I asked about the repository pattern](http://stackoverflow.com/questions/5472566/atomic-insert-or-get-and-compare-and-update-a-repository-violation) as I got myself all twisted up on a particular minor detail - the quote, from the answer I accepted, 'the pattern is there to serve you, not the other way round' springs to mind here :) – Andras Zoltan Oct 24 '12 at 22:15
  • @AndrasZoltan Hahaha, well, I agree. :) I have a really hard time letting it go with a solution that I feel is a bit wrong (it kind of really really bothers me deep down inside a lot knowing I don't undertand the concept). But, on the other hand learning new stuff is not always easy and some things is better to do the mindwork now in the start instead og a lot of extra handwork later on. I must say that the discussion here had me rethink a bit, and the solution I'm working on, and will post when tested, is based on the feedback I have gotten here so I hope in the end it is worth the time ;) – cfs Oct 26 '12 at 08:49
  • Thanks for the help. If I could mark two answers as accepted I would, but really it was the comments, more than the suggested solution, from you and the others above that proved useful. – cfs Nov 02 '12 at 23:50