ALTER COLUMN on XML column results in Error 511

We frequently change the schema that constrains an XML column in one of the tables. When it’s time to update the schema collection and re-type the column, we use the following steps:

  1. Remove the type with the ALTER TABLE statement:
    ALTER TABLE <table> ALTER COLUMN <xml-column> XML;
  2. Change the schema collection by recreating it.
  3. Reset permissions:
    GRANT EXECUTE ON XML SCHEMA COLLECTION…
  4. Reapply the schema constraint to the column:
    ALTER TABLE <table> ALTER COLUMN <xml-column> XML(<schema collection>)

Twice I’ve started getting error 511 on step one:

Msg 511, Level 16, State 1, Line 1
Cannot create a row of size 8073 which is greater than the allowable maximum of 8060.
The statement has been terminated.

This thread from the SQL Server XML forum indicates that the behavior is “by design.” There seems to be a limit to the number of times that you can alter a column. The good news is that there is a work-around. Rebuilding the indexes on the table appears to reset the alter-count.

VMWare NAT Service (vmnat.exe) goes wild

Tonight I decided to upgrade to VMWare Workstation 7. I’d been running on 6.5 since it was released but held off on 7.0.

The install went smoothly, as expected. The fun started after the final reboot. My usually well-responsive laptop was anything but. I fired up the Windows Task Manager and sorted by CPU but nothing registered at more than 1%. I then clicked the Show processes from all users option and vmnat.exe appeared at the top of the list with ~50% CPU and 5.9MB Memory.

Restarting the laptop didn’t solve the problem.

A web search turned up the following thread VMWare Workstation 6.5.3 + Windows 7 Enterprise 64 = VMNAT.EXE issue? in which the poster is complaining about high CPU utilization from vmnat.exe. The resolution to his problem was to remove NAT from VMNET0.

I used the Virtual Network Editor to check my configuration. No NAT on VMNET0. I used the Restore Default option and after it completed, vmnat.exe had calmed down to 0% CPU and 1.6MB Memory.

Upgrading TFS 2010 B2 to the RC

The Release Candidates of Visual Studio and Team Foundation Server were made available last week and I’m finally getting around to upgrading my installation of TFS. In this post, I’ll record the steps that I took and the problems encountered.

1. Backup the server
I’m actually running TFS in a VM hosted within a Hyper-V 2008 Server. To backup my server, I shutdown the VM and used the Hyper-V Manager to create a snapshot of it.

2. Uninstall TFS 2010 Beta 2

image

Happily the uninstall didn’t encounter any problems

3. Using the Hyper-V Manager, I mounted the TFS RC ISO.

4. Next I started the install of the Release Candidate.

5. After the install completed, I selected the Upgrade wizard. After stepping through the pages and providing all the information, the Configure step is kicked off to do the real work.

image

6. After a very short amount of time – success!

image

The final test was to fire up a VM containing an install of VS 2008 and access the new TFS installation. Unlike the rest of the upgrade, this didn’t work the first time. Instead I was met with:

image

The Forward Compatibility Update (KB974558) is a patch to VS2008 SP1 that enables interoperability with TFS 2010. I didn’t need this to access the Beta 2 install but who am I to argue with the computer…

KB974558 is a short download and a quick install. I launched VS2008 again and Team Explorer happily reached out to the TFS server!

And with that – my TFS 2010 installation has been upgraded to the RC. Kudos to the TFS team!!

Outlook Date Tricks

I love it when a piece of software surprises me in a good way - since it so rarely happens. In this case, I’m talking about Microsoft Outlook and some convenient functionality that’s embedded within its date fields. I actually ran into this a rather long time ago but have only now gotten around to writing it up.image of scheduling bars

Say that you’re creating a new meeting request for tomorrow. You could always use the scheduling bars to select the start and end times of the meeting. The greed bar represents the start time and the red for the end. It works well when the meeting is within the next couple of days. But what if meeting is for next Wednesday? You could:

  1. Scroll some number of days on the Scheduling page
  2. Calculate the date and type it in to the Start Time box
  3. Use the drop-down calendar to select the appropriate date
  4. Enter “next Wednesday” into the Start Time box

Yup. If you enter “next Wednesday” and press tab, Outlook will do the calculation for you:

Start time of next wednesday

Outlook calculates the correct value

Try it!

