akd
akd

Reputation: 6758

Prevent a self-referencing table from becoming circular in c#

Here @mellamokb explained how to create a constraint prevents circular dependency.

I will need to implement the same in C#. I believe this requires a sort of recursive function but I can't get my head around of it.

I have a table as below:

Managers UserId | ManagerId

An example:

UserId | ManagerId
1         2
2         1

This is allowed. Users can be manager of each other

However:

UserId | ManagerId
1         2
2         3
3         1

This is not allowed.

I tried the following:

 private Manager CheckCircularDependency(int userId)
    {
        var managers = Managers.GetByManagersId(userId);
        if(managers==null || managers.Count == 0)
        {
            return null;
        }

        foreach (Manager manager in managers)
        {
             var man= CheckCircularDependency(manager.UserId);
            if (man== null)
            {
                return manager;
            }
        }

        return null;
    }

here is the checking:

public boid  AddManager(int userId, int managerId){

 var manager= CheckCircularDependency(userId);
  if (manager!= null)
            {
                if (manager.ManagerId == userId && manager.UserId == managerId)
                {
                    //this is allowed
                }else if(manager.ManagerId != userId){
                  throw new InvalidOperationException(" Not allowed");
                 }
            }
}

I have in the table:

1   2
2   3

When I try to insert another manager as 3 => 1 I should get exception but I don't. the recursive always return null instead of return user 1.

Any idea why?

Upvotes: 0

Views: 385

Answers (2)

Jason G.
Jason G.

Reputation: 810

A circular dependency can occur at any level or branch within the recursive tree not just the bottom level. Currently as soon as you reach the bottom or a user without any managers your recursive function will return without going through the rest of the managers in your foreach. Instead I would change you recursive function to indicate whether or not the combination of UserId and ManagerId will create a circular dependency. Then you can return true as soon as you find a conflict or false if no conflict is found within any branch or level. See example:

private bool CheckCircularDependency(int userId, int managerId, bool rootNode = false)
{
    //Optional: A user may not manage themselves
    if(userId == managerId && rootNode) return true;

    var managers = Managers.GetByManagersId(userId);
    if(managers == null || managers.Count == 0)
    {
        //User is not managing anyone therefore no conflict
        return false;
    }

    foreach (Manager manager in managers)
    {
        //Circular dependency, unless they are managers of each other
        if(manager.UserId == managerId && !rootNode) return true;

         var circularDependency = CheckCircularDependency(manager.UserId, managerId);
        if (circularDependency)
        {
            return true;
        }
    }

    //No conflicts found
    return false;
}

Add method:

public void AddManager(int userId, int managerId)
{
    if(CheckCircularDependency(userId, managerId, true))
    {
        throw new InvalidOperationException(" Not allowed");
    }
    else
    {
        //this is allowed
    }
}

This example assumes Manager.GetByManagersId(userId) returns all records where userId is in the ManagerId column.

Working example at https://dotnetfiddle.net/Jc6tfY with output:

Adding manager 1 => 2 : Success!
Adding manager 2 => 3 : Success!
Adding manager 2 => 1 : Success!
Adding manager 3 => 1 : Not allowed

Upvotes: 1

Erik Philips
Erik Philips

Reputation: 54628

Personally I'm not a fan of recursion simply because at it's core recursive functions allow the possibility of a stack overflow and since there are many easier ways to do the same logic without a possible exception I don't use them unless I know for a fact that Set of items is limited.

If we're going to prevent the problem we have to disallow consumers to change users. Only exposing the IUser interface provides that type of accessibility. (User someone can change it via reflection but that's not the issue you asked about).

DotNetFiddle

