CSLA.Server.DataPortal

At this point, the client-side DataPortal is complete. We can move on to create the nontransactional and transactional server-side DataPortal classes, which reside in a pair of new assemblies. In fact, it turns out that the transactional DataPortal is the simpler of the two, since it's designed simply to run in Enterprise Services to start the transactional processing and then to delegate all its method calls into the nontransactional DataPortal . We discussed this in Chapter 2 when we designed the DataPortal mechanism.

What this means is that all the real work of the server-side DataPortal has to be written only once, in the Server.DataPortal class. From there, it can be called directly by the client-side DataPortal , or it can be called by the transactional DataPortal after the transactional context has been set up. Figure 5-6 shows how everything is related .

image from book
Figure 5-6: Relationship between client-side and server-side DataPortal objects
Tip  

The magic here is that the regular server-side DataPortal will automatically run within the COM+ transactional context when it's created by the transactional DataPortal . We discussed how and why this works in Chapter 3.

When called directly from the client, Server.DataPortal won't run within a COM+ transaction, so our data access code in the business object will (if necessary) implement its own transactions using ADO.NET or stored procedures. This is ideal when we're updating tables within the same database, since our code will run about twice as fast using manual transactions as it will using Enterprise Services.

When called from the transactional DataPortal , however, Server.DataPortal will be loaded into the COM+ transaction context. In this case, our business object's data access code can rely on COM+ to handle the transactional details ”we just need to raise an error if we want the transaction to fail. This is ideal when we're updating tables in multiple databases, since Enterprise Services will ensure that all the database updates are combined into one larger distributed transaction.

The Server.DataPortal class is relatively complex. For any data access request, it needs to perform a series of steps that ultimately culminate in the use of reflection to invoke the appropriate method on the business object itself. The server-side DataPortal must

  • Impersonate the client security (if using our custom, table-based security).

  • Create an instance of the business object (for the create, retrieve, and delete operations).

  • Call the appropriate method on the business object.

  • Return any result to the client-side DataPortal .

Let's implement this functionality now.

Server.DataPortal

The server-side DataPortal has four primary methods that are called by the client-side DataPortal , as shown in Table 5-3.

Table 5-3: Methods Exposed by the Server-Side DataPortal

Method

Description

Create()

Invokes the business object's DataPortal_Create() method

Fetch()

Invokes the business object's DataPortal_Fetch() method

Update()

Invokes the business object's DataPortal_Update() method

Delete()

Invokes the business object's DataPortal_Delete() method

Within each of these methods, we need to perform the steps just outlined. As is our tradition, we'll create a set of helper functions to do the hard work, so the code in each of the four methods will be relatively straightforward.

Creating the Assembly

The server-side DataPortal resides in its own assembly. This is because we may or may not want to deploy the code to the client, so we don't want it to be part of the CSLA assembly itself. Add a new Class Library project to our solution, and name it CSLA.Server.DataPortal .

We need to give the assembly a strong name. In Chapter 3, we discussed the use of Enterprise Services and how any assembly loaded into Enterprise Services required a strong name. It turns out that any assembly referenced by an assembly loaded into Enterprise Services also requires a strong name. We know that we'll be referencing Server.DataPortal from our transactional DataPortal , which will be running in Enterprise Services. Therefore, the current assembly also needs a strong name.

To give an assembly a strong name, we need a key file. If you already have a key file for your organization, then use that. Otherwise, you can use the sn.exe command-line utility to create a key file, as discussed in Chapter 3. Then, open the AssemblyInfo.cs file and add the following code (changing the path to point to your particular key file):

  [assembly: AssemblyKeyFile(@"c:\mykey.snk")]  

The next time the assembly is built, it will have a strong name, so it can be referenced by our transactional DataPortal project as it runs within Enterprise Services. That means we're all set to start writing code.

Moving the ConfigurationSettings Class