Through experimentation, I’ve discovered that the following also works:

  • name of a day (ex. Monday, Tuesday, etc)
  • “today”, “tomorrow”, “yesterday”
  • “last” {name of day}
  • “[number] week[s] from {today | tomorrow | name-of-a-day}” (ex. 2 weeks from Monday)
  • “last day of {this|next| last} month”
  • “last {name of day}”
  • “Christmas” (I was surprised!)

There’s probably more.

DateTime.MinValue != SQL Server minimum datetime value

I’m sure that most people know this but I’m betting that there are a few that don’t. I say this because I just found a line of code that is trying to insert DateTime.MinValue into a SQL Server datetime column. The result is:

Arithmetic overflow error converting expression to data type datetime.

The value of DateTime.MinValue is 1 Jan 0001 12:00:00am.

The minimum value that you can put into a SQL Server datetime column is 1 Jan 1753. On the other hand, if you’re lucky enough to be using SQL Server 2008 and you have control over the table definitions, you can use the new datetime2 datatype. Datetime2 has an extended date range of 1 Jan 0001 to 31 Dec 9999.

Automatically Backing up a Blogger.com Blog – Part 2

I started writing about my blog backup solution in a previous post. In that post I briefly discussed the Blogger API that I used. In this post I’d like to go over the rest.

The Solution


The solution is simple,  unimpressive and consist of only a few classes. I discussed the actual API calls in the previous post. Those are the heart of the Exporter and Authenticator classes. Here’s the rest of the Authenticator class:
public class Authenticator
{
private static ILog logger = LogManager.GetLogger(typeof(Authenticator));

private string _email;
private string _password;

private int _timeoutInMs = 100000;

public Authenticator(string email, string password)
{
_email = email;
_password = password;
}

/// <summary>
/// The request timeout in milliseconds
/// </summary>
public int TimeoutInMs
{
get { return _timeoutInMs; }
set
{
_timeoutInMs = value;
if (logger.IsDebugEnabled) logger.Debug("Authentication request timeout changed to: " + this._timeoutInMs);
}
}

private void CheckRequiredConfiguration()
{
if (string.IsNullOrEmpty(this._email) || string.IsNullOrEmpty(this._password))
throw new ConfigurationErrorsException("Either the username or password has not been supplied. Please check the configuration.");
}

/// <summary>
/// Make an authentication request
/// </summary>
/// <returns>an instance of <see cref="AuthenticationResult"/> containing the result of the authentication request</returns>
/// <exception cref="InvalidDataException">Occurs if no response or an unexpected response is received from the Blogger service.</exception>
/// <exception cref="AuthenticationException">Occurs if the status code is 'OK' but no Auth token was returned.</exception>
public AuthenticationResult Authenticate()
{
this.CheckRequiredConfiguration();

HttpWebRequest authenticationRequest = CreateAuthenticationRequest();

HttpWebResponse response;
try
{
response = (HttpWebResponse)authenticationRequest.GetResponse();
}
catch (WebException webException)
{
response = webException.Response as HttpWebResponse;

}

if (null == response) throw new InvalidDataException("An invalid response was received from the service.");

var result = new AuthenticationResult();

var status = response.StatusCode.ToString();
if (logger.IsDebugEnabled) logger.Debug("Authentication response result code: " + status);
result.Status = status;

var sr = new StreamReader(response.GetResponseStream());
string responseBody = sr.ReadToEnd();
if (logger.IsTraceEnabled) logger.Trace("Authentication response body: " + responseBody);
response.Close();

if (result.Status == "OK")
{
string authTokenValue = ParseAuthenticationToken(responseBody);
result.Token = this.BuildAuthToken(this._email, authTokenValue);
}

return result;

}

private AuthorizationToken BuildAuthToken(string email, string authValue)
{
return new AuthorizationToken(email, authValue);
}

private string ParseAuthenticationToken(string responseBody)
{
int authTokenPosition = responseBody.IndexOf("Auth=");
if (-1 == authTokenPosition)
{
var ex =
new AuthenticationException(
"Authentication request returned 'OK' but auth token was missing. Check exception data for authentication response.");
ex.Data.Add("authResponse", responseBody);
throw ex;
}

return responseBody.Substring(authTokenPosition + 5);
}

private HttpWebRequest CreateAuthenticationRequest()
{
var uri = new Uri("https://www.google.com/accounts/ClientLogin");
HttpWebRequest authenticationRequest = (HttpWebRequest)WebRequest.Create(uri);
authenticationRequest.AllowAutoRedirect = false;
authenticationRequest.Method = "POST";
authenticationRequest.ContentType = "application/x-www-form-urlencoded";
authenticationRequest.KeepAlive = false;
authenticationRequest.Expect = string.Empty;
authenticationRequest.Headers.Add("GData-Version", "2");
authenticationRequest.Timeout = this.TimeoutInMs;

var postBody = new StringBuilder();
postBody.Append("accountType=GOOGLE&");
postBody.AppendFormat("Email={0}&", this._email.ToUrlEncoded());
postBody.AppendFormat("Passwd={0}&", this._password.ToUrlEncoded());
postBody.Append("service=blogger&");
postBody.AppendFormat("source={0}", "malevy.net-Blogger.Backup-1".ToUrlEncoded());

if (logger.IsTraceEnabled) logger.Trace("body of post: " + postBody.ToString());

byte[] encodedData = (new ASCIIEncoding()).GetBytes(postBody.ToString());

authenticationRequest.ContentLength = encodedData.Length;
var stream = authenticationRequest.GetRequestStream();
stream.Write(encodedData, 0, encodedData.Length);
stream.Close();

return authenticationRequest;
}
}