using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
   public static void Main()
   {
      var userManager = new UserManager();
      // self referencing manager?
      var user1 = new User { Id = 1, ManagerId = 1 };

      // two users managing each other
      var user2 = new User { Id = 2, ManagerId = 3 };
      var user3 = new User { Id = 3, ManagerId = 2 };

      // three users managing each other
      var user4 = new User { Id = 4, ManagerId = 5 };
      var user5 = new User { Id = 5, ManagerId = 6 };
      var user6 = new User { Id = 6, ManagerId = 4 };

      // no manager?
      var user7 = new User { Id = 7, };

      IUser outUser;

      Console.WriteLine($"added user?{userManager.TryAdd(user1, out outUser)} user added:{outUser.ToConsole()}");
      Console.WriteLine($"added user?{userManager.TryAdd(user2, out outUser)} user added:{outUser.ToConsole()}");
      Console.WriteLine($"added user?{userManager.TryAdd(user3, out outUser)} user added:{outUser.ToConsole()}");
      Console.WriteLine($"added user?{userManager.TryAdd(user4, out outUser)} user added:{outUser.ToConsole()}");
      Console.WriteLine($"added user?{userManager.TryAdd(user5, out outUser)} user added:{outUser.ToConsole()}");
      Console.WriteLine($"added user?{userManager.TryAdd(user6, out outUser)} user added:{outUser.ToConsole()}");

      Console.WriteLine($"added user?{userManager.TryAdd(user7, out outUser)} user added:{outUser.ToConsole()}");

      Console.WriteLine("done adding...");

      Console.WriteLine("Current users:");
      foreach(var kvp in userManager.Users)
      {
         Console.WriteLine($"{kvp.Value.ToConsole()}");
      }

   }


   public class UserManager
   {
      private Dictionary<int, User> _users = new Dictionary<int, User>();

      public IReadOnlyDictionary<int, IUser> Users
      {
         get
         {
            return _users
            .Select(kvp => new { Key = kvp.Key, Value = kvp.Value as IUser })
            .ToDictionary(a => a.Key, a => a.Value);
         }
      }

      public bool TryAdd(IUser user, out IUser userResult)
      {
         userResult = null;
         var result = !IsUserCircular(user);
         if (result)
         {
            var validUser = new User { Id = user.Id, ManagerId = user.ManagerId };
            _users.Add(validUser.Id, validUser);
            userResult = validUser;
         }

         return result;
      }

      private bool IsUserCircular(IUser user)
      {
         var currentUser = user;
         var currentManagers = new HashSet<int> { user.Id };
         var result = false;
         while (currentUser?.ManagerId != null)
         {
            // just because they have an Id doesn't mean that user exists...
            // or does it?
            if (currentManagers.Contains(currentUser.ManagerId.Value))
            {
               // we've come full circle to the same user through X users
               result = currentManagers.Count > 2;
               break;
            }
            else
            {
               if (_users.TryGetValue(currentUser.ManagerId.Value, out User nextUser))
               {
                  currentManagers.Add(currentUser.ManagerId.Value);
                  currentUser = nextUser;
               }
               else
               {
                  // user has Manager that doesn't exist in our system
                  currentUser = null;
               }
            }
         }

         return result;
      }
   }
}
public interface IUser
{
   int Id { get; }

   int? ManagerId { get; }
}

public class User : IUser
{
   public int Id { get; set; }
   public int? ManagerId { get; set; }
}


public static class IUserExtensions
{
   public static string ToConsole(this IUser user)
   {
      if (user == null)
         return $"null";
      return $"Id={user.Id} ManagerId={(user.ManagerId.HasValue ? user.ManagerId.ToString() : "null")}";
   }

}

Output:

added user?True user added:Id=1 ManagerId=1

added user?True user added:Id=2 ManagerId=3

added user?True user added:Id=3 ManagerId=2

added user?True user added:Id=4 ManagerId=5

added user?True user added:Id=5 ManagerId=6

added user?False user added:null

added user?True user added:Id=6 ManagerId=null

done adding...

Current users:

Id=1 ManagerId=1

Id=2 ManagerId=3

Id=3 ManagerId=2

Id=4 ManagerId=5

Id=5 ManagerId=6

Id=7 ManagerId=null

Upvotes: 0

Related Questions