// // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // .NET StockTrader Sample WCF Application for Benchmarking, Performance Analysis and Design Considerations for Service-Oriented Applications //====================================================================================================== // The WCF client to the BSL. //====================================================================================================== using System; using System.Diagnostics; using System.Collections; using System.Collections.Generic; using System.Text; using System.Web; using Trade.StockTraderWebApplicationSettings; using Trade.StockTraderWebApplicationModelClasses; using Trade.BusinessServiceDataContract; using Trade.BusinessServiceContract; using Trade.Utility; namespace Trade.BusinessServiceClient { /// /// This is the business services client class that is called from StockTrader Web pages. /// public class BSLClient { private readonly ITradeServices BSL; /// /// Creating an Instance of BSLClient initializes the WCF Business Service from Config /// public BSLClient() { //Remote activation. //Note the same WCF client is used regardless of the //specific webservice implementation platform. For StockTrader, our client //interface for SOA modes is always the same: BusinessServiceClient. //We differentiate WebSphere only becuase StockTrader has some additional UI and //backend service functionality not provided by J2EE/Trade 6.1, and we need to detect //in just a couple of places in the Web app so we do not make method calls to an //implementation that has not implemented those methods. But as you see here, the //WCF client is always the same regardless. BSL = new BusinessServiceClient(); } /// /// Logs user in/authenticates against StockTrader database. /// /// User id to authenticate. /// Password for authentication public AccountDataUI login(string userid, string password) { try { AccountDataModel customer = BSL.login(userid, password); if (customer == null) return null; return new AccountDataUI((int)customer.accountID, customer.profileID, customer.creationDate, customer.openBalance, customer.logoutCount, customer.balance, customer.lastLogin, customer.loginCount); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.login Error: " + e.ToString()); throw; } } /// /// Gets recent orders for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. public TotalOrdersUI getOrders(string userID) { try { List orders = BSL.getOrders(userID); List ordersUI = new List(); decimal subtotalSell = 0; decimal subtotalBuy = 0; decimal subtotalTxnFee = 0; for (int i = 0; i < orders.Count; i++) { subtotalTxnFee += orders[i].orderFee; if (orders[i].orderType == StockTraderUtility.ORDER_TYPE_SELL) { subtotalSell += orders[i].price * (decimal)orders[i].quantity - orders[i].orderFee; } else { subtotalBuy += orders[i].price * (decimal) orders[i].quantity + orders[i].orderFee; } ordersUI.Add((convertOrderToUI(orders[i]))); } TotalOrdersUI totalOrders = new TotalOrdersUI(ordersUI, subtotalSell, subtotalBuy, subtotalTxnFee); return totalOrders; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getOrders Error: " + e.ToString()); throw; } } /// /// Gets specific top n orders for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. public TotalOrdersUI getTopOrders(string userID) { try { List orders = BSL.getTopOrders(userID); List ordersUI = new List(); decimal subtotalSell = 0; decimal subtotalBuy = 0; decimal subtotalTxnFee = 0; for (int i = 0; i < orders.Count; i++) { subtotalTxnFee += orders[i].orderFee; if (orders[i].orderType == StockTraderUtility.ORDER_TYPE_SELL) { subtotalSell += orders[i].price * (decimal)orders[i].quantity - orders[i].orderFee; } else { subtotalBuy += orders[i].price * (decimal)orders[i].quantity + orders[i].orderFee; } ordersUI.Add((convertOrderToUI(orders[i]))); } TotalOrdersUI totalOrders = new TotalOrdersUI(ordersUI, subtotalSell, subtotalBuy, subtotalTxnFee); return totalOrders; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getTopOrders Error: " + e.ToString()); throw; } } /// /// Gets account data for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. public AccountDataUI getAccountData(string userID) { try { AccountDataModel customer = BSL.getAccountData(userID); return new AccountDataUI((int)customer.accountID, customer.profileID, customer.creationDate, customer.openBalance, customer.logoutCount, customer.balance, customer.lastLogin, customer.loginCount); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getAccountData Error: " + e.ToString()); throw; } } /// /// Gets account profile data for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. public AccountProfileDataUI getAccountProfileData(string userID) { try { AccountProfileDataModel customerprofile = BSL.getAccountProfileData(userID); return new AccountProfileDataUI(userID, customerprofile.password, customerprofile.fullName, customerprofile.address, customerprofile.email, customerprofile.creditCard); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getAccountProfileData Error: " + e.ToString()); throw; } } /// /// Updates account profile data for a user. /// /// Profile data model class with updated info. public AccountProfileDataUI updateAccountProfile(AccountProfileDataUI customerprofile) { try { AccountProfileDataModel serviceLayerCustomerProfile = convertCustProfileFromUI(customerprofile); serviceLayerCustomerProfile = BSL.updateAccountProfile(serviceLayerCustomerProfile); return new AccountProfileDataUI(serviceLayerCustomerProfile.userID, serviceLayerCustomerProfile.password, serviceLayerCustomerProfile.fullName, serviceLayerCustomerProfile.address, serviceLayerCustomerProfile.email, serviceLayerCustomerProfile.creditCard); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.updateAccountProfile Error: " + e.ToString()); throw; } } /// /// Gets holding data for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. public Trade.StockTraderWebApplicationModelClasses.TotalHoldingsUI getHoldings(string userID) { try { List holdings = BSL.getHoldings(userID); List holdingsUI = new List(); decimal marketValue = 0; decimal gain = 0; decimal basis = 0; for (int i = 0; i < holdings.Count; i++) { //Note that here, we are constrained for interop purposes //with WebSphere to individually query for each stock price as a separate call for //each holding. The better approach is to get this data in a join query with the holding; //reducing web service round trips and database round trips. This is presumably becuase //of issues using entity beans with join statements and breaking the 1:1 object to table //relationship that entity beans are designed for. This is an example (there are others) //where entity beans/CMP and EJBQL can potentially reduce flexibility to "do the right" thing //with a relational database by not supporting ANSI SQL. At any rate, we follow Trade 6.1 here, and do extra queries and //Web service calls when they are not necessary. See the commented query in customer.cs //for a better way of handling, especially considering every time a holding comes back to //the UI, it also displays the current quote price for that holding. //A quite unnecessary Web service call for every holding: QuoteDataModel quote = BSL.getQuote(holdings[i].quoteID); HoldingDataUI holdingitem = new HoldingDataUI(holdings[i].holdingID, holdings[i].quantity, holdings[i].purchasePrice, holdings[i].purchaseDate.ToString(), holdings[i].quoteID, quote.price); holdingitem.convertNumericsForDisplay(false); holdingsUI.Add(holdingitem); decimal _marketValue = (decimal)holdings[i].quantity * quote.price; decimal _basis = (decimal)holdings[i].quantity * (decimal)holdings[i].purchasePrice; gain += _marketValue - _basis; marketValue += _marketValue; basis += _basis; } TotalHoldingsUI totalHoldings = new TotalHoldingsUI(holdingsUI, marketValue, basis, gain); return totalHoldings; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getHoldings Error: " + e.ToString()); throw; } } /// /// This routine allows us to take an unsorted list (or list sorted by HoldingID) of holdings, /// such as that returned by the WebSphere Trade 6.1 service, and return a sorted list of /// holdings by stock symbol. At the same time, we produce subtotal lines for each unique stock. /// This is used in a new page we added for StockTrader that Trade 6.1 does not have: /// (PortfolioBySymbol.aspx). Yet, it will work with the existing WebSphere backend service, /// since we do the sort here on the UI tier. We do it this way becuase WebSphere Trade 6.1 /// will always return an unsorted list of stock holdings since it does not implement a web service /// method to return a list sorted by quoteID. Without this limiting factor, a better and more /// performant way of doing this would be to implement a business services method and corresponding DAL method /// to execute a query that returned pre-sorted values by stock symbol (as opposed to unsorted, /// as the EJB/Trade 6.1 getHoldings operation does; or sorted by HoldingID as our getHoldings operation /// does. This would avoid the need to do a sort on the UI tier at all. The extra load this /// would place on the database would be negligable, given a typically small number of rows returned. /// At any rate, the sort is implemented here by taking advantage of a custom comparer in our /// HoldingsUI class on the quoteID property (stock symbol), and .NET's ability to sort /// generic typed lists. /// Hence, we can display even WebSphere-returned data that is non sorted in our extra /// PortfolioBySymbol page, adding the subtotal lines for each unique stock as well. /// We resisted adding a second method to the BSL and DAL to return a different sort order (quoteID), simply to /// be consistent across the InProcess and Web Service modes of operation and for true benchmark comparison /// purposes between the InProcess and web service modes. Note this logic is never called in /// published benchmark data, given the fact WebSphere Trade 6.1 does not implement this /// functionality, although even with the extra sort it performs very well. /// /// User id to retrieve data for. public Trade.StockTraderWebApplicationModelClasses.TotalHoldingsUI getHoldingsBySymbolSubTotaled(string userID) { try { //get the list of holdings from the BSL List holdings = BSL.getHoldings(userID); List holdingsUI = new List(); decimal marketValue = 0; decimal gain = 0; decimal basis = 0; //create our HoldingsUI class to pass back to the ASPX page. for (int i = 0; i < holdings.Count; i++) { //A quite unnecessary Web service call for every holding--again for Trade 6.1 compatibility: QuoteDataModel quote = BSL.getQuote(holdings[i].quoteID); HoldingDataUI holdingitem = new HoldingDataUI(holdings[i].holdingID, holdings[i].quantity, holdings[i].purchasePrice, holdings[i].purchaseDate.ToString(), holdings[i].quoteID, quote.price); holdingsUI.Add(holdingitem); decimal _marketValue = (decimal)holdings[i].quantity * quote.price; decimal _basis = (decimal)holdings[i].quantity * holdings[i].purchasePrice; gain += _marketValue - _basis; marketValue += _marketValue; basis += _basis; } //Call our implemented comparer class: see Trade.StockTraderWebApplicationModelClasses.HoldingDataUI.cs for the compararer //class source code. HoldingDataUI.HoldingDataUIComparer comparer = new HoldingDataUI.HoldingDataUIComparer(); comparer.ComparisonMethod = HoldingDataUI.HoldingDataUIComparer.ComparisonType.quoteID; //Do the sort! Sort method is built into the C# Generic List functionality; the comparer //calls our delegate method to compare by quoteID, so just one line of code here! holdingsUI.Sort(comparer); //Our list is now sorted, proceed with building in the subtotal lines. htmlRowBuilder rowBuilder = new htmlRowBuilder(); int uniqueStockCount = rowBuilder.buildPortfolioBySymbol(holdingsUI); TotalHoldingsUI totalHoldings = new TotalHoldingsUI(holdingsUI, marketValue, basis, gain, uniqueStockCount, holdingsUI.Count - uniqueStockCount); return totalHoldings; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getHoldingsBySymbolSubTotaled Error: " + e.ToString()); throw; } } /// /// Gets a holding for a user. Transforms data from DataContract to model UI class for HTML display. /// /// User id to retrieve data for. /// Holding id to retrieve data for. public HoldingDataUI getHolding(string userID, int holdingid) { try { HoldingDataModel holding = BSL.getHolding(userID,holdingid); QuoteDataModel quote = BSL.getQuote(holding.quoteID); HoldingDataUI holdingitem = new HoldingDataUI(holding.holdingID, holding.quantity, holding.purchasePrice, holding.purchaseDate, holding.quoteID, quote.price); return holdingitem; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getHolding Error: " + e.ToString()); throw; } } /// /// Logs a user out--updates logout count. /// /// User id to logout. public void logout(string userID) { try { BSL.logout(userID); return; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.logout Error: " + e.ToString()); } } /// /// Registers/adds new user to database. /// /// User id for account creation/login purposes as specified by user. /// Password as specified by user. /// Name as specified by user. /// Address as specified by user. /// Email as specified by user. /// Credit card number as specified by user. /// Open balance as specified by user. Might as well make it lots of $! public AccountDataUI register(string userID, string password, string fullname, string address, string email, string creditcard, decimal openBalance) { try { AccountDataModel customer = BSL.register(userID, password, fullname, address, email, creditcard, openBalance); return new AccountDataUI(customer.accountID, customer.profileID, customer.creationDate, customer.openBalance, customer.logoutCount, customer.balance, customer.lastLogin, customer.loginCount); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.register Error: " + e.ToString()); throw; } } /// /// Gets any closed orders for a user--orders that have been processed. Also updates status to complete. /// /// User id to retrieve data for. public List getClosedOrders(string userID) { try { List ordersUI = new List(); List orders = BSL.getClosedOrders(userID); for (int i = 0; i < orders.Count; i++) { ordersUI.Add((convertOrderToUI(orders[i]))); } return ordersUI; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getClosedOrders Error: " + e.ToString()); throw; } } /// /// Performs a stock buy operation. /// /// User id to create/submit order for. /// Stock symbol to buy. /// Shares to buy public OrderDataUI buy(string userID, string symbol, double quantity) { try { return convertOrderToUI(BSL.buy(userID, symbol, quantity, 0)); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.buy Error: " + e.ToString()); throw; } } /// /// Performs a holding sell operation. /// /// User id to create/submit order for. /// Holding id to sell off. /// Shares to sell. public OrderDataUI sell(string userID, int holdingID, double quantity) { try { if (quantity==0) return convertOrderToUI(BSL.sell(userID, holdingID, 0)); else return convertOrderToUI(BSL.sellEnhanced(userID, holdingID, quantity)); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.sell Error: " + e.ToString()); throw; } } /// /// Gets a list of stock quotes based on symbols. /// /// Symbols to get data for. public List getQuotes(string symbols) { try { string[] quotes = symbols.Split(new char[] { ' ', ',', ';' }); //Note: would be much more efficient to add and call a method getQuotes(string quoteString) to the // bsl class (TradeService), and do the parsing after the call to the bsl // since this would reduce round trips for multiple quotes. However, // WebSphere Trade 6.1 does not implement such a method, so for interop, // neither does .NET StockTrader. The performance cost is not so large here in the InProcess mode, // but it is in the web service modes where each call is remote, // and serialization/deserialization is happening with each QuoteDataModel object requested. List quoteList = new List(); foreach (string quote in quotes) { string stringquotetrim = quote.Trim(); if (!stringquotetrim.Equals("")) { QuoteDataUI quoteData = convertQuoteToUI(BSL.getQuote(stringquotetrim)); if (quoteData != null) { quoteList.Add(quoteData); } } } return quoteList; } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getQuotes Error: " + e.ToString()); throw; } } /// /// Gets a single quote based on symbol. /// /// Symbol to get data for. public QuoteDataUI getQuote(string symbol) { try { return convertQuoteToUI(BSL.getQuote(symbol)); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getQuote Error: " + e.ToString()); throw; } } /// /// Gets the current market summary. This results in an expensive DB query in the DAL; hence look to cache data returned for 60 second or so. /// public Trade.StockTraderWebApplicationModelClasses.MarketSummaryDataUI getMarketSummary() { try { return convertMarketSummaryDataToUI(BSL.getMarketSummary()); } catch (Exception e) { StockTraderUtility.Logger.WriteErrorMessage("StockTraderWebApplicationServiceClient.getMarketSummary Error: " + e.ToString()); throw; } } /// /// Converts from service data contract model class to a UI Model class for quick HTML display in ASPX pages. /// private QuoteDataUI convertQuoteToUI(QuoteDataModel quote) { if (quote != null) return new QuoteDataUI(quote.symbol, quote.companyName, quote.volume, quote.price, quote.open, quote.low, quote.high, quote.change); else return null; } /// /// Converts from service data contract model class to a UI Model class for quick HTML display in ASPX pages. /// private Trade.StockTraderWebApplicationModelClasses.MarketSummaryDataUI convertMarketSummaryDataToUI(MarketSummaryDataModelWS data) { List quoteGainers = new List(); List quoteLosers = new List(); for (int i = 0; i < data.topGainers.Count; i++) { QuoteDataModel quote = (QuoteDataModel)data.topGainers[i]; quoteGainers.Add((convertQuoteToUI(quote))); } for (int i = 0; i < data.topLosers.Count; i++) { QuoteDataModel quote = (QuoteDataModel)data.topLosers[i]; quoteLosers.Add((convertQuoteToUI(quote))); } return new MarketSummaryDataUI(data.TSIA, data.openTSIA, data.volume, quoteGainers, quoteLosers, data.summaryDate); } /// /// Converts from service data contract model class to a UI Model class for quick HTML display in ASPX pages. /// private OrderDataUI convertOrderToUI(OrderDataModel order) { string completionDate; if (order != null) { //MinValue used to indicate "null" data in the DB; equates to a pending order. if (order.completionDate == DateTime.MinValue) completionDate = "Pending"; else completionDate = order.completionDate.ToString(); return new OrderDataUI(order.orderID, order.orderType, order.orderStatus, order.openDate, completionDate, order.quantity, order.price, order.orderFee, order.symbol); } else return null; } /// /// Converts from service data contract model class to a UI Model class for quick HTML display in ASPX pages. /// private AccountDataModel convertAccountDataFromUI(AccountDataUI customer) { AccountDataModel serviceLayerCustomer = new AccountDataModel(); serviceLayerCustomer.accountID = (int)customer.accountID; serviceLayerCustomer.balance = customer.balance; serviceLayerCustomer.creationDate = customer.creationDate; serviceLayerCustomer.lastLogin = customer.lastLogin; serviceLayerCustomer.logoutCount = customer.logoutCount; serviceLayerCustomer.openBalance = customer.openBalance; serviceLayerCustomer.profileID = customer.profileID; return serviceLayerCustomer; } /// /// Converts from service data contract model class to a UI Model class for quick HTML display in ASPX pages. /// private AccountProfileDataModel convertCustProfileFromUI(AccountProfileDataUI customerprofile) { AccountProfileDataModel serviceLayerCustomerProfile = new AccountProfileDataModel(); serviceLayerCustomerProfile.password = customerprofile.password; serviceLayerCustomerProfile.address = customerprofile.address; serviceLayerCustomerProfile.creditCard = customerprofile.creditCard; serviceLayerCustomerProfile.email = customerprofile.email; serviceLayerCustomerProfile.fullName = customerprofile.fullName; serviceLayerCustomerProfile.userID = customerprofile.userID; return serviceLayerCustomerProfile; } } }