Linq to Umbraco

by Shannon Deminick 24. February 2009 06:38

[Entry migrated from suite101.thefarmdigital.com.au]

Intro

We've been working on 3 larger Umbraco sites in which we've implemented our own Linq to Umbraco strategy. It's a fairly simple strategy, it doesn't involve custom expression trees or anything, it simply leverages Linq to Xml, IEnumerable<T> and extension methods. In simple terms, we basically convert Xml nodes into our own objects which we can then run typed Linq queries against. Since the performance of deserialization of this Xml into objects was one of our concerns, we implemented Microsoft's Policy Injection framework to handle the caching of our Umbraco objects which works really well (more on this later). Here's a quick example of what this allows you to do with Umbraco data (in this case Umbraco is storing information about events, such as festivals, etc...)

var todayEvents = (from e in GetEvents()
                   where e.FromDate.Date == DateTime.Now.Date
                   select e).ToList(); 

The source code is available here: LinqUmbraco.zip (566.35 kb).

Background

In order for this to work, there is a bit of setup involved. The way that we've gone about this implementation is by defining a data model for each Document Type that you want to be able to use in this framework. In this example, Umbraco will need to be setup with a few Document Types: Event, EventComment, EventOrganizer, EventsContainer. I've included the definitions of these doc types in the source code so you can easily import them. For an event model, we would create an interface:

public interface IUmbEvent : IUmbracoItem 
{ 
    System.Collections.Generic.List EventComments { get; set; } 
    string EventTitle { get; set; } 
    DateTime FromDate { get; set; } 
    string FullDescription { get; set; } 
    string ShortDescription { get; set; } 
    bool ShowInListing { get; set; } 
    DateTime? ToDate { get; set; } 
} 

You'll notice this interface extends IUmbracoItem which I've defined in our code library. It contains all of the basic properties of an Umbraco node:

public interface IUmbracoItem 
{ 
    DateTime CreatedDate { get; set; } 
    int CreatorId { get; set; } 
    string CreatorName { get; set; } 
    int Id { get; set; } 
    int Level { get; set; } 
    System.Collections.Generic.Dictionary NodeData { get; } 
    string NodeName { get; set; } 
    int NodeType { get; set; } 
    string NodeTypeAlias { get; set; } 
    int ParentId { get; set; } 
    string Path { get; set; } 
    int SortOrder { get; set; } 
    int? Template { get; set; } 
    DateTime UpdateDate { get; set; } 
    string UrlName { get; set; } 
    string Version { get; set; } 
    string WriterName { get; set; } 
} 

Next I've created a class that implements this interface called UmbEvent which extends UmbracoItem(which in turn implements IUmbracoItem). The constructor function of these objects are what does the deserialization:

public UmbEvent(XElement x) 
            : base(x) 
{ 
    //Get the from date and use the DateConverter to convert it. 
    //If the returned date is MinValue, then the value in Umbraco has not actually 
    //been set and since it is mandatory, we'll throw an Exception. 
    FromDate = x.UmbSelectDataValue("FromDate", DateConverter); 
    if (FromDate == DateTime.MinValue) 
        throw new Exception(string.Format(
		"The node with id {0} does not have a start date set", 
		this.Id.ToString())); 

    //Get the ToDate using a NullableDateTimeConverter 
    ToDate = x.UmbSelectDataValue("ToDate", NullableDateTimeConverter); 

    EventTitle = x.UmbSelectDataValue("EventTitle"); 
    ShortDescription = x.UmbSelectDataValue("ShortDescription"); 
    FullDescription = x.UmbSelectDataValue("FullDescription"); 
    
    //Get the ShowInListing using the IntConverter since the value stored in Umbraco is 
    //an integer, not a boolean. 
    ShowInListing = x.UmbSelectDataValue("ShowInListing", IntConverter) == 1;            
    
    //Select all child "node" nodes that have a node type alias 
    //of "Event Comment" from the current element, deserialize them 
    //to UmbEventComment objects, and store them in our List property. 
    EventComments = x.UmbSelectNodes() 
        .UmbSelectNodesWhereNodeTypeAlias("EventComment") 
        .Select(n => new UmbEventComment(n)) 
        .ToList();            
} 

In the code for the UmbracoItem class are some helper methods for data conversion so exceptions are not thrown and that data is converted properly. 

The next step is to setup the data service layer. I generally create a different service class for each model and in this example we'll have IUmbEventService interface with an UmbEventService class defined as:

public interface IUmbEventService : IUmbracoService 
{ 
    int CreateEvent(IUmbEvent cqEvent); 
    void UpdateEvent(int eventId, IUmbEvent e); 
    List GetEvents(); 
    IUmbEvent GetEvent(int eventId); 
    DateTime LastEventDate { get; } 
} 

Using the code

Service Layer

Most of the important work is done in the service layer. The underlying method that is called for retreiving events is the GetEvents() method which returns a List<IUmbEvent> object which we can use to query. This method will look up all events in the Umbraco xml cache, convert each one to an UmbEvent and return a list of these deserialized objects. I've created a bunch of Linq to Xml extension methods for use with Umbraco which I use to query the Umbraco xml cache. This makes querying Umbraco xml much nicer, easier and allows you to use System.Xml.Linq objects instead of the old XPath objects. Here's the definition of the GetEvents() method:

[CachingCallHandler(1, 0, 0)] 
public List GetEvents() 
{ 

    Console.WriteLine("Looking up and caching all published events..."); 

    //Lookup the event container node in Umbraco 
    XElement xNode = UmbXmlLinqExtensions.GetNodeByXpath(EventContainerXPath); 

    var eventData = xNode 
        .UmbSelectNodes() //selects all descendant "node" nodes 
	//selects nodes of a certain alias 
        .UmbSelectNodesWhereNodeTypeAlias(EventNodeTypeAlias) 
	//This does the object conversion 
        .Select(x => new UmbEvent(x)) 
	//ensure we don't return events with no start date 
        .Where(x => x.FromDate != DateTime.MinValue); 

    return eventData.Cast().ToList(); 
} 

Now that this method is create, we can just query the result of the method to find Events by whatever criteria we've defined in the IUmbEvent interface. For example, the GetEvent method body is quite simple:

public IUmbEvent GetEvent(int eventId) 
{ 
    var myEvent = m_This.GetEvents() 
        .Where(x => x.Id == eventId); 

    return myEvent.SingleOrDefault(); 
} 

To expose my service layer classes, i have a service factory class that creates new instances of each service type when requested. This makes it easy to call services from code since there's always one point of entry.

Calling the services

I've included a class called Tests which calls the methods of the event service. With the above framework in place, using the code is extremely easy:

List events = m_Factory.EventService.GetEvents();

or

IUmbEvent e = m_Factory.EventService.GetEvent(LookupEventId); 

Also in the source code are service methods to create and update Umbraco nodes. If you're not familiar with how to do this, this will show you a fairly easy way to get the job done. The CreateEvent method actually checks to see if a "Pending" event organizer node is there and if not it creates one and then creates new unpublished events under this node.

Running the tests

I've created a simple ashx handler that you can setup in your Umbraco project to test this data layer. You'll need to build the solution and copy the DLL files over from the bin folder of the UmbracoData project into your Umbraco bin folder (don't copy over the Umbraco DLLs if you don't want to ... these are Umbraco 4 DLLs). Then add an HttpHandler to your web.config:

<add verb="*" path="datatest.ashx" type="UmbracoData.TestHandler, UmbracoData" />

You'll have to make sure Umbraco is setup for this project so you'll need to: 

  • Import all of the document types
  • Create a node of type "EventsContainer" called "Events" under the root content node

Now you can navigate to datatest.ashx which will allow you to run the tests. If you want to step through the code, just Attach to your Asp.Net process with the solution open.

Policy Injection

As I mentioned before, performance will be a factor if for every event query, we need to deserialize every event xml node into an object. This is where caching comes in handy. We use Policy Injection quite a bit to do caching and logging in our applications as it makes these tasks incredibly easy. Policy Injection is a simple form of AOP (Aspect Oriented Programming) and is part of Microsoft's Enterprise Library. You'll see that this method is attributed with the CachingCallHandler which is going to cache the output of this method for 1 hour in this case. In each subsequent call to this method Policy Injection will intercept the call and determine if it has been cached, and if so, it will simply return the cached results and cancel the execution of the method.

In order for Policy Injection to work, you need to either create a new service class or wrap and existing service class the with Policy Injection. As i mentioned before I use a service factory to expose all of my service classes and in this class, it creates each service with Policy Injection:

public class ServiceFactory 
{ 
    public IUmbEventService EventService 
    { 
        get 
        { 
            return PolicyInjection.Create(); 
        } 
    }        
} 

There is a catch!! You'll notice in the above code snippet that defines the GetEvent method uses the syntax: m_This.GetEvents(). This is because we need a reference to the event service class wrapped in Policy Injection inside the event service class.... confusing in know. This is because the Policy Injection framework is the only thing that knows anything about the CachingCallHandler attribute and if we call GetEvents inside one of the methods of the UmbEventService, then Policy Injection is actually not playing any part of that method call. To get around this, we define an internal property in the UmbEventService class of type IUmbEventService and in the constructor function, we wrap this instance in Policy Injection and assign it to this property:

public UmbEventService() 
{ 
    //Ensure our internal object is wrapped in PolicyInjection 
    m_This = PolicyInjection.Wrap(this); 
} 

///  
/// We declare this variable as a "trick" to ensure that PolicyInjection 
/// is still used when we call internal members of this class. 
/// For example, any method of UmbEventService that calls GetEvents() will need 
/// to call m_This.GetEvents() to ensure that it returns the cached data 
/// and does not re-execute the code in body of the method. 
///  
IUmbEventService m_This; 

Another point of interest is that i've included an Umbraco action handler to refresh the Policy Injection cache whenever a node is published.

The source code is available here: LinqUmbraco.zip (566.35 kb).

Tags: ,
Categories: .Net | Umbraco

Linq + Unity + AOP = Fun

by Shannon Deminick 20. February 2009 07:28

[Entry migrated from suite101.thefarmdigital.com.au]

I've recently had some time to look at using Dependency Injection alongside of Linq to SQL to manage the DataContext. Because of the way that Microsoft made Linq to SQL, it's not very easy to use dependency injection properly with the DataContext because many of the members in this framework are not interfaces or cannot be instantiated directly from code. Type mocking frameworks can be used to mock these objects but i wanted to see if there was a way around all of this. I've posted a project on the CodeProject website here. A few tricks had to be implemented like creating an interface for the DataContext (IDataContext) and exposing a new property called GetITable exposing a new interface called IEnumerableTable<>. This way, you can resolve IEnumerableTable's from the IDataContext instead of resolving a System.Data.Linq.Table from the GetTable<> method (which you would never be able to implement in a custom DataContext class). Because the IEnumerableTable<> interface is IEnumerable<T> itself, you can run all your normal Linq queries against it as per normal. IEnumerableTable<> also implements ITable so you can perform all of the normal Linq to SQL table methods (i.e. InsertOnSubmit, etc...) on the resolved object.

To get this framework setup and working there's quite a few things you need to build, but it's quite a fun exercise. Included in the source is a custom DataContext called XDataContext which turns the data store into XML files instead of database tables and still uses the Linq to SQL entities that are generated. You can simply just map the IDataContext to any custom data context using the Unity (Microsoft's dependency injection framework) configuration. I've also implemented PolicyInjection into this framework as it is extremely easy to do now that all of the objects are interfaced already. Now it's too easy to cache and log calls to methods.

There's a complete description of everything in the article.

Categories: .Net

Upgrading to Umbraco 4

by Shannon Deminick 5. February 2009 12:31

[Entry migrated from suite101.thefarmdigital.com.au]

This is a pretty detailed guide of how to upgrade a version 3 Umbraco web application project to be version Umbraco version 4. Theoretically Version 4 is 100% backwards compatible with version 3. I've run a couple of upgrades and everything seems to work great. To upgrade an Umbraco 3 installation that is not part of your own web application project, it should be nearly the same, however you wouldn't need to worry about DLL references, etc.. as mentioned below.

  1. Download umbraco 4 binary files: Umbraco 4 Download
  2. Backup Database
  3. Take backup of all files for current version 3 install
    • If using source control might be best to create a branch
  4. All of the Umbraco DLLs that your project is referencing need to be updated to the new Umbraco 4 DLLs.
    • Generally this means replacing the folder that contains your referenced DLLs with the DLLs in the bin folder of the Umbraco 4 download.
  5. Update/Replace the "umbraco" folder's files:
    • If your project has not customized any of the files located in the "umbraco" folder, then just delete your original "umbraco" folder and replace it with the new "umbraco" folder found in the Umbraco 4 download.
    • If you project does have customized files in the "umbraco" folder, then it might be worthwhile running a diff on the files compared to the new umbraco 4 files to see what has changed and then manually make the changes to the new files to suit your needs.
  6. Update/Replace the "umbraco_client" folder's files:
    • If your project has not customized any of the files located in the "umbraco_client" folder, then just delete your original "umbraco_client" folder and replace it with the new "umbraco_client" folder found in the Umbraco 4 download.
      • In most cases, files should not be modified in this folder
    • If you project does have customized files in the "umbraco_client" folder, then it might be worthwhile running a diff on the files compared to the new umbraco 4 files to see what has changed and then manually make the changes to the new files to suit your needs.
      • In most cases, files that have been modified in this folder have been done to modify the UI of the admin section (which requires a paid Umbraco license) or custom icons have been added to style tree nodes.
  7. Web.config changes:
    • There are a few changes to the Web.config in umbraco 4 which may need to be manually merged in the AppSettings:
      • Change the value of the umbracoConfigurationStatus to be an empty string... otherwise the installer will fail to run.
      • The umbracoSmtpServer value is no longer used, Umbraco 4 uses the system.net -> mailSettings -> smtp values instead
      • The umbracoDisableVersionCheck is no longer required and can be removed
      • The umbracoVersionCheckPeriod should be merged from the Web.config of the Umbraco 4 download files
      • The umbracoUseSSL should be merged from the Web.config of the Umbraco 4 download files
      • The umbracoUseMediumTrust should be merged from the Web.config of the Umbraco 4 download files
    • The browserCaps section can be removed as the /config/browserCaps.config is no longer used.
    • The Umbraco membership providers must be added to your membership providers section if you have one, otherwise create the same membership providers section as seen in the version 4 web.config
    • The Umbraco roleManager providers must be added to your roleManagers section if you have one, otherwise create the same rolderManagers section as seen in the version 4 web.config
    • The Umbraco siteMap providers must be added to your siteMap section if you have one, otherwise create the same siteMap section as seen in the version 4 web.config
    • In the pages->controls section, you need to add the umbraco reference, this is important otherwise the new master page templates will not work.
  8. All other files in the /config folder of your installation may need to be manually merged with the new versions of these files:
    • Don't overwrite your Dashboard.config, XsltExtension.config, formHandlers.config, 404handlers.config, metablogConfig.config, restExtensions.config, or your UrlRewriting.config files!!
    • If you've customized your tinyMceConfig.config then you will need to manually merge this file, otherwise, overwrite it with the new Umbraco 4 version
    • The umbraco.Settings file:
      • You'll need to merge the errors section if you want support for different error pages based on culture
      • The allowedAttributes property of the imaging tag has been updated to no longer support: title,hspace,vspace
      • The disableHtmlEmail property of the notifications section is no longer needed
      • By default the TidyEditorContent in Umbraco 4 is set to true (which uses a new version of Tidy)
      • The requestHandler section should be completely replaced by the new Umbraco 4 version (unless you've specifically made changes to this for your own reasons)
      • The templates section should be added
        • Please not if you set the useAspNetMasterPages to false, Umbraco 4 will still work (there's a couple of quirky bugs in the admin section with this but no show stoppers) but will be using the old Umbraco 3 templates. Setting this to true will automatically upgrade your templates to use master pages!
      • The logging section should be added
      • The webservices section should be replaced
      • The repostories and providers sections should be added
    • Replace the /config/splashes files with the new version 4 files.
    • Add the new App_Browsers folder
  9. Replace the default.aspx file in the root of your project with the new Umbraco 4 default.aspx file
  10. Replace the /install folder with the Umbraco 4 /install folder
  11. Copy the Umbraco 4 /data/packages folder into your /data folder
  12. Copy the master pages folder into your root directory
  13. Delete all DLLs from your project's output bin folder to ensure there are no old Umbraco DLLs hanging around
  14. Attempt to rebuild your project!
    • If your project does not build, then this is most likely due to an error in your project, not Umbraco 4 DLLs. Umbraco 4 is 100% backwards compatible with version 3 and version 3 controls/packages
  15. Run the installer: /yoursiteurl/install/
    • The installer should run seamlessly and you shouldn't need to modify anything
    • When prompted to install Runway, probably best not to do this on an existing installation
Some things to note after upgrading:
  • Though Umbraco automatically created new master page templates for me, i had to manually add the
  • I had to add a ScriptManager to my master master template as many of my controls relied on it. Umbraco 3 must have automatically inserted a ScriptManager
  • It seems as though any public boolean properties that exist in user controls that get set via macros used to either have a 1 or 0 passed in to it which Umbraco would convert. With the new umbraco:macro control, true or false needs to be passed to it instead of 1 or 0.


Happy Umbraco 4ing!
Tags: ,
Categories: .Net | Umbraco