The call to Authenticate returns an instance of AuthenticationResult. AuthenticationResult is a simple container that holds a success or failure indicator (shocking – I know!). In the event that the authentication succeeded, the token that was received from the AuthenticationService is wrapped in an instance of AuthorizationToken, along with the email. Those two pieces of information are required by the Exporter:

public class Exporter
{
private string _blogId;
private readonly IArchiveWriter _fileWriter;
private int _timeoutInMs = 100000;

private static ILog logger = LogManager.GetLogger(typeof(Exporter));

public Exporter(string blogId, IArchiveWriter archiveWriter)
{
_blogId = blogId;
_fileWriter = archiveWriter;
}

/// <summary>
/// The request timeout in milliseconds
/// </summary>
public int TimeoutInMs
{
get { return _timeoutInMs; }
set
{
_timeoutInMs = value;
if (logger.IsDebugEnabled) logger.Debug("Export request timeout changed to: " + this._timeoutInMs);
}
}


/// <summary>
/// Begins the export process
/// </summary>
/// <param name="authToken">The authorization token that was returned from the <see cref="Authenticator"/></param>
public void Export(AuthorizationToken authToken)
{
if (authToken == null) throw new ArgumentNullException("authToken");

if (string.IsNullOrEmpty(this._blogId))
throw new ArgumentException("Exporter must be configured with a Blogger blog ID.");

if (null == this._fileWriter) throw new ArgumentException("Exporter must be configured with a writer");

var request = this.CreateExportRequest(authToken);

var response = request.GetResponse();
using (var responseStream = response.GetResponseStream())
{
this._fileWriter.ArchiveBlogStream(responseStream, this._blogId);
}

if (logger.IsInfoEnabled) logger.Info(string.Format("Blogger blog {0} was successfully backed up.", this._blogId));

}

private HttpWebRequest CreateExportRequest(AuthorizationToken token)
{
// I originally wanted to get all the feeds using the archive functionality but I'm having problems getting archive
// to work when authenticating using ClientLogin. I'm going to punt to this method and hope that I get a response
// from the blogger api newsgroup.
const string archiveUriFormat = "http://www.blogger.com/feeds/{0}/posts/full?updated=1990-01-01T00:00:00&orderby=updated";
var uri = new Uri(string.Format(archiveUriFormat, this._blogId));
HttpWebRequest exportRequest = (HttpWebRequest)WebRequest.Create(uri);

exportRequest.AllowAutoRedirect = false;
exportRequest.Method = "GET";
exportRequest.KeepAlive = false;
exportRequest.Expect = string.Empty;
exportRequest.Headers.Add("GData-Version", "2");
exportRequest.Timeout = this.TimeoutInMs;

exportRequest.Headers.Add(token.ToAuthorizationHeader());

return exportRequest;
}

}

If the Export request succeeds, the response is passed to the IArchiveWriter. In my case, this will be an instance of ArchiveFileWriter. ArchiveFileWriter persists the stream to a file (another shocking discovery!). I’ll skip the code for this one.

The driver that pulls all of this together is an instance of the Agent class:

public class Agent
{
private readonly Authenticator _authenticator;
private readonly Exporter _exporter;

private static ILog logger = LogManager.GetLogger(typeof(Agent));

public Agent(Authenticator authenticator, Exporter exporter)
{
if (authenticator == null) throw new ArgumentNullException("authenticator");
if (exporter == null) throw new ArgumentNullException("exporter");

_authenticator = authenticator;
_exporter = exporter;
}

public void Execute()
{
var authenticationResult = this._authenticator.Authenticate();

if ("OK" != authenticationResult.Status)
{
logger.Error("Unable to authenticate with the Blogger service.");
return;
}

try
{
this._exporter.Export(authenticationResult.Token);
}
catch (Exception e)
{
logger.Error("BloggerBackup caught an unhandled exception.", e);
}
}
}

When the Execute() method is called, Agent uses the instance of Authenticator to make the authentication request. If that succeeds, the authorization token is passed to the exporter to retrieve the actual blog contents.

Now, the observant reader (you of course!) may have noticed that Agent accepts an instance of Authenticator and an instance of Exporter in its constructor. In my solution, all of the wiring is done using Spring.Net.  Here, I believe, is a good example of using an IOC container to support something other than testing. Although I will admit that taking this approach (i.e. Dependency Injection) does make testing much easier.

I decided to use Spring’s XML configuration. I know that there are many people that are very anti-XML for configuration but I don’t believe that the XML gets out of hand for this implementation. Here’s a portion of the XML that is used to wire all of the components together:

<object id="Backup-Agent" type="BloggerBackup.Agent.Agent, BloggerBackup.Agent">
<constructor-arg name="authenticator" ref="authenticator" />
<constructor-arg name="exporter" ref="exporter" />
</object>

<object id="authenticator" type="BloggerBackup.Agent.Authenticator, BloggerBackup.Agent" >
<constructor-arg name="email" value="${email}" />
<constructor-arg name="password" value="${password}" />
</object>

<object id="exporter" type="BloggerBackup.Agent.Exporter, BloggerBackup.Agent" >
<constructor-arg name="blogId" value="${id-of-blog-to-backup}" />
<constructor-arg name="archiveWriter" ref="fileWriter" />
</object>

<object id="fileWriter" type="BloggerBackup.Agent.ArchiveWriter.ArchiveFileWriter, BloggerBackup.Agent" >
<constructor-arg name="archiveRoot" value="${archive-root-folder}" />
</object>

When I ask for an instance of Agent, Spring.Net is nice enough to fill in the dependencies. Of course, that’s what IOC containers do.

When Configuration is not Configuration

I’ve always felt that there are two types of configuration metadata. The first is the type of metadata that represents a portion of the application. The Spring.Net config file is an example of this. I consider this file to be source code. It is describing the way that my application should be put together. In this case, Spring.Net is a factory that is creating objects for me.

The second type of configuration is “operational information.” For example, the timeout, email, and password values. I expect a support engineer to adjust these values as necessary but I don’t expect him or her to change Spring’s config file.

Why mention this? Well, if you look back at the Spring.Net config file, I’m injecting operational values into the objects.  A good example is the instance of ArchiveFileWriter that is given the name of the folder to store the archive files:

<object id="fileWriter" type="BloggerBackup.Agent.ArchiveWriter.ArchiveFileWriter, BloggerBackup.Agent" >
<constructor-arg name="archiveRoot" value="${archive-root-folder}" />
</object>

The funny syntax (${archive-root-folder}) is a place holder that allows me to put the actual value in the app.config file. I’m using Spring.Net’s PropertyPlaceholderConfigurer to retrieve operational values from the <appSettings> section of the app.config file. This way, the support engineer can change those values without having to edit the Spring.Net config file.

Pretty cool eh?

Making things automatic


There’s only one more piece to describe. I’m using an embedded scheduler called Quartz.Net. Now, I’ll be honest. I found using the native Quartz.Net API to be really frustrating. Luckily, Spring.Net has a really nice wrapper around Quartz.Net. In the Spring.Net config file, I define an instance of the SchedulerFactoryObject:

<object id="scheduler-factory" type="Spring.Scheduling.Quartz.SchedulerFactoryObject, Spring.Scheduling.Quartz">
<property name="triggers">
<ref object="Backup-Agent-Schedule"/>
</property>
</object>


To it, I provide an instance of the schedule that I want my backup to occur. I’m using an instance of CronTriggerObject:

<object id="Backup-Agent-Schedule" type="Spring.Scheduling.Quartz.CronTriggerObject, Spring.Scheduling.Quartz">
<property name="JobDetail" ref="backupRunner" />
<property name="CronExpressionString" value="${backup-agent-schedule-asCronExpression}" />
</object>


