SitePoint Sponsor

User Tag List

Results 1 to 9 of 9
  1. #1
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)

    Arrow Let's build this together...

    After taking a few days off from my project, and spending some time at the nhusers and DDD groups, I'd like to start looking at things again, and possibly building them here using your input. One reason I want to do this is to get a better direction to the project, and build on your experience. In addition, it will allow you all to see how I think, and make comments on things as they progress. So if you feel up to it, let's do this together, one step at a time.

    At the very heart of things, I needed a permission entity (app will provide a maintenance mode which will allow these to be altered or added and removed from). And so, that was born:

    Code Csharp:
    public class Permission : Entity
    {
     [Signature]
     public virtual string Code { get; protected set; }
     public Permission(string code)
     {
      Logic.Check(!string.IsNullOrEmpty(code) && !string.IsNullOrWhiteSpace(code), "Code can not be null, empty or whitespace!");
      Code = code.Trim();
     }
     protected Permission() { }
    }

    I then needed roles which the above permissions could be assigned to, thereby giving the role usefulness.

    Code Csharp:
    public class Role : Entity
    {
     [Signature]
     public virtual string Name { get; protected set; }
     private IList<Permission> permissions = new List<Permission>();
     public virtual IEnumerable<Permission> Permissions { get { return permissions; } }
     public virtual void AddPermission(Permission permission)
     {
      Logic.Check(permission != null, "Permission can not be null!");
      if (!permissions.Contains(permission))
       permissions.Add(permission);
     }
     public Role(string name)
     {
      Logic.Check(!string.IsNullOrEmpty(name) && !string.IsNullOrWhiteSpace(name), "Name can not be null!");
      Name = name.Trim();
     }
     protected Role() { }
    }

    Then I needed a user object to which roles could be assigned:

    Code Csharp:
    public class User : Entity
    {
     [Signature]
     public virtual string Username { get; protected set; }
     public virtual string Password { get; set; }
     public virtual string Email { get; set; }
     private IList<Role> roles = new List<Role>();
     public virtual IEnumerable<Role> Roles { get { return roles; } }
     public virtual void AddRole(Role role)
     {
      Logic.Check(role != null, "Role can not be null!");
      if (!roles.Contains(role))
       roles.Add(role);
     }
     public virtual void RemoveRole(Role role)
     {
      Logic.Check(role != null, "Role can not be null!");
      if (roles.Contains(role))
       roles.Remove(role);
     }
     public User(string username)
     {
      Logic.Check(!string.IsNullOrEmpty(username) && !string.IsNullOrWhiteSpace(username), "Username can not be null!");
      Username = username.Trim();
     }
     protected User() { }
    }

    So things are starting to look pretty good in my opinion, but before I go on, can I get some comments and feedback on these three preliminary entities? Keep in mind that I have yet to add a lot of other properties to them that aren't completely relavent at present. The two major questions I have at this point is in regards to the Role entity.

    - Should it not also have a list of associated users?
    - If so, should I allow for adding and removing of users from the role directly?
    - Or do I have this backwards to start with?

  2. #2
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Also, in order to help everybody see my vision for this, here's a basic diagram (minus most of the properties).
    Attached Images Attached Images

  3. #3
    SitePoint Zealot
    Join Date
    May 2004
    Location
    Jersey
    Posts
    175
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I'm personally using CQRS and Event Sourcing now so my perspective may be a little different but here's my thoughts.

    Firstly the simple stuff, I notice you're using 'signature' attributes similar to Sharp Architecture - personally I don't see the need for them. I know why you use them, but two User entities will never be equal anyway because their id's will be different, and you're still going to run some kind of check to see if that username exists in your database when the entity is transient. Similar for the Role entity, I'd remove the signature, perform the checks you'd perform anyway and just add the unique constraint when mapping it. I guess if they're part of your Entity base class and it's comparison methods it's not worth the hassle changing that though.

    I'd also remove (or use the protected modifier for an ORM) the public setters on the Password and Email fields. I couldn't think of any scenario where you'd create a new User without a password at least, even a predefined password would contain either no characters or 'password' etc. I'd set them in the constructor and have ChangePassword(), ChangeEmail() methods.

    As for the Role, I guess it depends on your end-user perspective. Is the User going to assign a role to themselves, or are you going to assign a user to a role. In most cases you're probably going to have some form of super user who assigns users to roles, in which case a Role.AddUser(IUser) would seem the more likely option. I'd take a look at this article that discusses a pretty similar problem, http://www.udidahan.com/2009/01/24/d...ional-mapping/.

    A job can be posted to multiple job boards. And a job board can have multiple jobs posted. A regular many to many relationship.

    ...

    While we could just leave the bi-directional/circular dependency between them, it would be preferable if we could make it uni-directional instead. To do that, we need to understand their relationship:

    If there was no such thing as “job”, would there be meaning to “job board” ? Probably not.

    If there was no such thing as “job board”, would there be meaning to “job” ? Probably. Yes. Our company can handle the hiring process of a job regardless of whether the candidate came in through Monster.com or not.

    From this we understand that the uni-directional relationship can be modelled as one-to-many from job board to job. The Job class would no longer have a collection of Job Board objects. In fact, it could even be in an assembly separate from Job Board and not reference Job Board in any way. Job Board, on the other hand, would still have a collection of Job objects.

    Going back to the code above we see that the right choice is jobBoard.Post(job);
    If you're starting over I'd personally spend a couple of hours reading up on CQRS, it might seem like a lot of extra work at first but I've found the opposite.
    Matt Daly

  4. #4
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Thanks for responding Matt. I really appreciate the input.

    As you surmised, my Signature attribute is indeed for comparisons, not for checking against a constraint. The first few things my base Entity class tests in Equals() is:


    if (obj == null)
    returnfalse;
    if (ReferenceEquals(this, obj))
    returntrue;
    if (!Equals(this.Id, ((Entity<T>)obj).Id))
    returnfalse;

    If it makes it past this point, it compares properties with the signature attribute (as opposed to the ValueObject base class which uses all of the properties).

    Are you saying that I don't need to go this far?

    As far as the protected property and the other "fields" in the above entities, don't pay that any mind at the moment. I simply put "some" properties in there to make it feel more like an entity. There are about 30 fields intended for the User entity alone. Those entities are only prelims and not final by any means.

    I liked what you had to say about my user to role relationship and this is indeed one of the thing I've been tossing back and forth. As I see it, a user cannot exist until at least one role has been defined. This would indicate role.AddUser(user), however, on registration, they are assigned the default new member role. This makes me lean more toward user.AddRole(defaultRole) rather than defaultRole.AddUser(newUser). Also note that some roles will be defined as publically joinable via an IsJoinable property on the role. Users should be able to go into their control panel and choose from a list of publically joinable roles, again leaning more towards the user maintain their own roles.

    On the same token, when viewing a user in the admincp, along with their properties, I want to see a checkbox list of roles, with those currently assigned checked. The same principle of the user managing their roles also seems to hold true.


    What are your thoughts on this?

  5. #5
    SitePoint Zealot
    Join Date
    May 2004
    Location
    Jersey
    Posts
    175
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Serenarules View Post
    Thanks for responding Matt. I really appreciate the input.

    As you surmised, my Signature attribute is indeed for comparisons, not for checking against a constraint. The first few things my base Entity class tests in Equals() is:


    if (obj == null)
    returnfalse;
    if (ReferenceEquals(this, obj))
    returntrue;
    if (!Equals(this.Id, ((Entity<T>)obj).Id))
    returnfalse;

    If it makes it past this point, it compares properties with the signature attribute (as opposed to the ValueObject base class which uses all of the properties).

    Are you saying that I don't need to go this far?
    Personally I don't see the need. When you compare two non transient entities they're obviously never going to be equal, their ids will differ. When you compare two transient entities signature attributes would be useful but how often does this occur? Most comparisons will end up being a transient (new user) against a non transient (exisiting user), which regardless of any signature attributes will never be equal because one has an id and one doesn't, you'll still have to perform a seperate call to check whether the username exists. I guess they'd be useful for comparisons prior to persistence, things like detecting duplicates in a list of data for data entry. I suppose it depends on the domain you're building. If you've already built them into pre-existing base classes you may as well leave them in.

    Quote Originally Posted by Serenarules View Post
    I liked what you had to say about my user to role relationship and this is indeed one of the thing I've been tossing back and forth. As I see it, a user cannot exist until at least one role has been defined. This would indicate role.AddUser(user), however, on registration, they are assigned the default new member role. This makes me lean more toward user.AddRole(defaultRole) rather than defaultRole.AddUser(newUser). Also note that some roles will be defined as publically joinable via an IsJoinable property on the role. Users should be able to go into their control panel and choose from a list of publically joinable roles, again leaning more towards the user maintain their own roles.

    On the same token, when viewing a user in the admincp, along with their properties, I want to see a checkbox list of roles, with those currently assigned checked. The same principle of the user managing their roles also seems to hold true.


    What are your thoughts on this?
    I see where you're coming from with a default user role and joinable roles but if you take the technical approach rather than the domain approach, if the User entity contained an AddRole method how could an admin/super-user create new roles, view a list of roles etc? You'd be digging down through all the users to list roles and you just can't create new roles.

    From that perspective Roles would appear to be an aggregate of it's own, in that case Role.AddUser(IUser) seems the only plausible solution - and is confirmed by the way the domain/business (I'm assuming you're acting as the domain expert) acts, someone assigns specific users to specific roles, the users themselves don't do that.

    To assign a user to a role you'd then use some form of user registration service that performs validation against the user, persists it and then calls AddUser on the default role to assign that user to that role.
    Matt Daly

  6. #6
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Alright, I think I understand what you're saying now. Considering I can't even add a user without a role defined, that makes sense.

    I'm wondering now, however, if I should subclass role into two entities PrimaryRole and JoinableRole, discriminating on "IsJoinable". This way a user belongs to a primary role, but can add joinable roles at will, without affecting their primary. For example:

    // on registration...

    role = roleRepository.SelectByName(defaultRoleName);
    role.AddUser(user);

    // then later user can...

    user.JoinRole(joinableRole);
    //or
    user.LeaveRole(joinableRole);

    Admins would set up both primary and secondary roles initially, users cannot create them, only join. A user will also never be able to leave their primary role. That would be an admins job to move them to a different primary.

    Would this be a slightly better approach?

  7. #7
    SitePoint Zealot
    Join Date
    May 2004
    Location
    Jersey
    Posts
    175
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    You could do that, but then your User object will end up knowing about roles and will contain an IEnumerable<JoinableRole>, which will never contain anything until the user calls JoinRole - which it might never do.

    To me it would make more sense to stick with an IsJoinable boolean in your Role, keep the simple Role.AddUser(..) and simply perform validation as a User calls that to ensure that the Role allows people to join themselves. This way it isn't upto the User to determine whether they are allowed to join a role, it is down to the Role (I'd say that it was upto each Role to keep users out, rather than the User to know whether or not it's allowed to join a certain role). You could then just filter out Roles where IsJoinable = false and not allow the user to select them (coupled with validation inside the AddUser method).
    Matt Daly

  8. #8
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    That makes a lot of sense. Thanks for the input. I'll need some time to flesh these three entities out a bit. Once I do, I'll post the finals. If we don't see anything that needs changing, I'll move to the next aggregate chain, which needs to be in place before we can connect the two.

  9. #9
    Resident OCD goofball! bronze trophy Serenarules's Avatar
    Join Date
    Dec 2002
    Posts
    1,911
    Mentioned
    26 Post(s)
    Tagged
    0 Thread(s)
    Ok, the revised entities, as they sit now (future refactoring aside):

    Code Csharp:
    namespace Genesis.Domain
    {
    using FluentEngine;
    using FluentEngine.Domain;
    public class Permission : Entity
    {
    [Signature]
    public virtual string Code { get; protected set; }
    public Permission(string code)
    {
    Logic.Check(!string.IsNullOrEmpty(code) && !string.IsNullOrWhiteSpace(code), "Code can not be null, empty or whitespace!");
    Code = code.Trim();
    }
    protected Permission() { }
    }
    }

    Code Csharp:
    using System.Collections.Generic;
    namespace Genesis.Domain
    {
    using FluentEngine;
    using FluentEngine.Domain;
    public class Role : Entity
    {
    private IList<Permission> permissions = new List<Permission>();
    private IList<User> users = new List<User>();
    [Signature]
    public virtual string Name { get; protected set; }
    public virtual string Description { get; set; }
    public virtual string MarkupClass { get; set; }
    public virtual bool ShowInGroupList { get; set; }
    public virtual bool IsJoinable { get; set; }
    public virtual IEnumerable<Permission> Permissions { get { return permissions; } }
    public virtual IEnumerable<User> Users { get { return users; } }
    public virtual void SetName(string name)
    {
    Logic.Check(!string.IsNullOrEmpty(name) && !string.IsNullOrWhiteSpace(name), "Name can not be null!");
    Name = name.Trim();
    }
    public virtual void AddPermission(Permission permission)
    {
    Logic.Check(permission != null, "Permission can not be null!");
    if (!permissions.Contains(permission))
    permissions.Add(permission);
    }
    public virtual void RemovePermission(Permission permission)
    {
    Logic.Check(permission != null, "Permission can not be null!");
    if (permissions.Contains(permission))
    permissions.Remove(permission);
    }
    public virtual void AddUser(User user)
    {
    Logic.Check(user != null, "User can not be null!");
    if (!users.Contains(user))
    users.Add(user);
    }
    public virtual void RemoveUser(User user)
    {
    Logic.Check(user != null, "User can not be null!");
    if (users.Contains(user))
    users.Remove(user);
    }
    public Role(string name)
    {
    SetName(name);
    }
    protected Role() { }
    }
    }

    Code Csharp:
    using System;
    namespace Genesis.Domain
    {
    using FluentEngine;
    using FluentEngine.Domain;
    public class User : Entity
    {
    [Signature]
    public virtual string Username { get; protected set; }
    public virtual string Password { get; protected set; }
    public virtual string Email { get; protected set; }
    public virtual string DisplayName { get; set; }
    public virtual bool ShowEmail { get; set; }
    public virtual bool ShowDisplayName { get; set; }
    public virtual string AimAddress { get; set; }
    public virtual string MsnAddress { get; set; }
    public virtual string YimAddress { get; set; }
    public virtual string AvatarUrl { get; set; }
    public virtual string Signature { get; set; }
    public virtual DateTime JoinDate { get; set; }
    public virtual string JoinHost { get; set; }
    public virtual Guid ActivateCode { get; set; }
    public virtual DateTime ActivateDate { get; set; }
    public virtual string ActivateHost { get; set; }
    public virtual DateTime LastVisitDate { get; set; }
    public virtual string LastVisitHost { get; set; }
    public virtual int LoginAttempts { get; set; }
    public virtual bool LockedOut { get; set; }
    public virtual DateTime UnlockDate { get; set; }
    public virtual bool Approved { get; set; }
    public virtual void SetUsername(string username)
    {
    Logic.Check(!string.IsNullOrEmpty(username) && !string.IsNullOrWhiteSpace(username), "Username can not be null!");
    Username = username.Trim();
    }
    public virtual void SetPassword(string password)
    {
    Logic.Check(!string.IsNullOrEmpty(password) && !string.IsNullOrWhiteSpace(password), "Password can not be null!");
    Password = password.Trim();
    }
    public virtual void SetEmail(string email)
    {
    Logic.Check(!string.IsNullOrEmpty(email) && !string.IsNullOrWhiteSpace(email), "Email can not be null!");
    Email = email.Trim();
    }
    public User(string username, string password, string email)
    {
    SetUsername(username);
    SetPassword(password);
    SetEmail(email);
    }
    protected User() { }
    }
    }

    Let me know if there is anything obvious I missed, or needs changing.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •