You hit the nail on the head in identifying the difficulty with using Entities as business objects. After much trial and error, here's the pattern that we've settled into, which has been working very well for us:
Our application is divided into modules, and each module is divided into three tiers: Web (front-end), Core (business), and Data. In our case, each of these tiers is given its own project, so there is a hard enforcement preventing our dependencies from becoming tightly-coupled.
The Core layer contains utility classes, POCOs, and repository interfaces.
The Web layer leverages these classes and interfaces to get the information it needs. For example, an MVC controller can take a particular repository interface as a constructor argument, so our IoC framework injects the correct implementation of that repository when the controller is created. The repository interface defines selector methods that return our POCO objects (also defined in the Core business layer).
The Data layer's entire responsibility is to implement the repository interfaces defined in the Core layer. It has an Entity Framework context that represents our data store, but rather than returning the Entities (which are technically "data" objects), it returns the POCOs defined in the Core layer (our "business" objects).
In order to reduce repetition, we have an abstract, generic EntityMapper
class, which provides basic functionality for mapping Entities to POCOs. This makes the majority of our repository implementations extremely simple. For example:
public class EditLayoutChannelEntMapper : EntityMapper<Entity.LayoutChannel, EditLayoutChannel>,
IEditLayoutChannelRepository
{
protected override System.Linq.Expressions.Expression<Func<Entity.LayoutChannel, EditLayoutChannel>> Selector
{
get
{
return lc => new EditLayoutChannel
{
LayoutChannelId = lc.LayoutChannelId,
LayoutDisplayColumnId = lc.LayoutDisplayColId,
ChannelKey = lc.PortalChannelKey,
SortOrder = lc.Priority
};
}
}
public EditLayoutChannel GetById(int layoutChannelId)
{
return SelectSingle(c => c.LayoutChannelId == layoutChannelId);
}
}
Thanks to the methods implemented by the EntityMapper base class, the above repository implements the following interface:
public interface IEditLayoutChannelRepository
{
EditLayoutChannel GetById(int layoutChannelId);
void Update(EditLayoutChannel editLayoutChannel);
int Insert(EditLayoutChannel editLayoutChannel);
void Delete(EditLayoutChannel layoutChannel);
}
EntityMappers do very little in their constructors, so it's okay if a controller has multiple repository dependencies. Not only does the Entity Framework reuse connections, but the Entity Contexts themselves are only created when one of the repository methods gets called.
Each module also has a special Test project, which contains unit tests for the classes in these three tiers. We've even come up with a way to make our repositories and other data-access classes somewhat unit-testable. Now that we've got this basic infrastructure set up, adding functionality to our web application is generally pretty smooth and not too error-prone.