The CronTriggerObject is supplied with the Cron expression which is being injected from the app.config file. It’s also being given a reference to the JobDetail object. Now, if you look at the same Quartz.Net examples that I saw, you’re shown examples of jobs that derive from JobDetail. I’m not crazy about this approach because it lets Quartz.Net leak into portions of the application where it probably doesn’t really need to be. The Spring.Net wrappers have a good solution to this with their MethodInvokingJobDetailFactoryObject. You configure it to receive an object and the name of a method on the object and the method will be called at the appropriate time:


<object id="backupRunner" type="Spring.Scheduling.Quartz.MethodInvokingJobDetailFactoryObject, Spring.Scheduling.Quartz">
<property name="TargetObject" ref="Backup-Agent" />
<property name="TargetMethod" value="Execute" />
</object>


The Host

The only thing left is to create the Spring.Net Application Context. When the context is created, all the objects will be instantiated, the schedule will start and everything will start working together. Just like a practiced orchestra. For this, I have a class called ContextHost:

public class ContextHost : IDisposable
{
private static ILog Log = LogManager.GetLogger(typeof (ContextHost));

private IApplicationContext _springContext = null;

public void Create()
{

if (Log.IsDebugEnabled) Log.Debug("Starting scheduler");
this._springContext = new XmlApplicationContext(false, "file://~/spring-config.xml");
if (Log.IsInfoEnabled) Log.Info("Scheduler started");
}

public void Close()
{
if (Log.IsDebugEnabled) Log.Debug("Stopping scheduler");

if (null != this._springContext)
{
this._springContext.Dispose();
this._springContext = null;
}

if (Log.IsInfoEnabled) Log.Info("Scheduler stopped");
}

public void Dispose()
{
if (null != this._springContext) this.Close();
GC.SuppressFinalize(this);
}
}


ContextHost is strictly responsible for creating and destroying the ApplicationContext.

OK. I lied. Since I have this solution residing in a Windows Service, I need my service class. It looks like this:

public partial class Host : ServiceBase
{
private static ILog Log = LogManager.GetLogger(typeof (Host));

private ContextHost _contextHost = null;

public Host()
{
InitializeComponent();
}

protected override void OnStart(string[] args)
{
try
{
_contextHost = new ContextHost();
_contextHost.Create();
}
catch (Exception e)
{
_contextHost.Close();
Log.Error("Caught unhandled exception", e);
}
}

protected override void OnStop()
{
if (null != _contextHost) _contextHost.Close();
}
}

When my service starts, ContextHost is used to create the context. When my service stops, the application context is destroyed. Nothing could be simpler.

Automatically Backing up a Blogger.com Blog

I’ve been in the computer software industry for a very long time. Long enough to know that the unexpected does happen and sometimes it causes a data loss. I can humbly say that there were times when the work that I have done was the cause of one or two “incidents”. With that said, I understand the importance of redundancy in some form.

Recently I stood up a Windows Home Server for the main purpose of automatically backing up the four computers that I have around the house.  It does an excellent job. With that done, my next goal was to use it to backup my blog. I don’t have a lot of posts but it’s still an investment that I don’t want to take a chance losing.

The manual way

I started the way that must computer people start – with a search engine. Unfortunately I didn’t find anything that I liked. The most obvious answer was to use the Export feature that Blogger provides. On the Blog Settings page of the Blogger.com Dashboard are a set of tools for Importing and Exporting a blog.

BlogTools

The Export functionality does exactly what I want with one exception: it’s not automatic. There also nothing within the Dashboard that allows me to schedule an export. Almost there…

Blogger API

It turns out that Blogger.com has an API that can be used to access and manipulate a blog. They’ve even gone as far as offering clients in a number of different languages: .NET, Java, JavaScript, PHP, and Python. So I took this as an opportunity to start another side project. I decided to create a service that would use the Blogger API and export my blog on a regular basis. And I’ll use my Windows Home Server to host the service.

I decided to not use the .NET client that was available and instead go with the native HTTP protocol that was provided. Why? Just to be different…

The export request is straight-forward to create but requires an Authentication header. So I started with that.

Authentication

The Blogger API supports three authentication methods: AuthSub, ClientLogin and OAuth.

Both AuthSub and OAuth  were designed to allow web-based applications to authenticate on behalf of a user. Since I’m not writing a web app, they weren’t applicable. 

