Enterprise Library on Windows Azure: Key Learnings from Customer Project
The following blog post summarizes some of my personal key learnings from a recent customer project where I have successfully been able to leverage Enterprise Library 5.0 (EntLib) on the Windows Azure platform and take advantage of several capabilities provided by the EntLib framework and its building blocks.
This post assumes your familiarity with Enterprise Library as a whole. For the sake of brevity, I dropped the part of this discussion related to some colorful questions like “What is Enterprise Library?”, “Why Enterprise Library on Azure?” or “Isn’t the Enterprise Library an overkill?”. I would be happy to elaborate on these topics should such dialog be required for some of you making implementation decisions in your Windows Azure solutions.
OK, let’s get started. In the next sections, I’m going to lay down my lessons learned including some issues I had to deal with, some challenges I had to overcome and general observations listed in no particular order. For each point, I will try and be sufficiently descriptive to provide you with essential context as well as refer to the solution or workaround that helped me get out of blocking or annoying state.
EntLib Assembly References in Azure Role Projects
The EntLib 5.0 core codebase is comprised of about 30 assemblies. You barely notice the dependency between them until it strikes when you need to deploy your solution that uses EntLib on Windows Azure.
Let’s say, your worker role project needs to use the Data Access Application Block. You add a reference to Microsoft.Practices.EnterpriseLibrary.Data.dll assembly and think this would suffice. The project may even run successfully on Dev Fabric without manifesting any evidence of a potential problem. You deploy your package on Windows Azure and it gets stuck in Initializing state forever, periodically cycling between Busy and Stopping statuses. Among other potential reasons why it could have happened, here is what I have encountered in my project.
The assemblies from which the EntLib’s application blocks are comprised of depend on a few core components through assembly references. For example, the Data Access Application Block’s assembly depends on the configuration framework and also uses the Unity Application Block for dynamic type instantiation. The MSDN documentation explains all dependencies between Enterprise Library assemblies however unless you read the manual and some additional materials on correct approach to referencing EntLib assemblies, it may not be obvious from the beginning.
Failing to include all dependent assemblies into a deployment package leads to a situation whereby the Windows Azure application cannot be successfully spun off and executed on the cloud. The IntelliTrace log is likely to show some evidence in the form of a System.IO.FileNotFoundException error indicating that some specific dependent assembly is missing. To avoid this problem, I followed a simple set of steps.
First, I explicitly referenced all EntLib core assemblies in my Visual Studio projects that correspond to Windows Azure role implementations. This includes the EntLib assemblies that are not directly being used by the worker role project, but rather present themselves as secondary references. For example, I referenced the entire chain of dependent assemblies used by the Data Access Application Block (DAB) although my worker role project does call into any of these except the DAB component.
Second, I set the Copy Local setting of each assembly reference to True. This guarantees that EntLib assemblies will be included into the CSPKG deployment package. The Copy Local setting is defaulted to False for all referenced assemblies which are found in the GAC. If EntLib assemblies were GAC’ed on the development machine, this will make them first-class victims for not being present in a CSPKG file unless the above setting is modified. I have also observed a strange behavior – an assembly reference that was configured with “Copy Local = True” was absent from the deployment package. Resetting this parameter from True to False and then back to True solved the problem. I don’t have any explanations as to why this may have occurred.
While troubleshooting assembly reference issues, I came across a magical lamp with a genie inside. The genie’s name is _CSPACK_FORCE_NOENCRYPT_ and it is an environmental variable that quickly helped me understand what goes into my CSPKG packages and what is missing. It is also very useful for exploring the internals of the Windows Azure deployment packages.
Logging Block Configuration Simplified
If you decide to take dependency on the Logging Application Block in your solutions, you might be impressed with how much various configuration “stuff” goes into your application’s .config file. After adding the <loggingSettings> configuration section that is required for the above block, the app.config file explodes with numerous new definitions of trace listeners, log entry formatters, event sources and intra-artifact relationships expressed in XML form with a strong developer accent. Even the initial configuration is so rich and complex, and as such it would require you to use the shiny new configuration tool shipped with Enterprise Library 5.0 (which is recommended for authoring complex configurations since early versions of EntLib) or craft a configuration file manually by taking advantage of the XML schema with IntelliSense support.
I was looking for ways of reducing the amount of configuration settings which EntLib was forcing me to stick into my configuration files. So, I may have found a solution. My assumption here was that there are many default logging settings that change very rarely and hence can be configured programmatically as opposed to be expressed declaratively in a configuration file. This includes such definitions as standard trace listeners and their default settings, standard formatters with default parameters, standard event sources and their mappings to trace listeners.
Instead of overloading my app.config with logging settings that never (or extremely rarely) change, I have decided to move many default settings into the code by using the following approach. I have wrapped the LoggingSettings object into a helper class that is responsible for applying the default configuration and providing simplified operations against the various collections of logging artifacts. Here is how it was accomplished.
First, a new component called LoggingConfigurationView was introduced. It expects an instance of the LoggingSettings object on top of which it creates an “updatable view” which provides the simplified mechanism for adding new trace listeners (via AddTraceListener method), configuring default trace listeners (e.g. via ConfigureEventLogTraceListener method) and enabling the trace listeners for specified event categories (via EnableListenerForEventCategory method).
/// Wraps the Enterprise Library's Logging Application Block configuration and provides simplified operations with/// the underlying configuration data. public class LoggingConfigurationView { // ... There was some infrastructure code which was omitted for brevity ... /// Initializes a new instance of a LoggingConfigurationView object based on the specified logging settings. public LoggingConfigurationView(LoggingSettings loggingSettings) { this.loggingSettings = loggingSettings; ApplyDefaultGlobalSettings(); ApplyDefaultFormatters(); ApplyDefaultTraceListener(); } /// <summary> /// Adds a new trace listener associated with an unique name and a type containing the implementation. /// </summary> /// <param name="name">The unique name under which a new trace listener will be added to the collection.</param> /// <param name="listenerType">The type implementing the new trace listener.</param> public void AddTraceListener(string name, Type listenerType) { /* ... */ } /// <summary> /// Configures the Event Log trace listener with the specified machine name, log name and log source. /// </summary> /// <param name="machineName">The machine name.</param> /// <param name="logName">The event log name.</param> /// <param name="logSource">The event log source name.</param> public void ConfigureEventLogTraceListener(string machineName, string logName, string logSource) { /* ... */ } /// <summary> /// Configures the Rolling Flat File trace listener with the specified file name and parameters. /// </summary> /// <param name="fileName">The log file name.</param> /// <param name="maxArchivedFiles">The maximum number of archived files to keep.</param> /// <param name="rollSizeKB">The maximum file size (KB) before rolling.</param> public void ConfigureRollingFlatFileTraceListener(string fileName, int maxArchivedFiles, int rollSizeKB) { /* ... */ } /// <summary> /// Enables the specified trace listener for a given log source and its associated trace level. /// </summary> /// <param name="listenerName">The name under which a trace listener is registered in the collection.</param> /// <param name="categoryName">The name for the represented log source.</param> /// <param name="traceLevel">The trace level for the represented log source.</param> public void EnableListenerForEventCategory(string listenerName, string categoryName, SourceLevels traceLevel) { /* ... */ } }
The above helper class makes it easier to interact with trace listener and formatter collections and populate these collections at runtime with standard configuration. What this means is that all standard listeners and formatters used by the application are no longer required to be included into <loggingSettings> section in app.config. Here is the example as to what’s happening inside one of these helpful methods:
private void ApplyDefaultFormatters() { AddFormatter(Resources.TraceLogTextFormatterName, Resources.TraceLogTextFormatterTemplate); AddFormatter(Resources.EventLogTextFormatterName, Resources.EventLogTextFormatterTemplate); } private void AddFormatter(string formatterName, string templateName) { if (!this.loggingSettings.Formatters.Contains(formatterName)) { lock (this.formatterLock) { if (!this.loggingSettings.Formatters.Contains(formatterName)) { TextFormatterData textTextFormatter = new TextFormatterData(); textTextFormatter.Name = formatterName; textTextFormatter.Template = templateName; this.loggingSettings.Formatters.Add(textTextFormatter); } } } }
The way how the above component is utilized is fairly straightforward. You obtain the respective configuration section from the configuration source, pass it to the LoggingConfigurationView component and use the latter to configure the logging settings. The configuration parameters that do change can now simply be stored in the classic <appSettings> section. And the good news is that you can now leave the <loggingSettings> section in app.config completely empty!
IConfigurationSource configSource = ConfigurationSourceFactory.Create(); LoggingSettings loggingConfig = configSource.GetSection(LoggingSettings.SectionName) as LoggingSettings; LoggingConfigurationView loggingConfigView = new LoggingConfigurationView(loggingConfig); loggingConfigView.ConfigureEventLogTraceListener(".", "Application", ConfigurationManager.AppSettings["EvtSource"]); loggingConfigView.ConfigureRollingFlatFileTraceListener(ConfigurationManager.AppSettings["LogFileName"], 5, 1000000); loggingConfigView.AddTraceListener("EtwTraceListener", typeof(FormattedEtwTraceListener)); loggingConfigView.EnableListenerForEventCategory("EtwTraceListener", "ApplicationErrors", SourceLevels.All);
I don’t claim that the above approach to simplifying the Logging Application Block’s configuration is truly elegant. However, it did a good job in my project and helped greatly reduce the amount of “noisy” configuration settings keeping only those that are genuinely necessary. After talking to the Patterns & Practices team, they pointed me to the Fluent Configuration API that enables doing pretty much what has been described above. Perhaps, next time I will consider taking advantage of this enhancement in EntLib 5.0.
Sharing Configuration Data in Hybrid Solutions
One of the technical requirements in my customer’s hybrid solution was the ability to store and manage the entire solution’s configuration in a central repository. All components and services in the solution architecture must be able to consume a very rich set of configuration settings that must be shared between different tiers (first dimension) and deployments (second dimension) of the hybrid solution in question. In addition, the location of this repository should not be constrained to a particular hosting environment. Initially, the customer preferred to keep the configuration repository on premises whereas their forward-looking future strategy is aimed towards visible mass of water droplets suspended in the atmosphere above the surface of the Earth.
The extensibility of EntLib makes it extremely easy to meet the above requirement by plugging in a custom configuration source. Through a “marriage” of my own implementation of a custom configuration source with AppFabric Service Bus, I was able to provide my customer with a solution that doesn’t impose any constraints on where the configuration data can be located. It simply enables to keep it anywhere in the world. This is a superb flexibility!
As a result, all cloud-hosted and on-premises services are now happily enjoying the luxury of having the shared configuration data that is discoverable and consumable regardless of the hosting environment. Moreover, with multiple Windows Azure worker roles that require access to a common set of extensively complex configuration settings, the custom configuration source helped avoid overcomplicating the configuration experience. Below is the actual view of the worker role configuration files in the solution. In summary, because everything is in fact stored centrally, the cloud service configuration files are empty (surprise, surprise!) which is by design.
To summarize, sharing configuration data between numerous components and services in a hybrid solution was a fairly simple task which involved taking advantage of the extensibility in EntLib, introducing a custom configuration source and gluing it all together using message relay service provided by AppFabric Service Bus.
Provider Model is Power
In this lesson learned, I will briefly touch on the role of provider model in EntLib and how it helped deliver rich functionality that follows the principles of code reuse and symmetry between on-premises and cloud.
The EntLib documentation makes it clear that Enterprise Library can serve as the basis for a custom library. You can take advantage of the extensibility points incorporated in each application block and extend the application block by supplying new providers. EntLib incorporates an easy-to-use extensible mechanism for you to add your own custom providers if you require specialized behavior. You can plug in custom providers that you create without needing to recompile the EntLib codebase.
I found the following extensibility points in EntLib to have been very valuable when developing hybrid solutions some parts of which must run on-premises whereas the other parts are deployed and running on Windows Azure:
-
Custom configuration source – provides the extensibility mechanism for plugging in your own provider of configuration data. This allows building a configuration framework that can be reused in its entirety both on on-premises and cloud. At runtime, different flavors of a custom configuration source are hooked into the provider chain transparently providing access to the configuration data.
-
Custom trace listener – allows extending the functionality of Logging Application Block with new components which are capable of supporting hosting environment-specific requirements for tracing and logging. For example, a service running on Windows Azure may need to pass diagnostic data into a specialized tracing component that knows how to buffer up and relay the data into an event collector in real-time. Conversely, an on-premises-hosted service would rather choose a local ETW tracing infrastructure for logging its instrumentation and telemetry data. Regardless of the use case, you can build a common instrumentation and tracing framework and reuse it in both environments. Simply plug in a custom trace listener that understands how to operate in a given environment and let the Logging Application block to do the rest for you.
-
Custom cache backing store – enables building custom providers for Caching Application Block to support the new breeds of caching services provided by server-based and cloud-based platforms. Again, a good use case here would be a distributed hybrid application. Some services are running on Windows Server platforms and these would be using a custom cache backing store that takes advantage of the Windows Server AppFabric cache. The cloud-based services would be utilizing another backing store that uses the respective platform’s capability. The actual type of the underlying backing store is completely abstracted from the consuming application. You simply build a component that tells EntLib “Hey, I need a cache!” and EntLib figures out which backing store is required based on the application configuration. The component can in turn be reused regardless of the hosting environment.
The following drawing attempts to zoom into and visualize the portion of a hybrid solution that may greatly benefit from the provider model and reusable framework layers:
Note though, while the sentiment about the power of the caching provider model is valid at the time of writing, the Caching Application Block will be deprecated in EntLib after version 5.0 as it is being superseded by System.Runtime.Caching classes in .NET Framework 4.0. Moreover, caching service symmetry between on-prem and cloud is not too far away. The upcoming release of Windows Azure AppFabric will bring a wealth of middleware services including distributed, highly available cache seamlessly accessible from within both hosting environments.
Configuration Section Deserialization
This lesson belong to a category of “gotchas”. In essence, as long as you don’t do what I did, you should be fine. However, if you ever needed to pass a configuration section through the wire and reconstruct the configuration object model from its XML-serialized fragment, the following may bite you if you were unaware of the behavior described below.
In my scenario, I’m relying on the EntLib’s Configuration Application Block and using a custom configuration source to read my cloud application configuration settings from a remote repository that is secured and managed on premises. The custom configuration source goes and interoperates with an on-premises WCF service that is hooked into the Azure AppFabric Service Bus. The WCF service returns an XML fragment representing my application’s settings in XML-serialized format. The contract supported by the service is the following:
// Defines a service contract supported by the configuration service hosted on-premises. [ServiceContract(Name = "IOnPremisesConfigurationService", Namespace = WellKnownNamespace.ServiceContracts.General)] public interface IOnPremisesConfigurationServiceContract { /// <summary> /// Retrieves the specified configuration section for a given application running on a specific machine. /// </summary> /// <param name="sectionName">The name of the configuration section.</param> /// <param name="applicationName">The name of the application requesting the configuration section.</param> /// <param name="machineName">The machine name where the requesting application is running.</param> /// <returns>The configuration section serialized into XML so that it can be sent through the wire.</returns> [XmlSerializerFormat] XmlElement GetConfigurationSection(string sectionName, string applicationName, string machineName); }
Once the XML representation of the configuration section is obtained from a remote configuration store, it is handed over for deserialization into corresponding configuration object such as LoggingSettings, CacheManagerSettings, or a custom .NET configuration object. At this point, a “gotcha” has caused some headache.
In summary, the problem manifested itself as a runtime exception telling me “Unrecognized element ‘add‘”. In your case, the actual element name may have been different. Note though that XML fragment was fully compliant with the structural requirements for a given configuration section and was in fact an XML-serialized version of the respective configuration section that has not been modified in any way while it was passing through the wire between on-premises WCF service and its client.
The problem was isolated to a single line of code in the EntLib codebase which expected the input XML to always have a single non-content node at the top. This could be either an XML processing instruction or a whitespace node.
Adding processing instruction was an easy task. Below is the final code snippet which reads a configuration section from a WCF service and turns it into a configuration object. This code was tested and later deployed into production.
// Invoke the WCF service in a reliable fashion and retrieve the specified configuration section. XmlElement configSectionXml = configServiceClient.RetryPolicy.ExecuteAction<XmlElement>(() => { return configServiceClient.Client.GetConfigurationSection(sectionName, CloudEnvironment.CurrentRoleName, CloudEnvironment.CurrentRoleMachineName); }); if (configSectionXml != null) { // Instantiate a configuration object that correspond to the specified section. ConfigurationSection configSection = ConfigurationSectionFactory.GetSection(sectionName); // Gotcha: configuration section deserializer requires a non-content node to come first. XmlDocument configXml = FrameworkUtility.CreateXmlDocument(); configXml.AppendChild(configXml.ImportNode(configSectionXml, true)); // Configure XML reader settings to disable validation and ignore certain XML entities. XmlReaderSettings settings = new XmlReaderSettings { CloseInput = true, IgnoreWhitespace = true, IgnoreComments = true, ValidationType = ValidationType.None, IgnoreProcessingInstructions = true }; // Create a reader to consume the XML data. using (XmlReader reader = XmlReader.Create(new StringReader(configXml.OuterXml), settings)) { // Attempt to cast the configuration section object into SerializableConfigurationSection for further check. SerializableConfigurationSection serializableSection = configSection as SerializableConfigurationSection; // Check if the the configuration section natively supports serialization/de-serialization. if (serializableSection != null) { // Yes, it's supported. Invoke the ReadXml method to consume XML and turn it into object model. serializableSection.ReadXml(reader); } else { // No, it's unsupported. // Need to do something different, starting with positioning the XML reader to the first available node. reader.Read(); // Invoke the DeserializeSection method via reflection. This is the only way as the method is internal. MethodInfo info = configSection.GetType().GetMethod("DeserializeSection", BindingFlags.NonPublic | BindingFlags.Instance); info.Invoke(configSection, new object[] { reader }); } reader.Close(); } }
Here is the source code for the helper method. It is dead simple.
// Creates an empty XML document that only contains the XML processing instruction. public static XmlDocument CreateXmlDocument() { XmlDocument document = new XmlDocument(); document.AppendChild(document.CreateXmlDeclaration("1.0", Encoding.Unicode.WebName, String.Empty)); document.PreserveWhitespace = false; return document; }
Hope this was helpful. If not, you were lucky not being affected by the above behavior.
Integrating Retry Logic into Data Access Block
The SqlDatabase provider shipped with EntLib is capable to work against SQL Azure databases out-of-the-box. However, as we learned from the past, any interaction with SQL Azure requires a defense mechanism to help deal with possible transient conditions. The best explanation on this can be found here. In summary, a solid and intelligent retry logic is a must in any SQ Azure client applications.
It turned out that integrating the retry logic into the Data Access Application Block was less painful than I was expecting. In fact, it was not painful at all. For this purpose, I have integrated the Transient Fault Handling Framework with the Data Access Block by leveraging the power of extension methods in the .NET Framework.
I picked up the main data access methods (ExecuteNonQuery, ExecuteScalar, ExecuteReader, etc.) provided by the Database class in EntLib from which all custom database providers are derived (including SqlDatabase). For each of these methods and their overloads, an extension method was added similar to the ones below:
// Provides a set of extension methods adding retry capabilities into the Enterprise Library’s base database provider. public static class EntLibDatabaseExtensions { /// <summary> /// Executes the specified command and returns the results in a new dataset. /// </summary> /// <param name="db">The database object which is required as per extension method declaration.</param> /// <param name="command">The command object that contains the query to execute.</param> /// <param name="retryPolicy">The policy defining whether to retry a command if it fails due to transient condition.</param> /// <returns>A dataset with the results of the command.</returns> public static DataSet ExecuteDataSet(this Database db, DbCommand command, RetryPolicy retryPolicy) { return (retryPolicy != null ? retryPolicy : RetryPolicy.NoRetry).ExecuteAction<DataSet>(() => { return db.ExecuteDataSet(command); }); } /// <summary> /// Executes the specified command and returns the number of rows affected. /// </summary> /// <param name="db">The database object which is required as per extension method declaration.</param> /// <param name="command">The command object that contains the query to execute.</param> /// <param name="retryPolicy">The policy defining whether to retry a command if it fails due to transient condition.</param> /// <returns>The number of rows affected by the command.</returns> public static int ExecuteNonQuery(this Database db, DbCommand command, RetryPolicy retryPolicy) { return (retryPolicy != null ? retryPolicy : RetryPolicy.NoRetry).ExecuteAction<int>(() => { return db.ExecuteNonQuery(command); }); } /// <summary> /// Executes the specified command and returns an data reader object through which the result can be read. /// </summary> /// <param name="db">The database object which is required as per extension method declaration.</param> /// <param name="command">The command object that contains the query to execute.</param> /// <param name="retryPolicy">The policy defining whether to retry a command if it fails due to transient condition.</param> /// <returns>An data reader object enabling to fetch the results.</returns> public static IDataReader ExecuteReader(this Database db, DbCommand command, RetryPolicy retryPolicy) { return (retryPolicy != null ? retryPolicy : RetryPolicy.NoRetry).ExecuteAction<IDataReader>(() => { return db.ExecuteReader(command); }); } /// <summary> /// Executes the specified command and returns the first column of the first row in the result set returned by the query. /// </summary> /// <param name="db">The database object which is required as per extension method declaration.</param> /// <param name="command">The command object that contains the query to execute.</param> /// <param name="retryPolicy">The policy defining whether to retry a command if it fails due to transient condition.</param> /// <returns>The value of the first column of the top row in the result set.</returns> public static object ExecuteScalar(this Database db, DbCommand command, RetryPolicy retryPolicy) { return (retryPolicy != null ? retryPolicy : RetryPolicy.NoRetry).ExecuteAction<object>(() => { return db.ExecuteScalar(command); }); } }
It’s easy to notice a pattern there. In essence, each standard ExecuteXXXX method has been “extended” with an additional overload that accepts a RetryPolicy object in the last parameter. Inside each extension method, the invocation of the standard ExecuteXXXX operation is being performed from within a retry-aware scope (managed by the ExecuteAction method).
The powerful thing here is that any extension method defined for a base class (or an interface) level is available throughout the inheritance chain. In the above example, any class deriving itself from the Database base class will inherit its extension methods even though the extensions were in fact defined for the base type.
Now that extension methods are in place, activating the retry logic was just a single step away. All I did was adding a valid instance of the RetryPolicy object as the last parameter in all ExecuteXXXX operations.
RetryPolicy retryPolicy = new RetryPolicy<SqlAzureTransientErrorDetectionStrategy>(10, TimeSpan.FromMilliseconds(500)); Database db = DatabaseFactory.CreateDatabase("SQLAzureInventoryDb"); DbCommand command = db.GetStoredProcCommand("usp_StoreInventoryItemDetails"); // ... Here is we populate the database command with required parameters ... // The original code in the line below was: db.ExecuteNonQuery(command); db.ExecuteNonQuery(command, retryPolicy);
To summarize, it was not too hard to add support for retry logic into the Data Access Application Block in EntLib. Everything essentially boils down to what techniques and approaches are adopted to perform retry-aware operations and how flexible and reusable these approaches are. Hopefully you are not wrapping each block of your database access code into a wonderful piece of artwork based on copied/pasted “for (int retryCount = 0; retryCount <= sqlMaxRetries; retryCount++) { … }” construct.
Summary
In this blog post, I shared some of my lessons learned while developing a hybrid solution with Enterprise Library 5.0. I have to admit that the initial implementation of the hybrid solution doesn’t use the full-blown set of capabilities provided by all the application building blocks in the EntLib family. For instance, I didn’t have any need to use the Unity dependency injection mechanism in the initial release. In addition, the Validation Application Block and Security Application Block haven’t been applied in V1. Otherwise, there could have been more learnings to share.
If for whatever reason you have been unable to find the answers you were looking for in this post, don’t blame the author. Instead, try and see if the following additional resources may be of any further assistance to you:
- “Using Enterprise Library 5.0 in Windows Azure” whitepaper explaining the capabilities and limitations of Enterprise Library 5.0 on Windows Azure platform.
- “Get logging in Windows Azure with Enterprise Library” post on Tom Hollander’s blog.
- “Using the Fluent Configuration API” article in the MSDN Library.
- “Enterprise Library 5.0 Extensibility Labs” resources to learn about key techniques for building custom providers and custom configuration sources.
- “Managed Extensibility Framework Overview” article in the MSDN Library.
- Community buzzing about “Enterprise Library on Windows Azure” via one of your favorite search engines: “G” engine, “B” engine, “Y” engine or whatever.
Authored by: Valery Mizonov
Reviewed by: Christian Martinez, Grigori Melnik, Fernando Simonazzi
2 Comments
Leave a Reply
You must be logged in to post a comment.














(1 votes, average: 4.00 out of 5)
Pingback: Microsoft Enterprise Library 5.0.1 Integration Pack para Windows Azure - Jorge Serrano - MVP Visual Developer - Visual Basic
Pingback: Enterprise Library Logging in Azure Part 1 The baseline | devMobile's blog