Cómo romper las dependencias circulares entre repositorios

To begin with, no I'm not using an ORM, nor am I allowed to. I have to hand-roll my repositories using ADO.NET.

Tengo dos objetos:

public class Firm
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public virtual IEnumerable<User> Users { get; set; }
}

public class User
{
    public Guid Id { get; set; }
    public string Username { get; set; }
    public Firm Firm { get; set; }
}

note the references to each other, a Firm has a list of Users, each User has only one Firm.

Now I want to design my repositories:

public interface IFirmRepository
{
    IEnumerable<Firm> FindAll();
    Firm FindById(Guid id);
}

public interface IUserRepository
{
    IEnumerable<User> FindAll();
    IEnumerable<User> FindByFirmId(Guid firmId);
    User FindById(Guid id);
}

So far, so good. I want to load the Firm for each User from my UserRepository. The FirmRepository knows how to create a Firm from persistence, so I'd like ot keep that knowledge with the FirmRepository.

public class UserRepository : IUserRepository
{
    private IFirmRepository _firmRepository;

    public UserRepository(IFirmRepository firmRepository)
    {
        _firmRepository = firmRepository;
    }

    public User FindById(Guid id)
    {
        User user = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, username, firm_id from users where u.id = @ID";
            SqlParameter userIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(userIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    user = CreateListOfUsersFrom(reader)[0];
                }
            }
        }
        return user;
    }

    private IList<User> CreateListOfUsersFrom(SqlDataReader dr)
    {
       IList<User> users = new List<User>();
       while (dr.Read())
       {
           User user = new User();
           user.Id = (Guid)dr["id"];
           user.Username = (string)dr["username"];
           //use the injected FirmRepository to create the Firm for each instance of a User being created
           user.Firm = _firmRepository.FindById((Guid)dr["firm_id"]);
       }
       dr.Close();
       return users;
    }

}

now when I go to load any User via the UserRepository, I can ask the FirmRepository to build the User's Firm for me. So far, nothing too crazy here.

Ahora el problema.

I want to load a list of Users from my FirmRepository. The UserRepository knows how to create a User from persistence, so I'd like to keep that knowledge with the UserRepository. So, I pass in a reference to IUserRepository to the FirmRepository:

public class FirmRepository : IFirmRepository
{
    private IUserRepository
    public FirmRepository(IUserRepository userRepository)
    {

    }
}

But now we have a problem. The FirmRepository depends on an instance of IUserRepository, and the UserRepository now depends on an instance of IFirmRepository. So one Repository cannot be created without an instance of the other.

If I keep IoC containers OUT of this equation (and I should, b/c Unit Tests should not use IoC containers), there is no way for me to accomplish what I'm trying to do.

No problem, though, I'll just create a FirmProxy to lazy-load the Users collection from Firm! That's a better idea, b/c I don't want to load ALL the Users all the time when I go to get a Firm or a list of Firms.

public class FirmProxy : Firm
{
    private IUserRepository _userRepository;
    private bool _haveLoadedUsers = false;
    private IEnumerable<User> _users = new List<User>();

    public FirmProxy(IUserRepository userRepository)
        : base()
    {
        _userRepository = userRepository;
    }

    public bool HaveLoadedUser()
    {
        return _haveLoadedUsers;
    }

    public override IEnumerable<User> Users
    {
        get
        {
            if (!HaveLoadedUser())
            {
                _users = _userRepository.FindByFirmId(base.Id);
                _haveLoadedUsers = true;
            }
            return _users;
        }
    }

}

So, now I have a nice proxy object to facilitate lazy-loading. So, when I go to create a Firm in the FirmRepository from persistence, I instead return a FirmProxy.

public class FirmRepository : IFirmRepository
{

    public Firm FindById(Guid id)
    {
        Firm firm = null;
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            SqlCommand command = connection.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = "select id, name from firm where id = @ID";
            SqlParameter firmIDParam = new SqlParameter("@ID", id);
            command.Parameters.Add(firmIDParam);
            connection.Open();
            using (SqlDataReader reader = command.ExecuteReader())
            {
                if (reader.HasRows)
                {
                    firm = CreateListOfFirmsFrom(reader)[0];
                }
            }
        }
        return firm;
    }

private IList<Firm> CreateListOfFirmsFrom(SqlDataReader dr)
{
    IList<FirmProxy> firms = new List<FirmProxy>([need an IUserRepository instance here!!!!]);
    while (dr.Read())
    {

    }
    dr.Close();
    return firms;
}

But this is still not working!!!

In order to return a FirmProxy instead of a Firm, I need to be able to new up a FirmProxy in my FirmRepository class. Well, the FirmProxy takes an IUserRepository instance b/c the UserRepository contains the knowledge of how to create a User object from persistence. As a result of the FirmProxy needing an IUserRepository, my FirmRepository now needs an IUserRepository as well, and I'm right back to square one!

So, given this long winded-explanation/source code, how can I go about being able to create an instance of a User from the FirmRepository and an instance of Firm from the UserRepository without:

  1. put the User creation code in the FirmRepository. I don't like this. Why should the FirmRepository know anything about creating an instance of a User? This to me is a violation of SoC.
  2. Not using the Service Locator pattern. If I go this route, I feel this is notoriously hard to test. Besides, constructors of objects that take explicit dependencies make those dependencies obvious.
  3. property injection instead of constructor injection. This doesn't fix a thing, I still need an instance of IUserRepository when new'ing up a FirmProxy no matter how the dependency is injected into FirmProxy.
  4. Having to "dumb down" either the Firm or the User object and expose a FirmID on User, for example instead of a Firm. If I'm just doing id's, then the requirement to load a Firm from the UserRepository goes away, but with it goes the richness of being able to ask the Firm object for anything withing the context of a given User instance.
  5. Resorting to an ORM. Again, I want to do it, but I can't. No ORM's. That's the rule (and yes, it's a crappy rule)
  6. keep all my inject-able dependencies as dependencies injected from the lowest level of the application, which is the UI (in my case, a .NET web project). No cheating and using IoC code in the FirmProxy to new up the appropriate dependency for me. That's basically using the Service Locator pattern anyhow.

I think about NHiberante and Enitity Framework, and it seems they have no problem figuring out how to generate sql for a simple example that I've presented.

Does anyone else have any other ideas/methods/etc... that will help me achieve what I want to do without an ORM?

Or maybe there is a different/better way to approach this? Want I don't want to lose is the ability to access a Firm from a User, or to get a list of Users for a given Firm

preguntado el 09 de marzo de 12 a las 16:03

@indiecodemonkey Have you found a solution? -

1 Respuestas

You need to think more clearly about your aggregate root objects, i.e. the focus of each repository. You don't create a repository for each table in your database, that's not the objective. The intent is to identify the aggregate root object that you need to work with, include the child tables/records, i.e. User(Firm, Address, Access Rights). That's what your repository will return to your app.

The difficulty you are having is showing you that your repositories are not correctly structured. Anytime I find my code becoming too difficult it raises an alert with me that I'm probably doing it wrong, I live my life by that ;)

respondido 09 mar '12, 16:03

+1 - in this case it seems as if both Firm and User are both aggregate roots. In these scenarios I usually have a soft reference between the two roots (via a reference key), and if I need a data structure that joins them together I do it explicitly (could be a query object or a view model etc). - MattDavey

I see the Firm being the Aggregate root, and the Users falling under that root. But at the same time, I do not see Firm being an aggregate root evidenced by I could want to work with a User, and not bother at all with the User's Firm. Yet when I go to persist the User back to the db, in the agg root example, I'd have to persist all of Firm, and all it's Users. It's interesting what MattDavey says about them BOTH being agg roots. He leads me back to soft references, which I wish I could stay away from, I like to use object references from each entity. - Michael mccarthy

The way I generally think about aggregate roots is by the rule of 'cascading deletes'. If you delete a aggregate root, everything underneath should be deleted too. If you delete a firm, should all the users be deleted too? - MattDavey

Right now, yes. But this means I need to handle all my User interactions via my Agg Root, the Firm. This also means the FirmRepository has a lot more heavy lifting to do as far as inserts and updates. It now has to potentially handle an update to one or more users in a list of users. This means I need to start putting in dirty tracking for User entities that have been added/changed since the Firm was loaded. ORM's I think could handle this for me, but in my case, it sounds like I'd have to write this tracking, and handling of what to do in my agg root at the time of persistence myself. - Michael mccarthy

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.