ClientLogin was designed to allow a user to authenticate from a desktop applications. I’m not writing a desktop application but the ideal holds. Basically you pass the email and password to Google authentication service and Google returns an authorization token. You then pass the token back with any other request that is being make on behalf  of the user.

To make the Authentication request, you do a POST request to:

https://www.google.com/accounts/ClientLogin

I create the request like this:

        private HttpWebRequest CreateAuthenticationRequest()
{
var uri = new Uri("https://www.google.com/accounts/ClientLogin");
HttpWebRequest authenticationRequest = (HttpWebRequest)WebRequest.Create(uri);
authenticationRequest.AllowAutoRedirect = false;
authenticationRequest.Method = "POST";
authenticationRequest.ContentType = "application/x-www-form-urlencoded";
authenticationRequest.KeepAlive = false;
authenticationRequest.Expect = string.Empty;
authenticationRequest.Headers.Add("GData-Version", "2");
authenticationRequest.Timeout = this.TimeoutInMs;

var postBody = new StringBuilder();
postBody.Append("accountType=GOOGLE&");
postBody.AppendFormat("Email={0}&", this._email.ToUrlEncoded());
postBody.AppendFormat("Passwd={0}&", this._password.ToUrlEncoded());
postBody.Append("service=blogger&");
postBody.AppendFormat("source={0}", "malevy.net-Blogger.Backup-1".ToUrlEncoded());

if (logger.IsTraceEnabled) logger.Trace("body of post: " + postBody.ToString());

byte[] encodedData = (new ASCIIEncoding()).GetBytes(postBody.ToString());

authenticationRequest.ContentLength = encodedData.Length;
var stream = authenticationRequest.GetRequestStream();
stream.Write(encodedData, 0, encodedData.Length);
stream.Close();

return authenticationRequest;
}


The ToUrlEncoded() is an extension method that Url encodes a string. I based it on a post from Rick Strahl.



(Just a word of caution, the service parameter is case-sensitive. I wasted *way* too much time on that little typo)



If the request succeeds, you’ll get back a 200 response with a body containing SID, LSID, and Auth values. The documentation says that we can ignore the SID and LSID values. Only the Auth value is required.



To use the Auth token, you attach it as a header to a request using the following format:



Authorization: email Auth={the-returned-auth-value}



Archive/Export



This is where things broke down for me. I’ll save you the reading time with a quick summary. I couldn’t get the Archive function to work. The request is really simple:



GET http://www.blogger.com/feeds/{blog-id}/archive



I created the request and attached the authorization token as laid out in the documentation. I was rewarded with a response saying that the authorization header was not recognized. I then spent a lot of time searching the Internet and came up with nothing. I even tried formatting the request and authorization header differently and I still wasn’t able to get it to work. As a last resort, I posted a message to the Blogger Developer Group. Posts from new members are moderated so I’ve not even seen my question appear in the list yet.







Many of the Blogger APIs require that you supply the ID of your blog. If you’re like me and don’t know the value, this Blogger Help article will help.


 



The Workaround



In the meantime, I’ve decided to use the querying capabilities of the API to pull down all the posts. The request looks like:



GET http://www.blogger.com/feeds/{blog-id}/posts/full?updated=1990-01-01T00:00:00&orderby=updated



Which basically retrieves all the posts that have been updated since midnight on 1 Jan 1990. I use the following code snippet to create the request:



        private HttpWebRequest CreateExportRequest(AuthorizationToken token)
{
const string archiveUriFormat = "http://www.blogger.com/feeds/{0}/posts/full?updated=1990-01-01T00:00:00&orderby=updated";
var uri = new Uri(string.Format(archiveUriFormat, this._blogId));
HttpWebRequest exportRequest = (HttpWebRequest)WebRequest.Create(uri);

exportRequest.AllowAutoRedirect = false;
exportRequest.Method = "GET";
exportRequest.KeepAlive = false;
exportRequest.Expect = string.Empty;
exportRequest.Headers.Add("GData-Version", "2");
exportRequest.Timeout = this.TimeoutInMs;

exportRequest.Headers.Add(token.ToAuthorizationHeader());

return exportRequest;
}


I’m going to stick with this approach until I can get some help with the Archive/Export functionality.



In the next post, I’ll discuss the form and architecture of the application. In particular I’ll discuss using Quartz.Net to kick off the archive functionality and the use of Spring.Net to pull everything together.