Before we begin, it might make sense to separate the concepts of contracts with layers. You specified 4 layers, but they can also be described as 3 levels and one contract scheme (domain contract scheme), which is an end-to-end split-up situation containing problems for DAL, BL, and potentially higher levels.
- Domain: domain classes only
- DAL: responsible for data access, containing UOWs and repositories and data access checks.
- BL: Unique Responsible Business Logic and Unique DAL Boss.
- User Interface: It's Not That Important! (how even it can be a console application!)
As for typical N-tier SOA architectures of the LOB type, where services can be consumed by other teams or clients, it usually makes sense to strive to increase three contractual schemes.
The first are the schemes of contracts between consumers and the services you want to provide. They may be versions. These schemes may be case-specific and provide โrepresentationsโ of the base object's scheme in various forms of normalization and composition. These schemes may also include metadata support to support specific case-specific requirements, such as a collection of search lists and specifying specific user permissions.
The second is the domain domain scheme in which all your current one works. This is always relevant and represents a logical diagram in an object-oriented design.
The third is the physical diagram in which your data is located. It is always relevant and contains your data in any suitable design dictated by storage technology, whether in a relational design or otherwise. Please note: although in the general case the physical scheme and the logical scheme may look the same or even identical, sometimes there are limitations in the physical storage technology that may require the physical scheme to be different. For example, in the MS Sql Server table there should not be more than 8060 bytes per row, and in some cases one logical object data object can be stored in several physical tables connected to each other, if MS Sql Server is used.
Based on these three schemes, you can then create your own solution to solve the problems associated with supporting these schemes.
To manage storage and physical scheme management, select a technology / storage strategy (for example: Sql Server, Oracle, MySql, xml, MongoDb, etc.)
To address I / O to a physical schema and transfer it from a physical schema to an entity schema, you enter an IDataAccess interface.
To eliminate business rules that must always be followed around an entity or set of objects, regardless of the option used, you enter the IBusiness level interface.
To address rules that are specific use cases that you do not want to repeat between multiple clients, and to translate between an entity schema and a service contract schema, you enter the IApplicationService level interface.
To address the external and external objects of the contract scheme transport, you enter the corresponding classes of the host controller (ApplicationServiceController, ApplicationServiceWCFHost or ApplicationService.asmx, etc.).
Here is a set of interfaces contained in a common namespace strategy that you can implement to provide you with abstractions for entity schemas, as well as for accessing data and business layers.
AcmeFrameworkContracts.dll
public class Response {} public class Response<TResponse> : Response where TResponse : Response<TResponse> {}
AcmeFramework.dll
links: AcmeFrameworkContracts.dll
namespace AcmeFramework { namespace Entity { public abstract class Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey> where TEntity : Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey> where TDataObject : Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey>.BaseDataObject where TDataObjectList : Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey>.BaseDataObjectList, new() where TIBusiness : Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey>.IBaseBusiness where TIDataAccess : Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKey>.IBaseDataAccess where TPrimaryKey : IComparable<TPrimaryKey>, IEquatable<TPrimaryKey> { public class BaseDataObject { public TPrimaryKey Id; } public class BaseDataObjectList : CollectionBase<TDataObject> { public TDataObjectList ShallowClone() { ... } } public interface IBaseBusiness { TDataObjectList LoadAll(); TDataObject LoadById(TPrimaryKey id); TDataObject LoadByIds(IEnumerable<TPrimaryKey> ids); IQueryable<TDataObject> Query(); IQueryable<TDataObject> FindBy(Expression<Func<T, bool>> predicate); void Delete(TPrimaryKey ids); void Delete(IEnumerable<TPrimaryKey> ids); void Save(TDataObject entity); void Save(TDataObjectList entities); ValidationErrors Validate(TDataObject entity); // <- Define ValidationErrors as you see fit ValidationErrors Validate(TDataObjectList entities); // <- Define ValidationErrors as you see fit } public abstract BaseBusiness : IBaseBusiness { private TIDataAccess dataAccess; protected BaseBusiness(TIDataAccess dataAccess) { this.dataAccess = dataAccess; } } public interface IBaseDataAccess { IQueryable<TDataObject> Query(); IQueryable<TDataObject> FindBy(Expression<Func<T, bool>> predicate); void DeleteBy(Expression<Func<T, bool>> predicate); void Save(TDataObjectList entities); } } } namespace Application { public interface IBaseApplicationService {} public class BaseApplicationServiceWebAPIHost<TIApplicationService> : ApiController where TIApplicationService : IBaseApplicationService { private TIApplicationService applicationService; public BaseApplicationServiceWebAPIHost(TIApplicationService applicationService) { this.applicationService = applicationService; } } public class BaseApplicationServiceWCFHost<TIApplicationService> where TIApplicationService : IBaseApplicationService { private TIApplicationService applicationService; public BaseApplicationServiceWCFHost(TIApplicationService applicationService) { this.applicationService = applicationService; } } }
Use it like this:
UserDomain.dll
links: AcmeFramework.dll
namespace UserDomain { public class User : Entity<User, User.DataObject, User.DataObjectList, User.IBusiness, User.IDataAccess, Guid> { public class DataObject : BaseDataObject { public string FirstName; public string LastName; public bool IsActive { get; } } public class DataObjectList : BaseDataObjectList {} public interface IBusiness : IBaseBusiness { void DeactivateUser(Guid userId); } public interface IDataAccess : IBaseDataAccess {} public class Business { public Business(User.IDataAccess dataAccess) : base(dataAccess) {} public void DeactivateUser(Guid userId) { ... } } } public class UserPermission : Entity<UserPermission, UserPermission.DataObject, UserPermission.DataObjectList, UserPermission.IBusiness, UserPermission.IDataAccess, Guid> { public class DataObject : BaseDataObject { public Guid PermissionId; public Guid UserId; } public class DataObjectList : BaseDataObjectList {} public interface IBusiness : IBaseBusiness {} public interface IDataAccess : IBaseDataAccess {} public class Business { public Business(UserPermission.IDataAccess dataAccess) : base(dataAccess) {} } } public class Permission : Entity<Permission, Permission.DataObject, Permission.DataObjectList, Permission.IBusiness, Permission.IDataAccess, Guid> { public class DataObject : BaseDataObject { public string Code; public string Description; } public class DataObjectList : BaseDataObjectList {} public interface IBusiness : IBaseBusiness {} public interface IDataAccess : IBaseDataAccess {} public class Business { public Business(Permission.IDataAccess dataAccess) : base(dataAccess) {} } } }
UserManagementApplicationContracts.dll
links: AcmeFrameworkContracts.dll
namespace UserManagement.Contracts { public class ReviewUserResponse : Response<ReviewUserResponse> { public class UserPermission { public Guid id; public string permission; // <- formatted as "permissionCode [permissionDescription]" } public class User { public Guid id; public string firstName; public string lastName; public List<UserPermissions> permissions; } public User user; } public class EditUserResponse : Response<EditUserResponse> { public class Permission { public Guid id; public string permissionCode; public string description; } public class UserPermission { public Guid id; public Guid permissionId; } public class User { public Guid id; public string firstName; public string lastName; public List<UserPermissions> permissions; } public List<Permission> knownPermissions; public User user; } public interface IUserManagementApplicationService : IBaseApplicationService { Response EditUser(Guid userId); Response SaveUser(EditUserResponse.user user); Response ViewUser(Guid userId); } }
UserManagementApplicationImplementation.dll
: AcmeFramework.dll, AcmeFrameworkContracts.dll, UserManagementApplicationContracts.dll, UserDomain.dll
namespace UserManagement.Implementation { public class UserManagementApplicationService : IUserManagementApplicationService { private User.IBusiness userBusiness; private UserPermissions.IBusiness userPermissionsBusiness; private Permission.IBusiness permissionBusiness; public UserManagementApplicationService(User.IBusiness userBusiness, UserPermission.IBusiness userPermissionsBusiness, Permission.IBusiness permissionBusiness) { this.userBusiness = userBusiness; this.userPermissionsBusiness = userPermissionsBusiness; this.permissionBusiness = permissionBusiness; } public Response EditUser(Guid userId) { ... } public Response SaveUser(EditUserResponse.user user) { ... } public Response ViewUser(Guid userId) { ... } } }
You can add up to UOW if you are so addicted. You can also expose services and hosts, more logical entities, and not use a specific case, as you consider necessary, but remember to not place the domain schema classes in your contract library. Thus, you can develop contracts regardless of the underlying implementations. And you can subclass Entity<TEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess> into a domain contract library such as UserDomain.Contracts.dll and leave implementations in domain libraries such as UserDomain.dll if you want this isolation .