I’m building my first enterprise grade solution (at least I’m attempting to make it enterprise grade). I’m trying to follow best practice design patterns but am starting to worry that I might be going too far with abstraction.
I’m trying to build my asp.net webforms (in C#) app as an n-tier application. I’ve created a Data Access Layer using an XSD strongly-typed dataset that interfaces with a SQL server backend. I access the DAL through some Business Layer Objects that I’ve created on a 1:1 basis to the datatables in the dataset (eg, a UsersBLL class for the Users datatable in the dataset). I’m doing checks inside the BLL to make sure that data passed to DAL is following the business rules of the application. That’s all well and good. Where I’m getting stuck though is the point at which I connect the BLL to the presentation layer. For example, my UsersBLL class deals mostly with whole datatables, as it’s interfacing with the DAL. Should I now create a separate “User” (Singular) class that maps out the properties of a single user, rather than multiple users? This way I don’t have to do any searching through datatables in the presentation layer, as I could use the properties created in the User class. Or would it be better to somehow try to handle this inside the UsersBLL?
Sorry if this sounds a little complicated… Below is the code from the UsersBLL:
using System;
using System.Data;
using PedChallenge.DAL.PedDataSetTableAdapters;
[System.ComponentModel.DataObject]
public class UsersBLL
{
private UsersTableAdapter _UsersAdapter = null;
protected UsersTableAdapter Adapter
{
get
{
if (_UsersAdapter == null)
_UsersAdapter = new UsersTableAdapter();
return _UsersAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)]
public PedChallenge.DAL.PedDataSet.UsersDataTable GetUsers()
{
return Adapter.GetUsers();
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public PedChallenge.DAL.PedDataSet.UsersDataTable GetUserByUserID(int userID)
{
return Adapter.GetUserByUserID(userID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public PedChallenge.DAL.PedDataSet.UsersDataTable GetUsersByTeamID(int teamID)
{
return Adapter.GetUsersByTeamID(teamID);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, false)]
public PedChallenge.DAL.PedDataSet.UsersDataTable GetUsersByEmail(string Email)
{
return Adapter.GetUserByEmail(Email);
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Insert, true)]
public bool AddUser(int? teamID, string FirstName, string LastName,
string Email, string Role, int LocationID)
{
// Create a new UsersRow instance
PedChallenge.DAL.PedDataSet.UsersDataTable Users = new PedChallenge.DAL.PedDataSet.UsersDataTable();
PedChallenge.DAL.PedDataSet.UsersRow user = Users.NewUsersRow();
if (UserExists(Users, Email) == true)
return false;
if (teamID == null) user.SetTeamIDNull();
else user.TeamID = teamID.Value;
user.FirstName = FirstName;
user.LastName = LastName;
user.Email = Email;
user.Role = Role;
user.LocationID = LocationID;
// Add the new user
Users.AddUsersRow(user);
int rowsAffected = Adapter.Update(Users);
// Return true if precisely one row was inserted,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateUser(int userID, int? teamID, string FirstName, string LastName,
string Email, string Role, int LocationID)
{
PedChallenge.DAL.PedDataSet.UsersDataTable Users = Adapter.GetUserByUserID(userID);
if (Users.Count == 0)
// no matching record found, return false
return false;
PedChallenge.DAL.PedDataSet.UsersRow user = Users[0];
if (teamID == null) user.SetTeamIDNull();
else user.TeamID = teamID.Value;
user.FirstName = FirstName;
user.LastName = LastName;
user.Email = Email;
user.Role = Role;
user.LocationID = LocationID;
// Update the product record
int rowsAffected = Adapter.Update(user);
// Return true if precisely one row was updated,
// otherwise false
return rowsAffected == 1;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteUser(int userID)
{
int rowsAffected = Adapter.Delete(userID);
// Return true if precisely one row was deleted,
// otherwise false
return rowsAffected == 1;
}
private bool UserExists(PedChallenge.DAL.PedDataSet.UsersDataTable users, string email)
{
// Check if user email already exists
foreach (PedChallenge.DAL.PedDataSet.UsersRow userRow in users)
{
if (userRow.Email == email)
return true;
}
return false;
}
}
Some guidance in the right direction would be greatly appreciated!!
Thanks all!
Max
The sort of layering you’re trying for usually involves moving away from the DataTable approach to something that uses an instance for (roughly) each row in the database. In other words, the DAL would return either a single User or a collection of Users, depending on which static Load method you call. This means that all of the methods that take a bunch of parameters to represent the user would instead accept a User DTO.
A DAL for users would look something like this:
It’s static because it has no state.
(Arguably, it could have a database
connection as its state, but that’s
not a good idea in most cases, and
connection pooling removes any
benefit. Others might argue for a
singleton pattern.)
It operates at the level of the User
DTO class, not DataTable or any
other database-specific abstraction.
Perhaps the implementation uses a
database, perhaps it uses LINQ: the
caller need not know either way.
Note how it returns an IEnumerable
rather than committing to any
particular sort of collection.
It is concerned only with data access,
not business rules. Therefore, it should
be callable only from within a business
logic class that deals with users. Such
a class can decide what level of access
the caller is permitted to have, if any.
DTO stands for Data Transfer Object, which
usually amounts to a class containing just
public properties. It may well have a dirty
flag that is automatically set when properties
are changed. There may be a way to explictly
set the dirty flag, but no public way to clear
it. Also, the ID is typically read-only (so
that it can only be filled in from
deserialization).
The DTO intentionally does not contain business
logic that attempts to ensure correctness; instead,
the corresponding business logic class is what
contextually enforces rules. Business logic
changes, so if the DTO or DAL were burdened with
it, the violation of the single responsibility
principle would lead to disasters such as not
being able to deserialize an object because its
values are no longer considered legal.
The presentation layer can instantiate a User
object, fill it in and ask the business logic layer
to please call the Save method in the DAL. If the
BLL chooses to do this, it will fill in the ID and
clear the dirty flag. Using this ID, the BLL can
then retrieve persisted instances by calling the
DAL’s Load-by-ID method.
The DAL always has a Save method and a Load-by-ID
method, but it may well have query-based load methods,
such as the LoadByDiv example above. It needs to offer
whatever methods the BLL requires for efficient
operation.
The implementation of the DAL is a secret as far as
the BLL and above are concerned. If the backing is a
database, there would typically be stored procedures
corresponding to the various DAL methods, but this is
an implementation detail. In the same way, so is any
sort of caching.