My business objects are coded with the following architecture:
- validation of any incoming data throws an exception in the setter if it doesn't fit business logic.
- property can not be corrupt/inconsistent state unless the existing default/null is invalid
- business objects can only be created by the business module via a static factory type method that accepts an interface implementation that is shared with the business object for copying into the business object.
- Enforces that the dependency container, ui, and persistence layers can not create an invalid Model object or pass it anywhere.
- This factory method catches all the different validation exceptions in a validation dictionary so that when the validation attempts are complete, the dictionary the caller provided is filled with field names and messages, and an exception is thrown if any of the validations did not pass.
- easily maps back to UI fields with appropriate error messages
- No database/persistence type methods are on the business objects
- needed persistence behaviors are defined via repository interfaces in the business module
Sample Business object interface:
public interface IAmARegistration
{
string Nbk { get; set; } //Primary key?
string Name { get; set; }
string Email { get; set; }
string MailCode { get; set; }
string TelephoneNumber { get; set; }
int? OrganizationId { get; set; }
int? OrganizationSponsorId { get; set; }
}
business object repository interface:
/// <summary>
/// Handles registration persistance or an in-memory repository for testing
/// requires a business object instead of interface type to enforce validation
/// </summary>
public interface IAmARegistrationRepository
{
/// <summary>
/// Checks if a registration record exists in the persistance mechanism
/// </summary>
/// <param name="user">Takes a bare NBK</param>
/// <returns></returns>
bool IsRegistered(string user); //Cache the result if so
/// <summary>
/// Returns null if none exist
/// </summary>
/// <param name="user">Takes a bare NBK</param>
/// <returns></returns>
IAmARegistration GetRegistration(string user);
void EditRegistration(string user,ModelRegistration registration);
void CreateRegistration(ModelRegistration registration);
}
Then an actual business object looks as follows:
public class ModelRegistration : IAmARegistration//,IDataErrorInfo
{
internal ModelRegistration() { }
public string Nbk
{
get
{
return _nbk;
}
set
{
if (String.IsNullOrEmpty(value))
throw new ArgumentException("Nbk is required");
_nbk = value;
}
}
... //other properties omitted
public static ModelRegistration CreateModelAssessment(IValidationDictionary validation, IAmARegistration source)
{
var result = CopyData(() => new ModelRegistration(), source, false, null);
//Any other complex validation goes here
return result;
}
/// <summary>
/// This is validated in a unit test to ensure accuracy and that it is not out of sync with
/// the number of members the interface has
/// </summary>
public static Dictionary<string, Action> GenerateActionDictionary<T>(T dest, IAmARegistration source, bool includeIdentifier)
where T : IAmARegistration
{
var result = new Dictionary<string, Action>
{
{Member.Name<IAmARegistration>(x=>x.Email),
()=>dest.Email=source.Email},
{Member.Name<IAmARegistration>(x=>x.MailCode),
()=>dest.MailCode=source.MailCode},
{Member.Name<IAmARegistration>(x=>x.Name),
()=>dest.Name=source.Name},
{Member.Name<IAmARegistration>(x=>x.Nbk),
()=>dest.Nbk=source.Nbk},
{Member.Name<IAmARegistration>(x=>x.OrganizationId),
()=>dest.OrganizationId=source.OrganizationId},
{Member.Name<IAmARegistration>(x=>x.OrganizationSponsorId),
()=>dest.OrganizationSponsorId=source.OrganizationSponsorId},
{Member.Name<IAmARegistration>(x=>x.TelephoneNumber),
()=>dest.TelephoneNumber=source.TelephoneNumber},
};
return result;
}
/// <summary>
/// Designed for copying the model to the db persistence object or ui display object
/// </summary>
public static T CopyData<T>(Func<T> creator, IAmARegistration source, bool includeIdentifier,
ICollection<string> excludeList) where T : IAmARegistration
{
return CopyDictionary<T, IAmARegistration>.CopyData(
GenerateActionDictionary, creator, source, includeIdentifier, excludeList);
}
/// <summary>
/// Designed for copying the ui to the model
/// </summary>
public static T CopyData<T>(IValidationDictionary validation, Func<T> creator,
IAmARegistration source, bool includeIdentifier, ICollection<string> excludeList)
where T : IAmARegistration
{
return CopyDictionary<T, IAmARegistration>.CopyData(
GenerateActionDictionary, validation, creator, source, includeIdentifier, excludeList);
}
Sample repository method that I'm having trouble writing isolated tests for:
public void CreateRegistration(ModelRegistration registration)
{
var dbRegistration = ModelRegistration.CopyData(()=>new Registration(), registration, false, null);
using (var dc=new LQDev202DataContext())
{
dc.Registrations.InsertOnSubmit(dbRegistration);
dc.SubmitChanges();
}
}
Issues:
- When a new member is added there are a minimum of 8 places a change must be made (db, linq-to-sql designer, model Interface, model property, model copy dictionary, ui, ui DTO, unit test
- Testability
- testing the db methods that are hard coded to depend on an exact type that has no public default constructor, and needs to pass through another method, makes testing in isolation either impossible, or will need to intrude on the business object to make it more testable.
- Using InternalsVisibleTo so that the BusinessModel.Tests has access to the internal contructor, but I would need to add that for any other persistence layer testing module, making it scale very poorly
- to make the copy functionality generic the business objects were required to have public setters
- I'd prefer if the model objects were immutable
- DTOs are required for the UI to attempt any data validation
I'm shooting for complete reusability of this business layer with other persistence mechanisms and ui mechanisms (windows forms, asp.net, asp.net mvc 1, etc...). Also that team members can develop against this business layer/architecture with a minimum of difficulty.
Is there a way to enforce immutable validated model objects, or enforce that neither the ui or persistance layer can get a hold of an invalid one without these headaches?