Business Logic Framework - Tutorial 22:
Centralized business logic can have many great benefits for application development. As the applications are getting more and more complex with increasing demands for various interfaces, it becomes imperative to centralize all the logic. Without such an effort the business logic is not only scattered but also results in duplication of effort, wastage of time and resources.
Benefits of Centralized Business Logic:
- Reduced Application Development Time
- Shorter Change Cycles
- Centralized Maintenance of Logic and Rules
- Parallel Development of Application and Logic
- Consistency in the application of Business Logic and Rules
To purpose of this tutorial is to demonstrate how you can leverage the Business Logic Framework's capabilities in building centralized business logic layer. The tutorial's target is to customize the Users class, and embed the registration functionality into it and all the validation rules are automatically applied. Multiple interfaces can leverage the same functionality and the logic for registering new users. If this logic, validation and functionality was developed inside an user interface, any time the application needed the ability for the users to register via a different interface all the logic, validation and functionality will have to be duplicated. Such duplication can result in maintenance night mare in the long run. This tutorial covers the customization of a smart business entity, and being able to preserve your customizations through multiple code regeneration cycles.
The entity classes can be customized by adding custom code in the Custom class files (.NET 1.1) or adding partial class files (.NET 2.0 or later).
|
|
| private Users _user = null; | |
| public Users User | |
| { | |
| get | |
| { | |
| if (_user == null) | |
| { | |
| _user = new Users(); | |
| } | |
| return _user; | |
| } | |
| } | |
| private void Tutorial22_Leave(object sender, EventArgs e) | |
| { | |
| // Since the user is leaving this screen it is time to dispose of the object. | |
| if (_user != null) | |
| { | |
| _user.Dispose(); | |
| _user = null; | |
| } | |
| } | |
| private void btnSaveUser_Click(object sender, EventArgs e) | |
| { | |
| // We have encapsulated all the logic and validation requirements into our class by customizing the Users class. | |
| // Now the only thing we need to do is call the RegisterNewUser method and pass the user supplied values. | |
| // There is no need to perform any validation since the business object will automatically do that for us. | |
| // This method also gives you the flexibility and control to decide where you want to house your business logic, and | |
| // the same can then be reused by multiple types of user interfaces, web services or other points of integrations. | |
| // NOTE: Please refer to the "CustomizedUsers.cs|vb" file in the QuickObjects.ObjectBase.BusinessLogicLayer project. | |
| Users.MembershipStatus status = this.User.RegisterNewUser(this.txtUserName.Text, this.txtFirstName.Text, this.txtLastName.Text, this.txtSecurityQuestion.Text, this.txtSecurityAnswer.Text, this.txtEmail.Text, this.txtPassword.Text); | |
| if (status == Users.MembershipStatus.Success) | |
| { | |
| MessageBox.Show("Registration successful!"); | |
| } | |
| else | |
| { | |
| MessageBox.Show("Failed to create a new user!" + Environment.NewLine + "Reason:" + Environment.NewLine + this.User.ErrorString); | |
| } | |
| } | |
| Private _user As Users = Nothing | |
| Public ReadOnly Property User() As Users | |
| Get | |
| If _user Is Nothing Then | |
| _user = New Users() | |
| End If | |
| Return _user | |
| End Get | |
| End Property | |
| Private Sub Tutorial22_Leave(ByVal sender As Object, ByVal e As EventArgs) | |
| ' Since the user is leaving this screen it is time to dispose of the object. | |
| If _user IsNot Nothing Then | |
| _user.Dispose() | |
| _user = Nothing | |
| End If | |
| End Sub | |
| Private Sub btnSaveUser_Click(ByVal sender As Object, ByVal e As EventArgs) | |
| ' We have encapsulated all the logic and validation requirements into our class by customizing the Users class. | |
| ' Now the only thing we need to do is call the RegisterNewUser method and pass the user supplied values. | |
| ' There is no need to perform any validation since the business object will automatically do that for us. | |
| ' This method also gives you the flexibility and control to decide where you want to house your business logic, and | |
| ' the same can then be reused by multiple types of user interfaces, web services or other points of integrations. | |
| ' NOTE: Please refer to the "CustomizedUsers.cs|vb" file in the QuickObjects.ObjectBase.BusinessLogicLayer project. | |
| Dim status As Users.MembershipStatus = Me.User.RegisterNewUser(Me.txtUserName.Text, Me.txtFirstName.Text, Me.txtLastName.Text, Me.txtSecurityQuestion.Text, Me.txtSecurityAnswer.Text, Me.txtEmail.Text, _ | |
| Me.txtPassword.Text) | |
| If status = Users.MembershipStatus.Success Then | |
| MessageBox.Show("Registration successful!") | |
| Else | |
| MessageBox.Show("Failed to create a new user!" + Environment.NewLine + "Reason:" + Environment.NewLine + Me.User.ErrorString) | |
| End If | |
| End Sub |
All the registration process logic has been embedded in the Users class.
| public partial class Users : Users_Base | |
| { | |
| public enum MembershipStatus : int | |
| { | |
| Success = 0, | |
| InvalidUserName = 1, | |
| InvalidPassword = 2, | |
| InvalidQuestion = 3, | |
| InvalidAnswer = 4, | |
| InvalidEmail = 5, | |
| DuplicateUserName = 6, | |
| DuplicateEmail = 7, | |
| UserRejected = 8, | |
| InvalidProviderUserKey = 9, | |
| DuplicateProviderUserKey = 10, | |
| ProviderError = 11, | |
| ValidationFailed = 12, | |
| } | |
| private static int PASSWORD_HASHED = 1; | |
| // **************************************** | |
| // NOTE: For this sample We are using a feature called "partial classes" only available from .NET 2.0 onwards. | |
| // If you would like to implement this in .NET 1.1 you can use the generated | |
| // Customers class under the "Custom" folder. In that case it would be important for you to | |
| // NOT regenerate the "Custom" classes, or your changes will get overwritten. | |
| // Normally it is not necessary to regenerate "Custom" classes and it is only necessary add/remove | |
| // tables from the underlying schema. If you have customized code, please make sure to create a backup | |
| // before regenerating and merge your changes manually (Only applies to .NET 1.1). | |
| // **************************************** | |
| #region Object Initialization Code | |
| // Override the OnObjectInitialized method and it can be used | |
| // to further customize your business object after the object has been initialized. | |
| // For example: Below we will add validation | |
| // to the FirstName and LastName fields. | |
| protected override void OnObjectInitialized(ObjectEventArgs e) | |
| { | |
| // Make sure to call the base OnObjectInitialized method else the ObjectInitialized event handlers will not be fired. | |
| base.OnObjectInitialized(e); | |
| // Starting v3.x you are able to leverage the power of built in validation and | |
| // the following code shows how to add complex validation. | |
| // Built-in Validator Types: | |
| // BaseFieldValidator (act as the basic validator that can ensure that a field marked with AllowNull = false can not have IsNull set to true.) | |
| // RequiredFieldValidator - This is generic validator that can check any field for null values and ensure that any field (irrespective of AllowNull) does not have a null value. For StringField's it will check to see if the passed in value is empty, and in such case also the validation will fail. | |
| // RangeFieldValidator - This is a specialized validator that can check for range of values. Minimum or Maximum values can be explicitely specified or other field instances can be specified for obtaining such values at the time of validation. | |
| // RegularExpressionValidator - This validator provides you extreme control over how you want each field value validated to match certain regular expression. | |
| // CompareFieldValidator - This field validator can compare the field being validated against another field value and various types of comparison's can be specified based on what type of fields are being compared. | |
| // We are going to add a RequiredFieldValidator. This field validator | |
| // is not dependent on the IsNull property of the field, and hence you can | |
| // mark any field as required even if the field in the database allows nulls. | |
| // We are going to require that a UserName, FirstName, LastName and Email values must be specified in all Insert/Update operations. | |
| new RequiredFieldValidator(this.UserName, true); | |
| // If you need to do anything further with the validator then you can store a reference to the validator locally to further customize it (See below where we create an instance of RegularExpressionFieldValidator). | |
| // For RequiredFieldValidator we don't have to do anything, so we simply created a new instance and used the constructor overload that will | |
| // add the validator into the field's Validators collection. | |
| new RequiredFieldValidator(this.FirstName, true); | |
| new RequiredFieldValidator(this.LastName, true); | |
| new RequiredFieldValidator(this.Email, true); | |
| new RequiredFieldValidator(this.Password, true); | |
| new RequiredFieldValidator(this.SecurityAnswer, true); | |
| // By default the BaseFieldValidator can enforce a string's minimum and maximum length limits. | |
| // MaximumLength value is automatically generated from the field size in the database. However, we can specify the | |
| // MinimumLength to enforce that as well. | |
| // NOTE: If the BaseFieldValidator instance is removed from the Validators collection of the field then this minimum length will not be enforced. | |
| this.UserName.MinimumLength = 5; | |
| // We can set a custom error message that will be formatted and returned when the validator fails. | |
| // For formatting options please see the documentation for each validator's CustomErrorMessage property. | |
| // BaseFieldValidator will replace {0} with the name or the alias of the field being validated, and {4} with the minimum required length of the field (only applicable to StringField). | |
| this.UserName.Validators[ValidatorTypes.BaseFieldValidator].CustomErrorMessage = "{0} must be at least {4} characters long."; | |
| this.Email.MinimumLength = 6; | |
| this.Email.Validators[ValidatorTypes.BaseFieldValidator].CustomErrorMessage = "{0} must be at least {4} characters long."; | |
| this.Password.MinimumLength = 6; | |
| this.Password.Validators[ValidatorTypes.BaseFieldValidator].CustomErrorMessage = "{0} must be at least {4} characters long."; | |
| this.SecurityAnswer.MinimumLength = 6; | |
| // We want to ensure that the password and the SecurityAnswer are not the same. To ensure that we can add a compare field validator | |
| // that can compare the values of the Password and SecurityAnswer fields. | |
| CompareFieldValidator cmValidator = new CompareFieldValidator(this.SecurityAnswer, true); | |
| // We can specify the Comparison type of NotEqual. | |
| cmValidator.ComparisonType = CompareFieldValidator.ComparisonTypes.NotEqual; | |
| // Lets specify the field that will be compared. | |
| cmValidator.FieldToCompare = this.Password; | |
| // We can take it a step further and specify a CustomErrorMessage that will be formatted and returned in case the validation fails. | |
| cmValidator.CustomErrorMessage = "{0} value should not be the same as the value of {4}"; | |
| // We also want to ensure that a valid email address is being provided so we will create a | |
| // RegularExpressionFieldValidator and attach it to the Email field. | |
| RegularExpressionFieldValidator emValidator = new RegularExpressionFieldValidator(this.Email, true); | |
| emValidator.Expression = @"^[A-Za-z0-9_\-\.]+@(([A-Za-z0-9\-])+\.)+([A-Za-z\-])+$"; | |
| emValidator.CustomErrorMessage = "{0} value of {2} does not match the required pattern of an email address (person@domain.com)"; | |
| // We are specifying a RegularExpressionFieldValidator for the Password field. | |
| RegularExpressionFieldValidator pwValidator = new RegularExpressionFieldValidator(this.Password, true); | |
| pwValidator.Expression = @"^.*(?=.{6,})(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$"; | |
| pwValidator.CustomErrorMessage = "{0} must contain at least one upper case and one lower case letter along with numbers and special characters. Allowed Characters: @ # $ % ^ & + = "; | |
| } | |
| #endregion | |
| #region Create New User Code | |
| /// <summary> | |
| /// RegisterNewUser method will create a new user account. If Customer role exists in the database then the newly created user will be given the "Customer" role otherwise no role will be given. | |
| /// </summary> | |
| /// <param name="UserName"></param> | |
| /// <param name="FirstName"></param> | |
| /// <param name="LastName"></param> | |
| /// <param name="SecurityQuestion"></param> | |
| /// <param name="SecurityAnswer"></param> | |
| /// <param name="Email"></param> | |
| /// <param name="Password"></param> | |
| /// <returns></returns> | |
| public MembershipStatus RegisterNewUser( | |
| String UserName, | |
| String FirstName, | |
| String LastName, | |
| String SecurityQuestion, | |
| String SecurityAnswer, | |
| String Email, | |
| String Password | |
| ) | |
| { | |
| DateTime dt = Helper.RoundToSeconds(DateTime.UtcNow); | |
| try | |
| { | |
| // We set the business object's property values with values of passed in parameters. | |
| // Since we have set the ObjectMode to ObjectModes.Save all field's UseInSave properties will be automatically set to true. | |
| this.ObjectMode = ObjectModes.Save; | |
| this.UserName.Value = UserName; | |
| this.Password.Value = Password; | |
| this.FirstName.Value = FirstName; | |
| this.LastName.Value = LastName; | |
| this.Email.Value = Email; | |
| this.SecurityQuestion.Value = SecurityQuestion; | |
| this.SecurityAnswer.Value = SecurityAnswer; | |
| this.PasswordFormat.Value = PASSWORD_HASHED; | |
| this.LastLockoutDate.Value = DateTimeField.SqlMinimumDate; | |
| this.LastLoginDate.Value = dt; | |
| this.LastActivityDate.Value = dt; | |
| this.LastPasswordChangedDate.Value = dt; | |
| this.IsActive.Value = true; | |
| this.IsLockedOut.Value = false; | |
| this.RequirePasswordChange.Value = false; | |
| this.FP_AnswerAttemptCount.Value = 0; | |
| this.FP_AnswerAttemptWindowStart.Value = DateTimeField.SqlMinimumDate; | |
| this.FP_AttemptCount.Value = 0; | |
| this.FP_AttemptWindowStart.Value = DateTimeField.SqlMinimumDate; | |
| // To ensure that these values pass our validation criteria | |
| // Lets check the value of IsValid property of the business object. | |
| if (!this.IsValid) | |
| { | |
| // Since the object values do not conform to the requirements we can return the ValidationFailed status code. | |
| return MembershipStatus.ValidationFailed; | |
| } | |
| else | |
| { | |
| // Now that the validation has passed, lets create a MD5 hash of the password and store the salt and the password. | |
| // MD5 hash is an effective technique of protecting passwords since the passwords can not be reverse engineered. | |
| this.PasswordSalt.Value = Helper.GenerateSalt(); | |
| this.Password.Value = Helper.EncodePassword(this.Password.Value, this.PasswordSalt.Value); | |
| // Lets do the same for SecurityAnswer | |
| this.SecurityAnswer.Value = Helper.TrimValue(this.SecurityAnswer.Value); | |
| this.SecurityAnswer.Value = Helper.EncodePassword(this.SecurityAnswer.Value.ToLower(CultureInfo.InvariantCulture), this.PasswordSalt.Value); | |
| } | |
| // The following line ensure that if either one of the values is found in the database FindAndLoad will return true. | |
| this.MoreResults = true; | |
| this.Email.Value = Email; | |
| this.Email.UseInSearch = true; | |
| this.UserName.Value = UserName; | |
| this.UserName.UseInSearch = true; | |
| // We don't want to select all the fields hence we can set the visibility of the fields to false. | |
| this.UseAllFieldsForDisplay(false); | |
| // We need at least one field visible so lets set the UserID's Visible property to true. | |
| this.UserID.Visible = true; | |
| // FindAndLoad method works just like Find method but Loads the values of the first found row. If the query returned more than one row the rest of the rows are | |
| // still available in the ResultSet. | |
| if (!this.FindAndLoad()) | |
| { | |
| // Since FindAndLoad returned false, that means there is no user record that matches either the Email or the UserName. | |
| // The following is used to ensure that any database commands from this point onwards will be running inside a transaction. | |
| this.UseTransaction = true; | |
| // Inser the user record | |
| if (this.Insert()) | |
| { | |
| // Since the user record was inserted successfully, lets check the database for a Role called "Customer" | |
| // By calling the Join_UserID_UserRoles_Child method we are automatically creating an instance of type UserRoles and sharing the connection and transaction both object instances. | |
| this.Join_UserID_UserRoles_Child(); | |
| // Lets extend it further to the Roles object instance by calling the Join_RoleID_Roles_Parent method. | |
| this.UserID_UserRoles_Child.Join_RoleID_Roles_Parent(); | |
| // Each of these instances can be used independently of each other. | |
| // Here we will use the this.UserID_UserRoles_Child.RoleID_Roles_Parent instance to search for "Customer" role. | |
| this.UserID_UserRoles_Child.RoleID_Roles_Parent.ObjectMode = ObjectModes.Search; | |
| this.UserID_UserRoles_Child.RoleID_Roles_Parent.RoleName.Value = "Customer"; | |
| // Calling the FindAndLoad method on the Roles object instances stored at RoleID_Roles_Parent will now return true if the database | |
| // does contain a record where RoleName is Customer. | |
| if (this.UserID_UserRoles_Child.RoleID_Roles_Parent.FindAndLoad()) | |
| { | |
| // Since we have found the RoleID lets save the UserRoles record that will grant the Customer Role to the User. | |
| this.UserID_UserRoles_Child.ObjectMode = ObjectModes.Save; | |
| // Since the RoleID value of the this.UserID_UserRoles_Child.RoleID_Roles_Parent has already been loaded | |
| // we can simply assign it. | |
| this.UserID_UserRoles_Child.RoleID.Value = this.UserID_UserRoles_Child.RoleID_Roles_Parent.RoleID.Value; | |
| this.UserID_UserRoles_Child.UserID.Value = this.UserID.Value; | |
| this.UserID_UserRoles_Child.IsActive.Value = true; | |
| if (this.UserID_UserRoles_Child.Insert()) | |
| { | |
| // Since the UserRole was inserted successfully, lets call the CommitTransaction method to commit the active transaction. | |
| this.CommitTransaction(); | |
| return MembershipStatus.Success; | |
| } | |
| else | |
| { | |
| // Since the Insert failed on this.UserID_UserRoles_Child instance, lets capture the ErrorString property. | |
| // The reason to copy error is just to make it easy for the consuming code to simply use the Users object instance's ErrorString property without having to lookat any other instances. | |
| this.ErrorString = this.UserID_UserRoles_Child.ErrorString; | |
| // Since the UserRole record was not created its time to rollback the transaction. | |
| this.RollBackTransaction(); | |
| return MembershipStatus.ProviderError; | |
| } | |
| } | |
| else | |
| { | |
| // Since there is no role in the database called "Customer" we can simply keep the User record without the role assignment. | |
| // NOTE: This is just as a demonstration, and the actual code or logic will depend on your business requirements. | |
| this.CommitTransaction(); | |
| return MembershipStatus.Success; | |
| } | |
| } | |
| else | |
| { | |
| // Since the user record insert failed, lets rollback the transaction. | |
| this.RollBackTransaction(); | |
| // We can check the reason for the insert to fail by checking the IsValid property to ensure that all field values are valid. | |
| if (!this.IsValid) | |
| { | |
| return MembershipStatus.ValidationFailed; | |
| } | |
| else | |
| { | |
| return MembershipStatus.ProviderError; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Since either Email or UserName has already been used we can find out which one has already been used by doing the following. | |
| // If we set the UseInSearch of Email to false then only the UserName field will be used to search. | |
| this.Email.UseInSearch = false; | |
| System.Collections.ArrayList ar = new System.Collections.ArrayList(); | |
| string sql = this.GetSelectSQL(ref ar); | |
| string s = sql; | |
| if (this.FindAndLoad()) | |
| return MembershipStatus.DuplicateUserName; | |
| else | |
| return MembershipStatus.DuplicateEmail; | |
| } | |
| } | |
| catch | |
| { | |
| throw; | |
| } | |
| } | |
| #endregion | |
| #region Change User Password Code | |
| public MembershipStatus ChangeUserPassword(string username, string oldPassword, string newPassword) | |
| { | |
| try | |
| { | |
| // By setting the MoreResults to false we ensure that the all the specified conditions will be met by the FindAndLoad method. | |
| this.MoreResults = false; | |
| this.ObjectMode = ObjectModes.Search; | |
| this.UserName.Value = username; | |
| this.IsActive.Value = true; | |
| if (this.FindAndLoad()) | |
| { | |
| // After the FindAndLoad method the original salt value can be retreive from the PasswordSalt field's Value property. | |
| string pass = Helper.EncodePassword(oldPassword, this.PasswordSalt.Value); | |
| // If the currently encoded password does not match the stored password then the user supplied incorrect password. | |
| if (this.Password.Value != pass) | |
| { | |
| return MembershipStatus.InvalidPassword; | |
| } | |
| // Now that we know that the user supplied the correct "old" password so now we can update the User record with the new password. | |
| this.ObjectMode = ObjectModes.Save; | |
| this.Password.Value = newPassword; | |
| this.LastPasswordChangedDate.Value = DateTime.UtcNow; | |
| // Lets make sure that the Password is valid. IsValid property on the business object will return true if all the fields about to be updated (or inserted in case of Insert) | |
| // are valid or not. | |
| if (!this.IsValid) | |
| { | |
| return MembershipStatus.ValidationFailed; | |
| } | |
| else | |
| { | |
| // Password is valid, so lets encode the password before saving. | |
| this.Password.Value = Helper.EncodePassword(this.Password.Value, this.PasswordSalt.Value); | |
| } | |
| // Now we simply call the Update method to change the password and the LastPasswordChangedDate values. | |
| if (this.Update()) | |
| { | |
| return MembershipStatus.Success; | |
| } | |
| else | |
| { | |
| return MembershipStatus.ProviderError; | |
| } | |
| } | |
| else | |
| { | |
| return MembershipStatus.ProviderError; | |
| } | |
| } | |
| catch | |
| { | |
| throw; | |
| } | |
| //return MembershipStatus.ProviderError; | |
| } | |
| #endregion | |
| #region CheckLogin code | |
| /// <summary> | |
| /// CheckLogin validates the authentication information against the information stored in the database. | |
| /// </summary> | |
| /// <param name="username"></param> | |
| /// <param name="password"></param> | |
| /// <returns></returns> | |
| public MembershipStatus CheckLogin(string username, string password) | |
| { | |
| try | |
| { | |
| // To ensure that the FindAndLoad method will only load the record if the UserName and the IsActive values match the specified values. | |
| this.MoreResults = false; | |
| // Here we are demonstrating an alternate way of performing searches. Instead of setting the ObjectMode to ObjectModes.Search we are setting | |
| // the UseInSearch properties of each individual field. End result will be the same. | |
| this.UserName.Value = username; | |
| this.UserName.UseInSearch = true; | |
| &nbs |

If you use the Custom class files generated by the Quick Objects designer, you must not regenerate those files or your customizations will be lost. In case you really need to regenerate the Custom classes (only if you added/removed tables or changed names), you can create a back up of the custom classes before regenerating and manually merge them.
If you are developing in .NET 2.0 or later please use the partial class feature and create a new file with your custom code, that way regeneration will not affect your code at all.