Implementing the Logic Layer of a Subscription Management Application


In the "Architecture of Subscription Management Interfaces" section (p. 185), we looked at the three-tier model and why it makes sense to build SMIs this way. For the music store application's SMI, the data layer is already built. When we compiled the ICF and ADF, SQL-NS created the instance and application database objects, including tables and stored procedures for managing subscriber and subscription data. We then created the remaining objects needed in the data layer in the "Creating the Database Objects for the ASP.NET Membership Provider" section (p. 192).

This section focuses on the logic layer. It describes the purpose of the logic layer and the operations it should support, and then looks at the implementation of the music store application's logic layer using the SQL-NS API.

Purpose of the Logic Layer

The purpose of the logic layer is to implement all the operations exposed to users through the user interface. The logic layer provides a simple programming interface by which the user interface layer can invoke these operations; internally, it uses the SQL-NS API to manipulate the data layer. As such, the logic layer is an intermediary between the user interface and data layers.

Each of the SMI operations that the logic layer supports may encapsulate several operations in the SQL-NS API. You can think of each logic layer operation as a wrapper for a set of operations in the SQL-NS API that logically go together. For example, the SQL-NS API provides separate operations for adding a subscriber and adding a subscriber device. In your application, it may be the case that whenever a new subscriber is added, you add a default subscriber device. You can expose this to the user interface layer through a single logic layer operation that wraps both the adding of the subscriber and the default device.

The logic layer can also implement business rules and policies. For example, you may want to restrict each subscriber to a maximum of five subscriptions. In the logic layer operation that adds a new subscription, you could check how many subscriptions the subscriber already has and return an error if the subscriber already has the maximum number. In general, it's inconvenient to implement rules and policies of this type in the data layer. Much of the data layer is created by compiling the ICF and ADF, which don't really provide a direct way to express rules and policies.

Another function of the logic layer is to perform validation checks that would be difficult to implement elsewhere. For example, in the subscriptions by genre that our music store application supports, a user should not be able to specify a genre name in her subscription that is not one of the supported genres defined by the music store. In other words, if the music store knows only about the Jazz, Rock, and Hip Hop genres, a user should not be able to create a subscription for the Opera genre. In the logic layer operation that adds a new subscription by genre, the genre name can be validated against the list of supported genres and an error returned if necessary. Also, the logic layer could implement an operation that returned the valid list of genres; the user interface could use this to provide the user with a menu of choices he can select from when creating a subscription.

Logic Layer Operations

As mentioned earlier, the logic layer implements the operations that the user interface exposes to users. When designing the logic layer for any application, the first thing to do is list these operations. Although the exact list of operations may vary per application, in most applications, the fundamental operations are adding, modifying, and deleting subscribers, subscriber devices, and subscriptions. Typically, the subscription-related operations are implemented separately for each subscription class.

In addition to these fundamental operations, the logic layer usually provides a number of "helper operations" that serve as utilities for the user interface. These utilities typically include the following operations:

  • Check whether a subscriber already exists in the instance

  • Retrieve the set of subscribers in the instance

  • Retrieve the set of subscriptions for a particular subscriber

  • Retrieve all the possible valid values for certain subscriber and subscription propertiesfor example, the list of supported genres for the genre name property in subscriptions.

Our music store application supports all these operations. The remainder of this section illustrates the implementation of these operations in the logic layer, and in the next section, we use them in a user interface.

Logic Layer Code

We implement the logic layer of the music store SMI as a class that exposes public methods for each of the supported operations. The user interface code calls these methods to invoke the various operations. Internally, these methods call the SQL-NS API.

Note

Before we examine the actual code for the logic layer operations, it's important to note that the logic layer does not have to completely wrap the entire SQL-NS API. In other words, it's perfectly acceptable to return SQL-NS API objects from the methods on the logic layer class. In some cases, the user interface can access the properties and methods on these objects directly.

In general, you should provide wrappers only where the wrapper adds value for the user interface. In cases where the SQL-NS API already provides a class or method that the user interface can consume directly, it's better not to provide unnecessary wrapper code.


If you have not already done so, open the Visual Studio solution containing the SMI code, as described in the section "The SMI Visual Studio Solution" (p. 192). In this solution, the logic layer code is implemented in a separate source file from the rest of the user interface code. To open the logic layer source file, expand the App_Code folder in the Solution Explorer and double-click the MusicStoreSMIOperations.cs file.

The logic layer class is called MusicStoreSMIOperations. Glance through the MusicStoreSMIOperations.cs source file to see the methods and properties it implements. In the remainder of this section, we'll look at each of these in detail. I'm not going to show the complete source code listing for this class in one place in the text. Instead, I'm going to highlight pieces of it as I explain each concept. As I did in Chapter 6, I'll use abbreviated code listings that show pieces of the class code in context, but I'll omit code that doesn't relate to the immediate discussion. In each abbreviated listing, omitted code is indicated with an ellipsis (...). This same technique is used in other code examples in subsequent chapters of this book.

Connecting to the Data Layer: The Instance and Application Objects