Earlier in the chapter we created a ConfigurationSettings class in the CSLA assembly. We'll need access to that class here in the CSLA.Server.DataPortal assembly as well. Later in the chapter we'll discover that CSLA will reference CSLA.Server.DataPortal , so if we move the ConfigurationSettings class to our new assembly it will be available to all our framework code.

To do this, just drag and drop the ConfigurationSettings file in Solution Explorer from the CSLA project to the new CSLA.Server.DataPortal project.

Creating the DataPortal Class

At this point, we can create the DataPortal class itself. The CSLA.Server.DataPortal project started with a default class called Class1 , so rename Class1.cs to DataPortal.cs , and we can start coding. First, change the class name:

  using System; namespace CSLA.Server {   public class DataPortal   {   } }  

The whole point of the server-side DataPortal is to have an object that will run on the server or, more accurately, have an object that's anchored to the server, as we discussed in Chapter 1. To create an anchored object, we inherit from MarshalByRefObject :

  public class DataPortal : MarshalByRefObject  

This ensures that the Server.DataPortal object, once created, will always run on the same machine.

Helper Functions

Now let's write the helper functions that will do the hard work. In essence, our code needs to perform three basic operations. It needs to set up security, it needs to create the business object (if appropriate), and it needs to call a method on the business object. Let's deal with each of those operations in order.

Handling Security

If the project is configured to use our custom table-based security, the method on the server-side DataPortal will be passed a principal object as a parameter from the client-side DataPortal . To ensure that our security context is the same as the client's, we need to set this value into our thread's CurrentPrincipal property.

On the other hand, if we're using Windows' integrated security, we won't do anything special. The assumption is that the security configuration on both client and server has automatically handled impersonation of the user 's identity on our behalf . Before we get started, let's reference the appropriate .NET security namespace:

  using System.Security.Principal;  

The first thing we need to do is retrieve the security setting from the application configuration file. Then we can write a function to return the configuration value:

  #region Security    private string AUTHENTICATION    {      get      {        return ConfigurationSettings.AppSettings["Authentication"];      }    }    #endregion  
Tip  

Keep in mind that the .NET runtime automatically reads and caches the configuration file on its first access. This means that we aren't really reading the file each time we check this value ”we're simply retrieving the value from .NET's in-memory cache.

This is the same configuration value that we specified when creating the client-side DataPortal to control the type of security we're using. An entry in the configuration file to make our application use the custom, table-based security might appear as

 <add key="Authentication" value="CSLA" /> 
Tip  

Technically this isn't required, because CSLA security is the default. However, it's good practice to include the specific setting in the configuration file for documentation purposes. By putting this line in the application configuration file, we clearly indicate that we're expecting to use CSLA security, rather than Windows' integrated security.

