| Misbah's profile.NET HITMANPhotosBlog | Help |
.NET HITMANMisbah Arefin's Coding Blog |
|||||
|
|
March 06 Select top n rows from a table for each groupWe have a retail shop online and with just 2 weeks of our website launch we already have close to 30 orders. Now of course marketing wanted to get some ads up on the site to derive more orders and one for the reports was to get the top two products from each manufacturer which have the best promotional price. A GROU BY clause immediately comes to mind for the above scenario, but SQL 2005/2008 offer a much better solution. SELECT ProductId, MfgPartNumber, DescriptionText, MfgListPrice, SellPrice, SavingsPercentage FROM ( SELECT ROW_NUMBER() OVER(PARTITION BY P.MfgCode ORDER BY (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice DESC) AS RowNumber, P.ProductId, P.MfgPartNumber, P.DescriptionText, P.MfgListPrice, PR.SellPrice, (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice AS SavingsPercentage FROM Product P INNER JOIN Price PR ON P.ProductId = PR.ProductId INNER JOIN SpecialPricingXRef SP ON PR.PriceGroupId = SP.PriceGroupId WHERE SP.PricingType = 'Promo' AND P.MfgListPrice > PR.SellPrice ) AS InnerTable WHERE RowNumber < 3 ORDER BY SavingsPercentage DESC; Aside from normal use of the ROW_NUMBER() function to generate sequence numbers for the result set the above is the perfect scenario where the ROW_NUMBER() function is really useful. ROW_NUMBER() function creates a new column in the result set with a unique / incremental number for each row. If the PARTITION BY clause (think of a partition as a category or group) is also specified then the sequence number of a row is reset to 1 for each new partition / category. Lets dissect the above query; following query returns all the products which have a promo price record with a RowNumber incremented for each row. I’ll get to why we need the RowNumber in our query later. SELECT ROW_NUMBER() OVER(ORDER BY (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice DESC) AS RowNumber, P.ProductId, P.MfgPartNumber, P.DescriptionText, P.MfgListPrice, PR.SellPrice, (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice AS SavingsPercentage FROM Product P INNER JOIN Price PR ON P.ProductId = PR.ProductId INNER JOIN SpecialPricingXRef SP ON PR.PriceGroupId = SP.PriceGroupId WHERE SP.PricingType = 'Promo' AND P.MfgListPrice > PR.SellPrice Adding the PARTITION BY clause to the above changes the output slightly. When the PARTITION BY clause is also specified the sequence number generated by ROW_NUMBER() is reset to 1 for each new partition. In concept the PARTITION BY clause is similar to a GROUP BY except it only applies to the function and not the select as a whole. SELECT ROW_NUMBER() OVER(PARTITION BY P.MfgCode ORDER BY (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice DESC) AS RowNumber, P.ProductId, P.MfgPartNumber, P.DescriptionText, P.MfgListPrice, PR.SellPrice, (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice AS SavingsPercentage FROM Product P INNER JOIN Price PR ON P.ProductId = PR.ProductId INNER JOIN SpecialPricingXRef SP ON PR.PriceGroupId = SP.PriceGroupId WHERE SP.PricingType = 'Promo' AND P.MfgListPrice > PR.SellPrice The where clause in our outer select is just filtering the results so that only those rows with RowNumber less than 3 are returned. Recall from our previous query that RowNumber is reset to 1 for each manufacturer; so to get just the top 2 rows for each manufacturer we need to get rows where RowNumber is 1 and 2 (< 3). The result is the list of 2 rows with highest savings for each manufacturer… simple really. P.S. If you prefer Common Table Expressions then you can rewrite the query to: WITH InnerTable AS( SELECT ROW_NUMBER() OVER(PARTITION BY P.MfgCode ORDER BY (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice DESC) AS RowNumber, P.ProductId, P.MfgPartNumber, P.DescriptionText, P.MfgListPrice, PR.SellPrice, (P.MfgListPrice - PR.SellPrice) / P.MfgListPrice AS SavingsPercentage FROM Product P INNER JOIN Price PR ON P.ProductId = PR.ProductId INNER JOIN SpecialPricingXRef SP ON PR.PriceGroupId = SP.PriceGroupId WHERE SP.PricingType = 'Promo' AND P.MfgListPrice > PR.SellPrice ) SELECT ProductId, MfgPartNumber, DescriptionText, MfgListPrice, SellPrice, SavingsPercentage FROM InnerTable WHERE RowNumber <= 3 ORDER BY SavingsPercentage DESC; the query plan for sub query vs CTE is the same so its all a matter of choice and readability. February 06 Caching Application Block and database backing storeCaching can help to overcome some of the challenges associated with enterprise-scale distributed web applications:
Out of the box ASP.NET provides three primary forms of caching:
By using the Caching Application Block we can write a consistent form of code to implement caching in any application component, be it the web UI, a Windows service, a WinForm desktop application, or a web service. The Caching Application Block is optimized for performance and is both thread safe and exception safe. The Caching Application Block works with ASP.NET cache and provides a number of features that are not available to the ASP.NET cache such as:
To use the Caching Application Block you need to add references of the following assemblies to your project:
The following namespaces need to be included in the classes that use the Caching Block:
If there is a requirement for a persistent backing store then the data access block needs to be included:
If there is a requirement to encrypt data in the persistent backing store then the encryption block needs to be included:
Configuration the Cache BlockIn Memory Cache<configSections> <section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections> <cachingConfiguration> <cacheManagers> <add expirationPollFrequencyInSeconds="60" maximumElementsInCacheBeforeScavenging="10" numberToRemoveWhenScavenging="5" backingStoreName="Null Storage" name="Prices" /> </cacheManagers> <backingStores> <add encryptionProviderName="" type="Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations .NullBackingStore, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" name="Null Storage" /> </backingStores> </cachingConfiguration> Cache Using Backing StoreUse the database backing store provider when deploying your application on a web farm on multiple computers or on multiple processes on the same machine scenario. To use the database backing store you need to first create the cache database on SQL Server. The script to do this can be found in <Enterprise Library Source Dir>\App Blocks\Src\Caching\Database\Scripts. <configSections> <section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <section name="cachingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Caching.Configuration.CacheManagerSettings, Microsoft.Practices.EnterpriseLibrary.Caching, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </configSections> <dataConfiguration defaultDatabase="Northwind" /> <connectionStrings> <add name="CacheDSN" connectionString="Data Source=(local);Initial Catalog=Caching;Integrated Security=True;User Instance=False" providerName="System.Data.SqlClient" /> <add name="Northwind" connectionString="Data Source=(local);Initial Catalog=Northwind;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <cachingConfiguration defaultCacheManager="Customers"> <cacheManagers> <add expirationPollFrequencyInSeconds="60" maximumElementsInCacheBeforeScavenging="11000" numberToRemoveWhenScavenging="10" backingStoreName="DataStorage" name="Customers" /> </cacheManagers> <backingStores> <add databaseInstanceName="CacheDSN" partitionName="MyFirstCacheApp" encryptionProviderName="" type="Microsoft.Practices.EnterpriseLibrary.Caching.Database.DataBackingStore, Microsoft.Practices.EnterpriseLibrary.Caching.Database, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" name="DataStorage" /> </backingStores> </cachingConfiguration> Cache Application Block Class ReferenceCacheFactory Class - The CacheFactory uses the supplied configuration information to determine the type of cache object to construct GetCacheManager - The GetCacheManager method returns a CacheManager object determined by the configuration information ICacheManager myCache = CacheFactory.GetCacheManager(); //uses the default cache specified in configuration ICacheManager myCustomerCache = CacheManager.GetCacheManager("Customers"); //overload creates the name cache CustomerData CacheManager Class - The CacheManger class acts as the interface between the application and the rest of the caching block. It provides all the methods required to manage the applications GetData - The GetData method returns an object from the cache containing the data that matches the supplied ID. If the data does not exist or if it has expired Null is returned Customer oCustomer = myCustomerCache.GetData("CustomerID"); Add - The Add method will add an item to the cache myCustomerCache.Add("CustomerID", oCustomer); myCustomerCache.Add("CustomerID", oCustomer, scavengingPriority, refreshAction, cacheExpirations); Contains - The Contains method returns true if the item exists in the cache bool dataExists = myCustomerCache.Contains("CustomerID"); Remove - The Remove method will delete an item from the cache myCustomerCache.Remove("CustomerID"); Flush - The Flush method removes all items from the cache. If an error occurs during the flush the cache is left unchanged myCustomerCache.Flush(); Monitoring Your Cache PerformanceMonitoring your cache usage and performance can help you understand whether your cache is performing as expected and helps you to fine tune your cache solution. You can use the Windows performance monitor application (Perfmon) to view and analyze your cache performance data when it is not delivering the expected performance. To monitor cache performance
Regular monitoring of your cache should highlight any changes in data use and any bottlenecks that these might introduce. This is the main management task associated with the post-deployment phase of using a caching system. Synchronizing Caches in a Server FarmA common problem for distributed applications developers is how you synchronize cached data between all servers in the farm. Generally speaking, if you have a situation in which your cache needs to be synchronized in your server farm, it almost always means that your original design is faulty. You should design your application with clustering in mind and avoid such situations in the first place. You can configure the Cache Application Block to share the backing store between servers in a web farm. All machines in the farm can have the same cache instance and partition and can read/write to the store. But the in-memory version of the cache is always unique to each server in the farm. However, if you have one of those rare situations where such synchronization is absolutely required, you should use file dependencies to invalidate the cache when the information in the main data store changes. To create file dependencies for cache synchronization
NOTE: because replicating a file across the server farm can take time, it is inefficient in cases where the cached data changes every few seconds. The Caching Application Block should not be used if:
January 11 Microsoft AntiXSS LibraryCross site scripting (XSS) is the most common web application vulnerability and is listed in the Top 10 web application vulnerabilities on OWASP. XSS can also be called HTML injection attack, it occurs when un-validated user input is inserted into HTML output. This allows the attacker to construct a URL with HTML input and get it executed on the browser in the user's context. This attack can be used to extract cookie information, steal sessions, write new html tags, invoke ActiveX controls, etc. Essentially, anything that can be done with a browser can be done with this attack without the user's knowledge. Microsoft AntiXSS LibraryMicrosoft recently released the v3.0 beta for AntiXSS library which helps you to protect your current applications from cross-site scripting attacks, at the same time helping you to protect your legacy application with its Security Runtime Engine. The Microsoft Anti-Cross Site Scripting Library is an encoding library, provided by the ASP.NET and Application Consulting & Engineering (ACE) teams at Microsoft, designed to help developers protect their Web-based applications from XSS attacks.AntiXSS 3.0 is a powerful tool in the Microsoft toolbox that mitigates XSS risks and provides a consistent level of security allowing you to focus on solving business problems and not on security problems. Whats new in AntiXSS 3.0:
This library differs from most encoding libraries in that it uses the principle-of-inclusions technique to provide protection against XSS attacks. This approach works by defining a valid or allowed set of characters, treating anything outside this set as invalid characters or potential attacks and encoding it. Cross-site scripting (XSS) attacks exploit vulnerabilities in Web-based applications that fail to properly validate and/or encode input that is embedded in response data. Malicious users can then inject client-side script into response data causing the unsuspecting user's browser to execute the script code. The script code will appear to have originated from a trusted site and may be able to bypass browser protection mechanisms such as security zones. These attacks are platform and browser independent, and can allow malicious users to perform undesired actions such as gaining unauthorized access to client data like cookies or hijacking sessions entirely. In order for a malicious user to conduct an XSS attack against the application, they first need to find a page where all of the following are true:
A malicious user can easily exploit this by tricking a user into visiting the page while passing script such as <script>alert('Virus Alert!')</script> into one of the parameters. The script injected by the malicious user gets executed in the unsuspecting user's Web browser. To protect our application from XSS attacks we first need to understand the methods that malicious users can use to conduct such attacks. We can do using the following steps:
Following will be helpful in determining which encoding method to use: HtmlEncode - Untrusted input is used in HTML output except when assigning to an HTML attribute HtmlAttributeEncode - Untrusted input is used as an HTML attribute JavaScriptEncode - Untrusted input is used within a JavaScript context UrlEncode - Untrusted input is used in a URL (such as a value in a querystring) VisualBasicScriptEncode - Untrusted input is used within a Visual Basic Script context XmlEncode - Untrusted input is used in XML output, except when assigning to an XML attribute XmlAttributeEncode - Untrusted input is used as an XML attribute
After we've added the reference to the Anti-Cross Site Scripting Library, we encode the output generated by the our page. To do this:
Additional StepsTo make malicious users' jobs even harder, there are some additional layers of defense that can be implemented to further prevent XSS attacks in the our application.
ConclusionXSS attacks are easily one of the most common encountered by IT teams, and with the number of Web applications increasing each day, that number is expected to continue growing. Developers need to protect their application users from such attacks by:
NOTE: remember, a common mistake is to encode untrusted input more than once, which can result in outputs being displayed incorrectly. For more information on XSS attacks, some good references are: November 09 Creating a generic CLR audit triggerThere’s an interesting article at SqlJunkies http://sqljunkies.com/Article/4CD01686-5178-490C-A90A-5AEEF5E35915.scuk which shows how to create a generic CLR audit trigger. The audit trigger works great and includes tracking of:
But there is just one small problem with this trigger code. The PerformedBy column of the Audit table in the sample code is set to the UserID of the connection string which in most applications would be the same UserID for all connections because of connection pooling. This means the trigger will log all operations performed by the application but it will not log the real user (application user) who made the change. So the first step was to make sure that the application logged in user’s UserID is passed in to all the CRUD stored procedures from my web application. CREATE PROCEDURE spSomeProc( ... @PerformedByUserId VARCHAR(32)) AS BEGIN SET NOCOUNT ON; ... RETURN; END GO You can get the application logged in user’s UserID from HttpContext.Current.User.Identity.Name or Thread.CurrentPrincipal.Identity.Name and pass this value from the application to the CRUD stored procedure. Now that we have a way to pass in the UserID to our stored procedures, we need to somehow pass this parameter value to the trigger code. Although there is no direct way of passing a parameter down to the trigger, the forums at SqlJunkies suggested to create a temporary table in your procedure, insert the UserID as the only row in the temporary table and then retrieve this value in the CLR trigger by executing a query against this temporary table. Since the trigger uses the same connection session we don't need to create a global temporary table. This solution works fine but it uses the tempdb which is a bit of a concern as a load test scenario performing CRUD operations could easily bloat up the tempdb. Another option is to pass in the UserID via the CONTEXT_INFO, which frees up tempdb for other tasks. We can do so with the following code snippet added at the beginning of every CRUD stored procedure CREATE PROCEDURE spSomeProc( ... @PerformedByUserId VARCHAR(32)) AS BEGIN SET NOCOUNT ON; DECLARE @BinaryUserId VARBINARY(128); SET @BinaryUserId = CAST(@PerformedByUserId AS VARBINARY(128)); SET CONTEXT_INFO @BinaryUserId; ... SET CONTEXT_INFO 0x0; RETURN; END GO and in the CLR trigger you can get the CONTEXT_INFO as: ... oCmd.CommandText = "SELECT CAST(CONTEXT_INFO() AS VARCHAR(128))"; string userid = (string)oCmd.ExecuteScalar(); ... We can also add an additional check here to get the UserID from “SELECT CURRENT_USER” just to make sure that if the CONTEXT_INFO is not set or returns NULL as in the case where some DBA made a change to any of the tables directly (for production support issue or whatever). SqlCommand CurrentUserCmd = new SqlCommand("SELECT CAST(CONTEXT_INFO() AS VARCHAR(128))", Connection); string CurrentUser = CurrentUserCmd.ExecuteScalar().ToString(); if (string.IsNullOrEmpty(CurrentUser)) { CurrentUserCmd.CommandText = "SELECT CURRENT_USER"; CurrentUser = CurrentUserCmd.ExecuteScalar().ToString(); } The full source code from the original article and modified as described above (converted to C#) is given below using System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; using Microsoft.SqlServer.Server; public partial class Triggers { //This is the original template for Trigger metadata. Note that it is table-specific (i.e. it suggests that the trigger should apply to one table only). //<Microsoft.SqlServer.Server.SqlTrigger(Name:="Trigger1", Target:="Table1", Event:="FOR UPDATE")> _ //This is our actual declaration. Note that it does not specify any particular table. We don't know if it is Microsoft's intention to allow table-agnostic trigger code, but this works and we hope that it keeps working. //GENERIC AUDIT TRIGGER: AuditCommon [Microsoft.SqlServer.Server.SqlTrigger(Name = "AuditCommon", Event = "FOR UPDATE, INSERT, DELETE")] public static void AuditCommon() { try { #if(DEBUG) EmitDebugMessage("Enter Trigger"); #endif //Grab the already-open Connection to use as an argument #if(DEBUG) EmitDebugMessage("Open Connection"); #endif SqlTriggerContext Context = SqlContext.TriggerContext; SqlConnection Connection = new SqlConnection("CONTEXT CONNECTION=TRUE"); Connection.Open(); //Load the "inserted" table #if(DEBUG) EmitDebugMessage("Load INSERTED"); #endif SqlDataAdapter TableLoader = new SqlDataAdapter("SELECT * FROM INSERTED", Connection); DataTable InsertedTable = new DataTable(); TableLoader.Fill(InsertedTable); //Load the "deleted" table #if(DEBUG) EmitDebugMessage("Load DELETED"); #endif TableLoader.SelectCommand.CommandText = "SELECT * FROM DELETED"; DataTable DeletedTable = new DataTable(); TableLoader.Fill(DeletedTable); //Prepare the "audit" table for insertion #if(DEBUG) EmitDebugMessage("Load AUDIT schema for insertion"); #endif SqlDataAdapter AuditAdapter = new SqlDataAdapter("SELECT * FROM AUDIT WHERE 1 = 0", Connection); DataTable AuditTable = new DataTable(); AuditAdapter.FillSchema(AuditTable, SchemaType.Source); SqlCommandBuilder AuditCommandBuilder = new SqlCommandBuilder(AuditAdapter); //Create DataRow objects corresponding to the trigger table rows. #if(DEBUG) EmitDebugMessage("Create internal representations of trigger table rows"); #endif string TableName = ""; DataRow InsertedRow = null; if (InsertedTable.Rows.Count > 0) { InsertedRow = InsertedTable.Rows[0]; TableName = DeriveTableNameFromKeyFieldName(InsertedTable.Columns[0].ColumnName); } DataRow DeletedRow = null; if (DeletedTable.Rows.Count > 0) { DeletedRow = DeletedTable.Rows[0]; TableName = DeriveTableNameFromKeyFieldName(DeletedTable.Columns[0].ColumnName); } //get the current database user SqlCommand CurrentUserCmd = new SqlCommand("SELECT CAST(CONTEXT_INFO() AS VARCHAR(128))", Connection); string CurrentUser = CurrentUserCmd.ExecuteScalar().ToString(); if (string.IsNullOrEmpty(CurrentUser)) { CurrentUserCmd.CommandText = "SELECT CURRENT_USER"; CurrentUser = CurrentUserCmd.ExecuteScalar().ToString(); } //Perform different audits based on the type of action. switch (Context.TriggerAction) { case TriggerAction.Update: //Ensure that both INSERTED and DELETED are populated. If not, this is not a valid update. if (InsertedRow != null && DeletedRow != null) { //Walk through all the columns of the table. foreach (DataColumn Column in InsertedTable.Columns) { //ALTERNATIVE CODE to compare values and record only if they are different: //If Not DeletedRow.Item(Column.Ordinal).Equals(InsertedRow.Item(Column.Ordinal)) Then //This code records any attempt to update, whether the new value is different or not. if (Context.IsUpdatedColumn(Column.Ordinal)) { //DEBUG output indicating field change #if(DEBUG) EmitDebugMessage("Create UPDATE Audit: Column Name = " + Column.ColumnName + ", Old Value = '" + DeletedRow[Column.Ordinal].ToString() + "'" + ", New Value = '" + InsertedRow[Column.Ordinal].ToString() + "'"); #endif //Create audit record indicating field change DataRow AuditRow = AuditTable.NewRow(); //populate fields common to all audit records long RowId = (long)InsertedRow[0]; //use "Inserted.TableName" when Microsoft fixes the CLR to supply it WriteCommonAuditData(AuditRow, TableName, RowId, CurrentUser, "UPDATE"); //write update-specific fields AuditRow["FieldName"] = Column.ColumnName; AuditRow["OldValue"] = DeletedRow[Column.Ordinal].ToString(); AuditRow["NewValue"] = InsertedRow[Column.Ordinal].ToString(); //insert the new row into the audit table AuditTable.Rows.InsertAt(AuditRow, 0); } } } break; case TriggerAction.Insert: //If the INSERTED row is not populated, then this is not a valid insertion. if (InsertedRow != null) { //DEBUG output indicating row insertion #if(DEBUG) EmitDebugMessage("Create INSERT Audit: Row = '" + InsertedRow[0].ToString() + "'"); #endif //Create audit record indicating field change DataRow AuditRow = AuditTable.NewRow(); //populate fields common to all audit records long RowId = (long)InsertedRow[0]; //use "Inserted.TableName" when Microsoft fixes the CLR to supply it WriteCommonAuditData(AuditRow, TableName, RowId, CurrentUser, "INSERT"); //insert the new row into the audit table AuditTable.Rows.InsertAt(AuditRow, 0); } break; case TriggerAction.Delete: //If the DELETED row is not populated, then this is not a valid deletion. if (DeletedRow != null) { //DEBUG output indicating row insertion #if(DEBUG) EmitDebugMessage("Create DELETE Audit: Row = '" + DeletedRow[0].ToString() + "'"); #endif //Create audit record indicating field change DataRow AuditRow = AuditTable.NewRow(); //populate fields common to all audit records long RowId = (long)DeletedRow[0]; //use "Inserted.TableName" when Microsoft fixes the CLR to supply it WriteCommonAuditData(AuditRow, TableName, RowId, CurrentUser, "DELETE"); //insert the new row into the audit table AuditTable.Rows.InsertAt(AuditRow, 0); } break; } //update the audit table AuditAdapter.Update(AuditTable); //finish #if(DEBUG) EmitDebugMessage("Exit Trigger"); #endif } catch (Exception ex) { //Put exception handling code here if you want to connect this to your database-based error logging system. Without this Try/Catch block, any error in the trigger routine will stop the event that fired the trigger. This is early-stage development and we're not expecting any exceptions, so for the moment we just need to know about them if they occur. throw; } } //Write data into the fields of an Audit table row that is common to all types of audit activities. private static void WriteCommonAuditData(DataRow auditRow, string tableName, long rowId, string currentUser, string operation) { auditRow["TableName"] = tableName; auditRow["RowId"] = rowId; auditRow["OccurredAt"] = DateTime.Now; auditRow["PerformedBy"] = currentUser; auditRow["Operation"] = operation; } //SQL CLR does not deliver the proper table name from either InsertedTable.TableName or DeletedTable.TableName, so we must use a substitute based on our key naming convention. We assume that in each table, the KeyFieldName = TableName + "Id". Remove this routine and its uses as soon as we can get the table name from the CLR. private static string DeriveTableNameFromKeyFieldName(string keyFieldName) { return keyFieldName.Substring(0, keyFieldName.Length - 2); //assumes KeyName = TableName & "Id" } //Emit debug messages. This function gives us the option to turn off debugging messages by changing one value (here). #if(DEBUG) private static void EmitDebugMessage(string message) { SqlContext.Pipe.Send(message); } #endif }
September 08 Unity - Dependency Injection and Inversion of Control ContainerThe Dependency Injection Pattern The Old Way
Problem
Service Locator pattern attempts to solve some of the problems mentioned above by providing a dictionary of objects. The objects are all stored in this dictionary and the Get method simply returns the object to the caller. Service Locator Example
Pros
Cons
Dependency Injection Containers
In a nutshell, dependency injection just means that a given class or system is no longer responsible for instantiating their own dependencies. In this case “Inversion of Control” refers to moving the responsibility for locating and attaching dependency objects to another class or a DI tool. That might not sound that terribly profound, but it opens the door for a lot of interesting scenarios.
Unity Application Block The Unity Application Block includes the following features:
Unity has no dependency on Enterprise Library core and can be used without having to install Enterprise Library on the host system. To use Unity in your application you need to add reference to the following dlls in your project The Unity container can be configured through configuration files or you can use code to register dependencies dynamically at run time. To use Unity with configuration files you need to add reference to the following dll Steps when using Dependency Injection
Setup the container We create a simple interface for the container property so that we can access our container using this interface
class in the Global.asax file:
The BuildContainer method is where we will setup our container and register our types for dependency injection. The RegisterType<TFrom, TTo>() method tells Unity that whenever someone asks for a dependency on TFrom give them Tto. In the example code below the statement container.RegisterType<ILogger, EventLogLogger>() tells Unity that whenever someone has a dependency on type ILogger go ahead and create an object of type EventLogLogger. There are a couple of different flavors of Dependency Injection
Constructor Injection
If a class that developers instantiate using the Resolve method of the Unity container has a constructor that defines one or more dependencies on other classes, the Unity container will automatically create the dependent object instance specified in parameters of the constructor. In the above example StockQuotes has a dependency on ILogger. When we create an instance of the StockQuotes class using the Resolve method of the Unity container, Unity will automatically create an instance of EventLogLogger and pass it to the constructor of StockQuotes class. The benefit of using constructor injection is that the constructor function now explicitly declares the dependencies of a class. Constructor injection is often recommended as it eliminates chatty calls to the object and creates a valid object in as few steps as possible. Setter Injection
To force dependency injection of the dependent object, developers must apply the [Dependency] attribute to the property declaration. Many would argue that setter injection is really useful when legacy code needs to be upgraded and provides a smooth transition from legacy code to the new model. Making sure that any new code that depends on undesirable legacy code uses Dependency Injection leaves an easier migration path to eliminate the legacy code later with all new code. As a service locator
Here we are just telling unity to give us the ILogger interface which is already registered with the container. Using the container in this manner makes it a service locator. Unity Dependency injection provides a number of ways to configure the container. As described above you use the RegisterType method to inform the container about dependencies. But Unity can also manage the object lifetime e.g. Dependencies as singleton
The above code tells Unity that whenever someone asks for type Database give them type SqlDatabase and return the same object every time instead of creating a new one for each dependency. Named Instance
Named instance allows you to configure Unity with multiple dependencies for the same type but assign them different names. Thi s allows you to do fancy stuff where you want a default type mapping but also want to override the mapping by providing a name during object creation. Registering an existing object instance
When using RegisterIntance Unity will automatically make the objects singletons. Configuring Unity via config file Use following code to read the container setup from configuration file:
.config file:
Nested Containers
While registering types with Unity there is a certain risk of introducing unintentional circular references, which are not easy to detect or prevent.
It is the responsibility of the developer to prevent this type of error by ensuring that the members of classes they use with dependency injection do not contain circular references. References
|
||||
|
|