The operations that the logic layer implements involve manipulating data in the instance and application database objects. The SQL-NS API provides the NSInstance and NSApplication classes to facilitate connecting to these database objects. These classes allow you to connect to the databases containing the objects, just by specifying the instance and application names. You do not need to know where those databases are located or what they're called; NSInstance and NSApplication read the Registry keys installed when the instance is registered (with the nscontrol register command line tool or the Register Instance command in Management Studio) to obtain the information required to establish a database connection.

The logic layer declares NSInstance and NSApplication objects as private members and instantiates them in its constructor. These are then used by the other operations. Listing 7.1 shows the declaration of the NSInstance and NSApplication objects and the code for the class constructor:

Listing 7.1. Constructor That Creates the NSInstance and NSApplication Objects

 public class MusicStoreSMIOperations {     // Constant strings used throughout this class.     private const string INSTANCE_NAME = "MusicStore";     private const string APPLICATION_NAME = "SongAlerts";     ...     // Private member variables.     private NSInstance musicStoreInstance;     private NSApplication songAlertsApp;     /// <summary>     /// Just initializes the connection to the instance and application.     /// </summary>     public MusicStoreSMIOperations()     {         try         {             musicStoreInstance = new NSInstance(INSTANCE_NAME);             songAlertsApp = new NSApplication(                 musicStoreInstance,                 APPLICATION_NAME);         }         catch (NSException ex)         {             string errorMsg = String.Format(                 "Unable to connect to {0} instance and {1} application: {2}",                 INSTANCE_NAME,                 APPLICATION_NAME,                 ex.Message);             throw new Exception(errorMsg, ex);          }       }       ... } 

The instance and application names are defined in constant strings declared at the top of the class. Then, an NSInstance object called musicStoreInstance and an NSApplication object called songAlertsApp are declared.

The MusicStoreSMIOperations constructor instantiates the NSInstance class with the new operator, passing the instance name to the NSInstance constructor. If you configured your development environment for SQL Server Authentication when setting up the source code in Chapter 2, this line of code on your system will differ slightly from what's shown here in Listing 7.1. Specifically, the version of the code on your system will pass two additional arguments (not shown in the listing here) to the NSInstance constructor: the SQL username and password to use when connecting to the database. The values will be hard-coded to the username and password for the SQL development account you created in Chapter 2. For example, if the username is "SQL-NS_Dev" and the password is "devPassword," the line that creates the musicStoreInstance object would read as follows:

 musicStoreInstance = new NSInstance(INSTANCE_NAME, "SQL-NS_Dev", "devPassword"); 


The API uses the given SQL credentials to connect to the database for all subsequent operations. If you're using Windows Authentication, no username or password is passed to the NSInstance constructor, so the code reads exactly as shown in Listing 7.1. With Windows Authentication, the API connects to the database using the credentials of the Windows account under which the SMI runs.

Caution

From a security perspective, it's not a good idea to hard-code usernames and passwords in your code. This technique is used here (in the case of SQL Server Authentication) only for simplicity of illustration.

If you're using SQL Server Authentication in a production application, you should design a scheme by which your application can be configured with the appropriate SQL username and password without compromising security. A common technique is to encrypt the username and password and store them in a Registry key. Your code can read this Registry key, decrypt the values, and then set the SqlUser and SqlPassword properties.

When implementing any scheme that encrypts values, it's recommended that you use the Windows Data Protection API (DP API). You can find more information on this API in the MSDN documentation or online at http://msdn.microsoft.com/library/enus/dnsecure/html/windataprotection-dpapi.asp.


After the musicStoreInstance object is created, the constructor creates a new NSApplication object, songAlertsApp, with the new operator. It passes the instance object and the application name as parameters.

The creation of the NSInstance and NSApplication objects can fail for a number of reasons. For example, the instance may not be registered, the SQL Server may be unreachable, or the username and password supplied for SQL authentication may be incorrect. If an error does occur, an NSException is thrown. In the constructor code, the creation of the instance and application objects happens with a TRy block that catches exceptions of type NSException. The catch block simply constructs a meaningful error message that the user interface can display and then throws a new exception with this error message and the original NSException as the inner exception.

Manipulating Subscribers

The MusicStoreSMIOperations class implements a method called AddSubscriber() to add a new subscriber record to the system. When a new user wants to register as a subscriber, the user interface invokes this method. Listing 7.2 shows the code for AddSubscriber().

Listing 7.2. Implementation of the AddSubscriber() Method

 public class MusicStoreSMIOperations { ...     public Subscriber AddSubscriber(string subscriberId, bool enabled)     {         Subscriber s = new Subscriber(musicStoreInstance);         // Set the properties on the subscriber object.         s.SubscriberId = subscriberId;         s.Enabled = enabled;         // Write the subscriber record to the database.         s.Add();         // Also add a default device for the subscriber.         AddSubscriberDevice(s);         return s;      } ... } 

AddSubscriber() takes two parameters: a subscriber ID string and a Boolean flag indicating whether the subscriber record should be marked as enabled when it is created. The subscriber ID string becomes the subscriber ID in the new subscriber record.

The first line of code creates a new instance of the Subscriber class, which is provided by the SQL-NS API to represent subscribers. When creating the new Subscriber object, it passes the instance object (created in the constructor as shown in Listing 7.1) to the Subscriber constructor. This is the pattern followed by almost all the objects in the SQL-NS API: The constructor takes either an NSInstance or an NSApplication object, depending on whether the object represents an entity that is part of the SQL-NS instance or a particular application. Recall that subscribers are stored at the instance level, so the Subscriber constructor takes an instance object.

The next two lines set the SubscriberId and Enabled properties on the subscriber object. The SubscriberId property is a string and should specify a unique identifier for the subscriber in the instance. The format of the subscriber ID is entirely up to you, as long as each subscriber has a unique ID. Typically, applications use an identifier for the subscriber ID that serves as a key into other records related to the subscriber. For example, if your system already keeps a profile for each user, keyed by user ID, that user ID can be used as a subscriber ID. As you'll see later when we look at the user interface code, our application uses the username managed by the ASP.NET SqlMembershipProvider (our user account management system) as the subscriber ID.

The Enabled property behaves like an on/off switch for the subscriber. If a subscriber is enabled, SQL-NS processes its subscriptions and sends it notifications. If a subscriber is disabled, SQL-NS does not do any processing for it: The subscriber's subscriptions are not evaluated when events arrive, and any undelivered notifications are not sent. As you see in this chapter, several SQL-NS entities, including subscribers, subscriber devices, and subscriptions, can be enabled or disabled.

When the subscriber properties (SubscriberId and Enabled) are set, the code then calls Add() on the subscriber object. This causes the subscriber record to be written to the database. Until the Add() method is called, the subscriber record and its properties exist only in memory. The final line of code in AddSubscriber() calls AddSubscriberDevice(), another operation method on our logic layer class, which adds a default subscriber device for the subscriber. We look at this operation in the "Manipulating Subscriber Devices" section (p. 204). Note that by calling AddSubscriberDevice() here, our logic layer ensures that every subscriber added gets a default subscriber device. The adding of a subscriber and adding of a subscriber device are combined into a single operation from the perspective of the user interface layer.

After the subscriber device is added, the AddSubscriber() operation is complete. The method returns the newly created subscriber object to the caller.

Subscriber Records and Subscriber Details

For each subscriber, SQL-NS maintains a record that contains the subscriber ID and enabled state. All other entities associated with the subscriber (its subscriptions and subscriber devices) are related via the subscriber ID. SQL-NS does not need to keep any other information about subscribers, but other parts of the application may well need to. For example, it may be necessary to store a customer profile and billing information for each subscriber.

If your application needs data about a subscriber beyond what SQL-NS stores, you must store this data in non-SQL-NS database objects. In general, you define the schema for the detail data and create the corresponding database structures (tables, views, and stored procedures) separately from your SQL-NS development. In the case of using the ASP.NET SqlMembershipProvider, the database objects required to store details about users can be generated automatically: the script you ran in the "Creating the Database Objects for the ASP.NET Membership Provider" section was generated by running a tool called aspnet_regsql that is shipped with ASP.NET. For more information on this tool, refer to the ASP.NET 2.0 documentation.

In some cases, the subscriber detail information is written to the non-SQL-NS objects as part of the process of adding a subscriber. In our application, this is not the case: user accounts are created separately from adding subscribers. However, in other applications, it's not uncommon for an operation such as AddSubscriber() to implement both the creation of the subscriber record using the SQL-NS API and the writing of the additional subscriber details using other data access APIs.

Modifying Subscriber Records

The only property of a subscriber record that can be changed after it is created is the Enabled property. You cannot change the SubscriberId, because this is the unique identifier for the subscriber.

The logic layer class does not provide an explicit operation to support modifying the Enabled property of the subscriber. This is because the SQL-NS API already provides an interface for performing this operation: You simply change the value of the Enabled property on the subscriber object and call the Update() method on it. Because the subscriber object is returned from the AddSubscriber() method (and can be obtained later via a subscription enumeration), the user interface can perform the update on this object directly. As mentioned earlier, it's not necessary to completely wrap the SQL-NS API in the logic layer code; where it makes sense to do so, you can allow the user interface to manipulate SQL-NS API objects directly. Changing a subscriber's enabled property is an example of such a case.

Removing Subscriber Records

SMI user interfaces commonly provide users with a way to remove their subscriber registrations. To support this, the logic layer code has to provide an operation to remove subscriber records.

The MusicStoreSMIOperations class provides a RemoveSubscriber() method for this purpose. As you'll see from the code, this method is another example of the logic layer code encapsulating several separate SQL-NS API operations into a single logic operation that the user interface can invoke. Listing 7.3 shows the code for RemoveSubscriber().

Listing 7.3. Implementation of the RemoveSubscriber() Operation

 public class MusicStoreSMIOperations { ...     public void RemoveSubscriber(Subscriber s)     {         bool failedBecauseDisabled = false;         try         {            // Attempt to delete the subscriber.            s.Delete();         }         catch (NSException ex)         {             // If the delete failed because the subscriber was disabled,             // we're going to enable it and then try again, so we'll set             // a flag that we'll check later. If it failed for any other             // reason, we'll just throw the exception out to the caller.             if (ex.ErrorCode == NSEventEnum.SubscriberDisabled)             {                 failedBecauseDisabled = true;             }             else             {                 throw;             }         }         // If the delete failed because the subscriber was disabled,         // enable it and then try the delete again.         if (failedBecauseDisabled)         {             // We do the enable in its own try block, so that we isolate             // any exceptions that occur from this operation and form a             // meaningful error message to the caller.             try             {                 // Set the enabled bit and update the subscriber.                 s.Enabled = true;                 s.Update();              }              catch (NSException ex)              {                  throw new Exception(                      "Failed to enable subscriber before deleting: " +                      ex.Message,                      ex);              }              // Now try the delete again. If this fails, the exception              // will just get thrown out to the caller.              s.Delete();         }     } ... } 

RemoveSubscriber() is passed a subscriber object that represents the subscriber record to be removed. It's reasonable to expect callers (the user interface code) to pass in a subscriber object because subscriber objects are returned from the AddSubscriber() method, and the logic layer provides a way to obtain a subscriber object for an existing subscriber, as we'll see later.

SQL-NS semantics dictate that a subscriber must be enabled before it is deleted. This makes sense from the perspective of consistency, but it's not necessarily something you'd want to expose in the user interface. Therefore, the logic layer operation, RemoveSubscriber(), encapsulates the code that enables a subscriber before deleting it, hiding this perhaps counterintuitive SQL-NS requirement from the user interface. RemoveSubscriber() provides the user interface an easy facility for removing a subscriber, regardless of its enabled state.

In the first try block in the code shown in Listing 7.3, the Delete() method on the subscriber object is called. This attempts to delete the subscription. If this fails, an NSException is thrown. If the subscriber record failed to be deleted because the subscriber was disabled, the NSException has an error code that indicates this.

The catch block checks the error code in the NSException that was caught. If the code indicates that the subscriber was disabled, it sets a flag indicating that this was the cause of failure. Later, the code checks this flag to see whether another attempt should be made. If the error code in the exception object indicates any other problem, the exception is simply rethrown to the caller (causing the whole operation to fail).

Note

All NSException objects contain an error code that you can access via the ErrorCode property. All the error codes are defined in the enumeration NSEventEnum. The values in this enumeration are documented in the SQL-NS Books Online, but not in much detail because many of them are internal error codes used only within the SQL-NS engine. However, in certain cases where the error code is useful to calling applications, the SQL-NS Books Online points out values that you should look for.


If the delete operation failed because the subscriber was enabled, the next try block attempts to enable the subscriber. It first sets the Enabled property to true and then calls the Update() method to write this changed value to the database. These operations appear in their own TRy block, because if they fail, we want to construct a meaningful error message for the caller. Simply allowing any thrown exceptions to bubble up to the caller would be confusing; the caller might not be expecting an error having to do with enabling a subscriber, because the enable operation is hidden inside the implementation of the remove operation. Instead, the code catches the exception, and in the catch block, constructs an error message that says that it failed to enable the subscriber before attempting to delete it. The original NSException is passed as the inner exception to the new Exception object that is constructed and thrown.

If the enabling of the subscriber succeeds, we try to call the Delete() method on the subscriber again. If this fails, we allow the resulting exception to be thrown to the caller.

You might wonder why it's necessary to attempt the delete to determine whether the subscriber is disabled, rather than simply checking the Enabled property on the subscriber object passed in. The reason is that the object may contain stale data. All objects in the SQL-NS API are in-memory caches of data in the instance or application databases. If the underlying data in the database changes, the snapshot reflected in the objects may not reflect reality. Thus, we cannot trust the value of the Enabled property: The enabled state of the underlying subscriber record may have been changed by the user running another instance of the SMI, or by an administrator.

Another approach would have been to always enable the subscriber before attempting the delete. However, this would have required two round-trips to the database for every remove operation. In cases where the subscriber was already enabled (the common case), only one round-trip is really necessary. The way the code is currently written, we make additional round-trips to the database only where absolutely necessary. In the common case, the subscriber is enabled, and the first delete succeeds.

Manipulating Subscriber Devices

Each subscriber can have several devices to which notifications can be delivered. Each device is represented by a subscriber device record in the instance database, which specifies the device type, address, and delivery channel.

Your SMI probably needs to provide a way for subscribers to add, modify, and delete their subscriber device records. In the logic layer, you can implement any business rules associated with subscriber devices. For example, you could stipulate that every subscriber must have one default subscriber device and ensure that this subscriber device record is created whenever a new subscriber is added (the code in our music store example does this). You could also, for example, restrict the user of certain delivery channels that offer premium service to those subscribers who have paid a fee for such service.

In addition to providing an implementation of the subscriber device business rules, the logic layer can also provide the decision logic that chooses an appropriate delivery channel based on choices the user made in the user interface. For example, suppose that your application supports sending notifications via email and via Short Message Service (SMS), the text messaging protocol used by cell phones and pagers. You would probably have two delivery channels, each of which represented a connection to a delivery system that supported one of the two methods of delivery. The names given to these delivery channels may not be meaningful to the user; all the user knows is whether he wants his notifications sent via email or via SMS. The logic layer could expose a method that took the delivery method as a parameter and chose the appropriate delivery channel name based on this.

At this point, our music store instance has only one delivery channel: the one configured to use the File delivery protocol. Using additional delivery channels is not explained until Chapter 10, so at this point it's difficult to show much interesting code around subscriber devices and delivery channel choices. For now, I'll show the logic layer code that adds the default subscriber device. When we look at delivery channels in more detail in Chapter 10, I'll describe the corresponding SMI logic layer code to support them.

Adding Subscriber Devices

As you saw in Listing 7.2, the AddSubscriber() operation adds a default subscriber device by calling AddSubscriberDevice(), another method on the MusicStoreSMIOperations class. Listing 7.4 shows the implementation of this method.

Listing 7.4. Implementation of the AddSubscriberDevice() Method

 public class MusicStoreSMIOperations { ...     public SubscriberDevice AddSubscriberDevice(Subscriber s)     {         SubscriberDevice device = new SubscriberDevice(musicStoreInstance);         // Set the device properties. We use the default values for the         // single delivery channel in the application.         device.SubscriberId = s.SubscriberId;         device.DeviceName = "DefaultDevice";         device.DeviceTypeName = "File";         device.DeviceAddress = "";         device.DeliveryChannelName = "FileChannel";         // Add the device.         device.Add();         return device;      } ... } 

AddSubscriberDevice() takes the subscriber object representing the subscriber for whom the default device should be added. It instantiates a new SubscriberDevice object, passing the instance object as a parameter to the SubscriberDevice constructor. It then sets several properties on the subscriber device object: the subscriber ID, device name, device type, device address, and delivery channel name.

Don't be concerned if these properties are unfamiliar at this time. For the moment, note that the subscriber device is associated with a particular subscriber (by subscriber ID) and delivery channel (by delivery channel name).

The subscriber device itself has a name, which can be any string by which you can meaningfully identify the device when displaying it in the user interface. The device name must be unique within the scope of the owning subscriber. In other words, a single subscriber cannot have two devices with the same name, but two different subscribers could each have a device with the same name.

Note

We'll look at the other subscriber device properties in more detail in later chapters. We'll cover device types when we look at content formatting in Chapter 9, "Content Formatters," and device addresses when we look at delivery protocols in Chapter 10.


After the subscriber device properties have been set, the code calls the Add() method to write the subscriber device record to the database. AddSubscriberDevice() then returns the newly created subscriber device object to the caller.

Modifying and Removing Subscriber Devices

The subscriber device object provides a Delete() method that can be used to remove a subscriber device record from the instance database. Also, it provides an Update() method that writes any changes made to the object's properties back to the subscriber device record in the database. Because at this stage our application has a single delivery channel and every subscriber must have a default device configured to use this channel, it does not make sense to remove or modify subscriber devices. Therefore, our logic layer and user interface do not implement these operations; you will not see the subscriber device Delete() or Update() methods being called.

Manipulating Subscriptions

The most important function of an SMI is to provide users with a way to manipulate their subscriptions. The ADF defines the subscription classes that represent the kinds of subscriptions the application supports, and the SMI code described in this section creates, deletes, and modifies subscriptions of these subscription classes.

The SQL-NS API provides a Subscription class that you can use to perform operations on subscription data. This is used in the SMI logic class, to implement operations that manipulate subscriptions. In the implementation of these operations, you can encode business rules such as restrictions on the number of subscriptions that subscribers can have, or validation of the subscription parameters.

Note

The terms "Subscription class" and "subscription class" can sometimes be confused. Subscription is a class in the SQL-NS API that represents individual subscriptions. A subscription class in a SQL-NS application represents a particular kind of subscription supported by the application. The Subscription class can be used to manipulate subscriptions of any subscription class.


Listing 7.5 shows the implementation of the AddNewSongByArtistSubscription() method of the MusicStoreSMIOperations class. This method adds a new subscription of the NewSongByArtist subscription class, defined in Chapter 5. The user interface calls this method when it needs to add a new subscription of this subscription class.

Listing 7.5. Implementation of the AddNewSongByArtistSubscription() Method

 public class MusicStoreSMIOperations { ...     private const string      NEW_SONG_BY_ARTIST_SUBSCRIPTION_CLASS_NAME = "NewSongByArtist"; ...     public Subscription AddNewSongByArtistSubscription(         Subscriber  s,         string      artistName)     {         // Create a new subscription object for a subscription of         // the NewSongByArtist subscription class.         Subscription subscription = new Subscription(             songAlertsApp,             NEW_SONG_BY_ARTIST_SUBSCRIPTION_CLASS_NAME);         // Set the subscriber id.         subscription.SubscriberId = s.SubscriberId;         // Set the fields in the subscription class         // schema (we have only one).         subscription["ArtistName"] = artistName;         // Add the subcription;         subscription.Add();         return subscription;     } ... } 

AddNewSongByArtistSubscription() takes a subscriber object and an artist name as arguments. The subscriber object represents the subscriber for whom the subscription should be added, and the artist name provides a value for the subscription class field, ArtistName, declared in the ADF. Had the subscription class schema defined more fields, this method would have needed additional arguments to obtain values for them.

The first statement in the method creates a new instance of the Subscription class, passing the application object and the subscription class name as arguments to the constructor. The subscription class name comes from a constant defined as a private member of the MusicStoreSMIOperations class.

After creating the subscription object, the subscriber ID property is set using the value from the subscriber object. The next line supplies a value for the ArtistName field. Note that this field is not represented as a property on the object in the same way as the subscriber ID. Remember that Subscription is a generic class that is supposed to represent subscriptions of any subscription class. Because each subscription class may have a different set of fields (as declared in the ADF), the generic Subscription class cannot possibly define properties to represent them all.

Instead, field values are set by means of the index operator ([]) on the subscription object, passing the field name as the index. In the case of the code in Listing 7.5, subscription["ArtistName"] refers to the ArtistName field in the subscription. The value provided for this field is obtained from the argument passed to the AddNewSongByArtistSubscription() method. After setting the properties and fields on the subscription object, the Add() method is called to write the subscription data to the subscription table in the application database. Finally, the newly created subscription object is returned to the caller.

Tip

One advantage of implementing operations in your logic layer to add subscriptions, instead of having the user interface call the SQL-NS API directly, is that you can ensure that the values for the subscription fields are set correctly. Using the SQL-NS API directly, there is no guarantee that all the required fields will be supplied or that the values supplied will be of the correct type. By implementing an add method that takes the required set of fields as strongly typed parameters and sets them appropriately in the subscription object, you greatly reduce the opportunity for error.


Specifying Subscriptions Schedules

The code in Listing 7.5 adds an event-triggered subscription. For event-triggered subscriptions, the only values that need to be set on the subscription object are the subscriber ID and the subscription class fields. However, when adding scheduled subscriptions, you also need to provide a subscription schedule. Listing 7.6 shows the code for the operation that adds a subscription of the NewSongByGenre scheduled subscription class.

Listing 7.6. Implementation of the AddNewSongByGenreSubscription() Method

 public class MusicStoreSMIOperations { ...     private const string NEW_SONG_BY_GENRE_SUBSCRIPTION_CLASS_NAME = "NewSongByGenre"; ...     public Subscription AddNewSongByGenreSubscription(         Subscriber  s,         string      genreName,         DateTime    scheduleStartTime,         Microsoft.SqlServer.NotificationServices.TimeZone timeZone)     {         // Create a new subscription object for a subscription of the         // NewSongByGenre subscription class.         Subscription subscription = new Subscription(             songAlertsApp,             NEW_SONG_BY_GENRE_SUBSCRIPTION_CLASS_NAME);             // Set the subscriber id.             subscription.SubscriberId = s.SubscriberId;             // Set the fields in the subscription class             // schema (we have only one).             subscription["GenreName"] = genreName;             // Set the subscription schedule.             subscription.ScheduleRecurrence = "FREQ=DAILY;";             subscription.ScheduleStart = FormatScheduleStart(                 scheduleStartTime,                 timeZone);             // Add the subcription;             subscription.Add();             return subscription;       } ... } 

In addition to the subscriber object and the genre name (which is used as the value for the GenreName subscription class field), AddNewSongByGenreSubscription() also takes a schedule start time and a time zone object. These two arguments are required to construct the subscription schedule.

As the name suggests, the schedule start time specifies the first time that the subscription should fire. The time zone object is an instance of the TimeZone class in the SQL-NS API that represents the time zone in which the schedule start time is expressed. Note that we have to use the fully qualified class name (including the namespacespecifier) for the TimeZone class. This is because there is also a .NET Framework class called TimeZone in the System namespace. The use of the Microsoft.SqlServer.NotificationServices namespace disambiguates the class name.

The AddNewSongByGenreSubscription() method creates a new subscription object, passing the application object and the subscription class name to the constructor. Note that the same SQL-NS API class, Subscription, is used to represent both event-triggered and scheduled subscriptions. After instantiating the subscription object, the code sets the subscriber ID property and the value of the subscription field, GenreName.

Thereafter, two properties that relate to the subscription schedule, ScheduleRecurrence and ScheduleStart, are set. The ScheduleRecurrence property specifies how often the subscription should fire, after the time of the first firing, which is specified in ScheduleStart.

The value of the ScheduleRecurrence property is a string that indicates the recurrence pattern for the schedule. The simplest and most frequent recurrence pattern is daily recurrence. When this pattern is specified, the subscription fires at the same time every day. Other recurrence patterns are

  • Weekly On particular days of the week

  • Monthly On a particular day of the month, in particular months of the year

  • Yearly On a particular day of a particular month

The string value assigned to ScheduleRecurrence follows the ICalendar recurrence rule syntax (ICalendar is an Internet standard for calendaring and scheduling). In the code shown in Listing 7.6, we assign the value, "FREQ=DAILY;", which is the ICalendar syntax for daily recurrence. The syntax for specifying the other recurrence patterns in the preceding list is given in the SQL-NS Books Online (see the documentation for the ScheduleRecurrence property of the Subscription class).

The value of the ScheduleStart property is just a formatted string version of the start time and time zone passed in. The format is specified in detail in the SQL-NS Books Online in the documentation for the ScheduleStart property of the Subscription class. The code in Listing 7.6 calls a helper method, FormatScheduleStart(), to properly format the schedule start string. Listing 7.7 shows this function.

Listing 7.7. Implementation of the FormatScheduleStart() Helper Method

 public class MusicStoreSMIOperations { ...     private string FormatScheduleStart(         DateTime scheduleStartTime,         Microsoft.SqlServer.NotificationServices.TimeZone   timeZone)     {         return String.Format("TZID={0}:{1}T{2}",             timeZone.TimeZoneId.ToString().Trim(),             scheduleStartTime.ToString("yyyyMMdd"),             scheduleStartTime.ToString("HHmmss"));     } ... } 

This method takes the start time and time zone as parameters and returns a string formatted according to the following notation:

 TZID=<TimeZoneId>:yyyyMMddTHHmmss 


In this notation, <TimeZoneId> is the identifier for the time zone, obtained from the TimeZoneId property on the time zone object. yyyy, MM, and dd are the parts of the date, and HH, mm, and ss are the parts of the time. The date and time portions of the string are separated by the "T" character. To format the date and time correctly, this method uses the ToString() method on the .NET Framework DateTime structure, which takes a format specifier string as an argument.

Caution

Although the specification of the schedule start time includes both date and time portions, the date portion is ignored by the SQL-NS engine. Just the time value is used to determine the time of day at which the subscription should be evaluated on the days specified by the schedule recurrence.


Going back to the code in Listing 7.6, you see that after the schedule recurrence and start time are set, the Add() method on the subscription object is called to write the subscription record, including the schedule, to the database. The method then returns the newly created subscription object. Notice that the code for adding a scheduled subscription is similar to the code for adding an event-triggered subscription. The only difference is that for scheduled subscriptions you need to set the ScheduleRecurrence and ScheduleStart properties on the subscription object.

Modifying and Removing Subscriptions

Like the other objects in the SQL-NS API, the subscription object provides Delete() and Update() methods that can be used to remove and modify the underlying subscription records. Because we are returning subscription objects from the logic layer operations that add subscriptions, we do not need to implement explicit logic layer operations to delete and modify subscriptions. The user interface layer can call the subscription object's Delete() and Update() methods directly. You would implement wrappers for these methods in the logic layer only if you needed to implement business logic associated with these operations.

Helper Operations in the Logic Layer

In addition to the fundamental operations that we just examined, the logic layer also provides a number of helper methods that serve as convenient utilities for the user interface layer. In this section, we look at the helper methods implemented on the MusicStoreSMIOperations class.

Retrieving All the Subscribers in an Instance

The SQL-NS API provides a SubscriberEnumeration class that is used to represent a collection of subscriber objects. The enumeration class provides methods to iterate over the individual subscriber objects in the collection and access a particular object via the index operator.

The logic layer should provide an operation that creates a SubscriberEnumeration containing all the subscribers in the instance and returns this to the caller. The user interface can use this operation whenever it needs to obtain a collection of all subscribers, such as when it needs to display them in a list.

In the MusicStoreSMIOperations class, this operation is implemented as a property, as shown in Listing 7.8.

Listing 7.8. Implementation of the Subscribers Property

 public class MusicStoreSMIOperations { ...      public SubscriberEnumeration Subscribers      {          get          {              return new SubscriberEnumeration(musicStoreInstance);          }      } ... } 

The get accessor for the property creates and returns a new SubscriberEnumeration object, passing the instance object to the constructor. This method of instantiating the SubscriberEnumeration class results in a collection that contains all the subscribers in the instance.

Note

The data in the SubscriptionEnumeration class is populated lazily. In other words, the data isn't actually fetched from the database until the individual items in the collection are enumerated. Because of this design, creating a SubscriberEnumeration is not an expensive operation, even if an instance has thousands (or even millions) of subscribers.


Checking Whether a Subscriber Exists

A common operation in SMI user interfaces is checking whether a subscriber record exists for a given subscriber ID. This can be used, for example, to determine whether a given user is already an existing subscriber or has to register as a subscriber for the first time.

The MusicStoreSMIOperations class implements a Boolean function called SubscriberExists() that takes a subscriber ID parameter and returns true only if a subscriber record for that subscriber ID exists. Listing 7.9 shows the code for this function.

Listing 7.9. Implementation of the SubscriberExists() Function

 public class MusicStoreSMIOperations { ...      public bool SubscriberExists(string subscriberId)      {          bool result = false;          try          {              // Try to access the subscriber record using the subscriber id              // as an index.              Subscriber s = this.Subscribers[subscriberId];              result = true;          }          catch (IndexOutOfRangeException)          {              // Catching an IndexOutOfRange exception when indexing the              // subscriber enumeration means the record doesn't exist.              result = false;          }          return result;      } ... } 

The first line in the try block attempts to obtain a subscriber object for the given subscriber ID. Let's dissect this line of code:

             Subscriber s = this.Subscribers[subscriberId]; 


By referencing this.Subscribers, this line is actually invoking the get accessor of the Subscribers property we looked at in Listing 7.8. This returns a SubscriberEnumeration containing all the subscribers in the instance. The subscriber enumeration is then being indexed via the subscriber ID. SubscriberEnumeration provides an index operator that takes a subscriber ID as a string and returns the corresponding subscriber object.

If there is no subscriber object in the collection with the given subscriber ID, the index operator throws an IndexOutOfRangeException. The code in Listing 7.9 catches this exception and uses it as an indication that the subscriber record does not exist. In the catch block, it sets the local result variable to false.

If no exception is thrown by the index operator, it means that a subscriber object with the given subscriber ID was found. Control proceeds to the next line, which sets the result variable to true. At the end of the function, the value of the result variable is returned.

Retrieving Subscriptions

The user interface often needs to display the existing subscriptions that a given subscriber has. To do this, it needs to obtain a collection of the subscription objects that represent those subscriptions. It can then read the properties of these subscription objects to display the subscription data.

The MusicStoreSMIOperations class implements two functions that return subscription collections, each of which supports one of the two subscription classes in the application. Listing 7.10 shows these functions.

Listing 7.10. Implementation of the Methods That Return the Subscriptions for a Given Subscriber

 public class MusicStoreSMIOperations { ...     public SubscriptionEnumeration GetNewSongByArtistSubscriptions(         Subscriber s)     {         SubscriptionEnumeration subscriptions = s.GetSubscriptions(             songAlertsApp,             NEW_SONG_BY_ARTIST_SUBSCRIPTION_CLASS_NAME);         return subscriptions;      }      ...      public SubscriptionEnumeration GetNewSongByGenreSubscriptions(          Subscriber s)      {          SubscriptionEnumeration subscriptions = s.GetSubscriptions(              songAlertsApp,              NEW_SONG_BY_GENRE_SUBSCRIPTION_CLASS_NAME);          return subscriptions;      } ... } 

Both functions take a subscriber object as input and return a SubscriptionEnumeration object. The SubscriptionEnumeration class provides the same enumeration capabilities for Subscription objects that the SubscriberEnumeration class does for Subscriber objects.

Let's examine the GetNewSongByArtistSubscriptions() function first. As its name suggests, this function returns an enumeration of the subscription objects representing the given subscriber's NewSongByArtist subscriptions.

The Subscriber object provides a GetSubscriptions() method that retrieves subscriptions. GetSubscriptions() takes an application object and a subscription class name as parameters and returns an enumeration of the subscriber's subscriptions in that subscription class.

GetNewSongByArtistSubscriptions() calls the GetSubscriptions() method on the subscriber object it is passed. It supplies the application object and the name of the NewSongByArtist subscription class as parameters. The enumeration object obtained is then returned to the caller.

The GetNewSongByGenreSubscriptions() function is almost identical. The only difference is the subscription class name passed to the GetSubscriptions method on the subscriber object.

Implementing helper methods like these raises the level of abstraction at which the user interface code can operate. It does not need to know the subscription class names or manage the NSApplication objects required to retrieve the data.

Data Collections

There are cases where the SMI needs to display collections of data from which the user can make a selection. The logic layer provides these data collections for the user interface's use.

In the music store application, there are two such collections of data that the user interface needs to display. One is the list of genres supported by the music store, and the other is the list of time zones that can be used in a subscription schedule.

The MusicStoreSMIOperations class provides properties that return collections representing the set of genres and the set of time zones. Listing 7.11 shows the code for these properties.

Listing 7.11. Implementation of Data Properties Used by the User Interface

 public class MusicStoreSMIOperations { ...     public TimeZoneEnumeration TimeZones     {         get         {             return new TimeZoneEnumeration(musicStoreInstance, "en");         }     }     ...     public string[] Genres     {         get         {             // NOTE: We could actually obtain the values by reading them             //       from the music store database.             return new string[3]{"Jazz", "Rock", "Hip Hop"};         }     } ... } 

The TimeZones property returns a TimeZoneEnumeration object. TimeZoneEnumeration is a class in the SQL-NS API that provides enumeration capabilities over objects of its TimeZone class. The get accessor for the TimeZones property implemented in Listing 7.11 creates and returns a new TimeZoneEnumeration object. The TimeZoneEnumeration constructor takes the instance object and a language name as parameters. It returns an enumeration of TimeZone objects whose property values are expressed in the given language. In this simple example, we pass in the identifier "en" to represent English because that's the only language the simple user interface in this chapter will support.

The Genres property returns an array of strings that represent the genre names currently supported by the music store. For the sake of simplicity, I've just hard-coded the genre names. They could also have been read from the actual music store database. This would definitely be the more robust solution because it would allow support for new genres to be added without the logic layer or user interface having to change. But because I'm trying to focus on the SQL-NS APIs here and not digress into general database access code, I've just shown the simplified, hard-coded implementation.




Microsoft SQL Server 2005 Notification Services
Microsoft SQL Server 2005 Notification Services
ISBN: 0672327791
EAN: 2147483647
Year: 2006
Pages: 166
Authors: Shyam Pather

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