Assuming that we're using our custom security, the real work happens in the SetPrincipal() method. We're passed a principal object that contains the user's identity from the client. This object needs to be placed into our thread's CurrentPrincipal value. Add the following code to the same region:

  private void SetPrincipal(object principal)    {      if(AUTHENTICATION == "Windows")      {        // when using integrated security, Principal must be Nothing        // and we need to set our policy to use the Windows principal        if(principal == null)        {          AppDomain.CurrentDomain.SetPrincipalPolicy(                                            PrincipalPolicy.WindowsPrincipal);          return;        }        else          throw new System.Security.SecurityException(            "No principal object should be passed to DataPortal " +            "when using Windows integrated security");      }      else      {        System.Diagnostics.Debug.Assert(AUTHENTICATION.Length > 0,          "No AUTHENTICATION token found in config file");      }      // we expect Principal to be of type BusinessPrincipal, but      // we can't enforce that since it causes a circular reference      // with the business library so instead we must use type Object      // for the parameter, so here we do a check on the type of the      // parameter      if(principal.ToString() == "CSLA.Security.BusinessPrincipal")      {        // see if our current principal is        // different from the caller's principal        if(!ReferenceEquals(principal, System.Threading.Thread.CurrentPrincipal))      {        // the caller had a different principal, so change ours to        // match the caller's so all our objects use the caller's        // security        System.Threading.Thread.CurrentPrincipal = (IPrincipal)principal;      }    }    else      throw new System.Security.SecurityException(        "Principal must be of type BusinessPrincipal, not " +        principal.ToString());    }  

The first thing we do here is to determine whether we're using Windows or CSLA security. If we're using Windows security, we make sure that we weren't passed a principal object by mistake. This is done primarily to simplify debugging, since this would indicate that the client-side configuration is different from the server-side configuration:

 throw new System.Security.SecurityException(            "No principal object should be passed to DataPortal " +            "when using Windows integrated security"); 

Assuming that we're using CSLA security, we then need to make sure that the principal object that was passed as a parameter is our specific type of principal object (which we'll create later in the chapter) ”it will be called CSLA.Security.BusinessPrincipal . Now, normally we'd check its type with code such as

 if(Principal is CSLA.Security.BusinessPrincipal) 

However, this would require that our CSLA.Server.DataPortal assembly have a reference to the CSLA assembly, and we already know that the CSLA assembly needs a reference to CSLA.Server.DataPortal . This type of circular reference between assemblies isn't legal and will prevent our code from compiling properly.

To avoid this issue, we can instead compare the text data type of the object to a string value, achieving the same result without requiring a reference back to the CSLA assembly:

 if(principal.ToString() == "CSLA.Security.BusinessPrincipal") 

At this point, we know we're using CSLA security and that we have the right type of principal object. We can move on to make it our CurrentPrincipal . First, we make sure that it isn't already the current value:

 if(!ReferenceEquals(principal, System.Threading.Thread.CurrentPrincipal)) 

This can happen in two scenarios. If Server.DataPortal is running in the client process, then obviously it will already have the same principal object. It's also possible that Server.DataPortal is running via remoting, and by chance we've already made a data access call that set the security value to our value. This can happen only if no other users have used that same server thread in the meantime, since they obviously would have changed the value to match their own principal object.

If we make it through all those checks, the final step is to set our principal object as "current":

 System.Threading.Thread.CurrentPrincipal = (IPrincipal)principal; 

The SetPrincipal() method can be called by each of our four data access methods before doing any other work, thus ensuring that the server's security context is the same as the client's.

In Chapter 7, we'll see how security is used within our business objects, and in Chapters 8 through 10 we'll see how it's used in the UI. In each case, the business developer will use standard .NET Framework techniques to access security information about the user. What we're doing here takes place entirely behind the scenes.

Creating the Business Object

Create, retrieve, and delete operations must all create a new business object on the server. (For an update operation, we are passed a preexisting business object from the client, so we don't need to worry about it.) Of course, creating an object is typically very straightforward ”we just use the new keyword:

 BusinessObject obj = new BusinessObject(); 

However, as we discussed in Chapter 2, we'll be using the class-in-charge model for our business objects. This means that the creation, retrieval, or deletion of an object will be handled through a static method on the business class.

What we're really doing is utilizing an object factory design pattern, where the object isn't created directly by the client code; rather, it's created by a factory and returned to the client code. This scheme protects the client code (the UI code in our case) from knowing exactly how the object was created ”it just happens.

To ensure that the UI code doesn't directly create a business object, all of our business objects will have a private constructor:

 private BusinessObject()   {     // Prevent direct creation   } 

With this constructor, we effectively prevent the UI code from using the new keyword to create an instance of our object, forcing the code to call our static factory methods instead. Of course, this would also appear to prevent our server-side DataPortal code from creating an instance of the object, but luckily we can use reflection to get around the problem.

Tip  

Obviously, the UI developer could also use reflection to create an instance of our object directly. There's no practical way to prevent a developer from going out of her way to break the rules ”at some point, we have to assume that the UI developer isn't malicious and won't go out of her way to misuse our objects.

We discussed reflection in Chapter 3. Before we use it here, we should import its namespace:

 using System;  using System.Reflection;  using System.Security.Principal; 

After all that, the CreateBusinessObject() method itself isn't too complex:

  #region Creating the business object    private object CreateBusinessObject(object criteria)    {      // get the type of the actual business object      Type businessType = criteria.GetType().DeclaringType;      // create an instance of the business object      return Activator.CreateInstance(businessType, true);    }    #endregion  

Since this method will be used only for create, retrieve, and delete operations, we'll always have a Criteria object passed as a parameter from the client-side DataPortal . We can use it to find the data type of the actual business class:

 Type businessType = criteria.GetType().DeclaringType; 

Given a Type object corresponding to the business class itself, we can then use reflection to create an instance of the class:

 return Activator.CreateInstance(businessType, true); 

The second parameter to CreateInstance() indicates that we want to create the object even if it has only a private constructor.

Calling a Method

The final operation our code needs to perform is to call the appropriate DataPortal_xyz() method on the business object itself. Normally, calling a method is trivial, but in this case we know that the DataPortal_xyz() methods on our business objects are non- public . Typically, they are protected in scope, meaning that they can't be called through conventional means.

This is intentional, since we don't want the UI developer (or some other business object) to call DataPortal_xyz() methods on our business object ”only the server-side DataPortal should call these methods. By making them non- public , we effectively make them unavailable for accidental invocation, but we do need a way for the server-side DataPortal code to invoke them. Again, reflection comes to the rescue.

The first thing we need to call a method through reflection is a MethodInfo object that describes the method. We can get a MethodInfo object for a method by using reflection, just as we did for the client-side data portal:

  #region Calling a method    MethodInfo GetMethod(Type objectType, string method)    {      return objectType.GetMethod(method,        BindingFlags.FlattenHierarchy         BindingFlags.Instance         BindingFlags.Public         BindingFlags.NonPublic);    }    #endregion  

We can now create a function to call the method on the business object by using our GetMethod() function:

  object CallMethod(object obj, string method, params object[] p)    {      // call a private method on the object      MethodInfo info = GetMethod(obj.GetType(), method);      object result;      try      {        result = info.Invoke(obj, p);      }      catch(System.Exception e)      {        throw e.InnerException();      }      return result;    }  

First, we get the MethodInfo object corresponding to the method by calling our GetMethod() function:

 MethodInfo info = GetMethod(obj.GetType(), method); 

Then we attempt to invoke the method in a try catch block:

 try    {      result = info.Invoke(obj, p);    }    catch(System.Exception e)    {      throw e.InnerException();    } 

If there's an error, all we do to process it is to raise another error. This is of key importance, however, because the error we'll get as a result of calling the Invoke() method on a MethodInfo object is not the error that occurred within the method itself (if any).

The error we'll get from an Invoke() call is to tell us that the Invoke() call failed! This means that the Exception object we get describing the error is virtually useless, but thankfully Exception objects can contain other Exception objects, and that's what happens here.

The Exception object we get to tell us that Invoke() failed contains a base Exception object that represents the real error that occurred within the function that was invoked. All we're doing in our code is catching the first error and then raising the more meaningful error it contains. The end result is that the calling code gets an error that contains information about whatever caused the code in the method itself to fail ”exactly the same error that would have been returned had the method been invoked using normal means rather than reflection.

Data Access Methods

At this point, we have all the helper functions we need, and we can implement the four core data access methods that are called by the client-side DataPortal .

Create

The Create() method will set security, and then create an instance of the business object and call its DataPortal_Create() method. The newly created and populated business object is then returned to the client as a result:

  #region Data Access    public object Create(object criteria, object principal)    {      SetPrincipal(principal);      // create an instance of the business object      object obj = CreateBusinessObject(criteria);      // tell the business object to fetch its data      CallMethod(obj, "DataPortal_Create", criteria);      // return the populated business object as a result      return obj;    }    #endregion  

Since we did all the hard work in the helper functions, this code should be straightforward and easy to read.

Note that this code contains no error handling ”this is intentional. The only reason to catch an exception is if we can do something about it or if we want to wrap it within another exception of our own making. Within this method, there's nothing we can do to resolve or address an exception that the business object throws as part of its data access routine. Also, there's little value to be added by wrapping the exception within a new exception of our own making.

In fact, were we to wrap the exception within some new exception, we'd reduce the business developer's ability to work easily with the exception in his business code. Right now, we can't predict the type of exception that might be raised by the business object's data access. We might get a generic Exception , an ApplicationException , a SecurityException , or one of any number of others, depending on exactly what went wrong.

If we wrap the detailed exception into some generic exception that we rethrow from within our code, then the poor business developer will be unable to utilize structured error handling to handle the detailed exceptions. Right now, the business developer could write something like this in his static factory method:

 public static Resource NewResource(string id)   {     try     {       return DataPortal.Create(new Criteria(id));     }     catch(SecurityException ex)     {       // Deal with the security exception     }     catch(SqlException ex)     {       // Deal with the SQL data exception     }     catch(Exception ex)     {       // Deal with the generic exception     }   } 

But if we catch and rethrow the exception in the server-side DataPortal code, then the business developer loses the ability to differentiate between the various types of exception object that might have been thrown by the underlying data access code.

Fetch

The Fetch() method is similar to the Create() method. It also sets security and creates a business object, but then it calls its DataPortal_Fetch() method. The resulting fully populated business object is returned as a result:

  public object Fetch(object criteria, object principal)     {       SetPrincipal(principal);       // create an instance of the business object       object obj = CreateBusinessObject(criteria);       // tell the business object to fetch its data       CallMethod(obj, "DataPortal_Fetch", criteria);       // return the populated business object as a result       return obj;     }  

As with the Create() method, the business object will be returned as a simple object reference if the server-side DataPortal is running in-process, or it will be copied by value across the network to the client if the server-side DataPortal is running on a separate server via remoting. Those details are handled automatically by .NET on our behalf.

Update

The Update() method is a bit simpler, since it gets passed the business object, rather than a Criteria object, as a parameter. All it needs to do is set security and then call DataPortal_Update() on the object:

  public object Update(object obj, object principal)     {       SetPrincipal(principal);       // tell the business object to update itself       CallMethod(obj, "DataPortal_Update");       return obj;     }  

Remember that DataPortal_Update() might very well alter the object's data, so we need to return the object in order that the client gets that updated data. If the server-side DataPortal is running in-process, this isn't strictly necessary, but it's a requirement if we're using remoting. By always coding for the more complex case, we ensure that our framework will operate just fine in either case.

Delete

Finally, we can implement the Delete() method. It sets security, creates an instance of the business object, and then calls the DataPortal_Delete() method:

  public void Delete(object criteria, object principal)     {       SetPrincipal(principal);       // create an instance of the business object       object obj = CreateBusinessObject(criteria);       // tell the business object to delete itself       CallMethod(obj, "DataPortal_Delete", criteria);     }  

Since there's no data resulting from a delete operation, this routine doesn't return anything as a result. If the delete operation fails, the exception thrown by our DataPortal_Delete() method is simply returned to the client.

Updating the CSLA Project

At this point, the Server.DataPortal class is complete. Our client-side DataPortal code requires a reference to this class, so we should return to the CSLA project and add a reference to the CSLA.Server.DataPortal project as shown in Figure 5-7.

image from book
Figure 5-7: Referencing CLSA.Server.DataPortal

We're almost at a point where our code compiles. Once we complete the transactional DataPortal , we should be ready to build.



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111
Authors: Rockford Lhotka
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net