RavenDB Caching done right (EventsZilla part II)
In the previous post we created the basics for an events publishing application, and discussed the modeling aspect of things.
I put some more work into the app, and now it actually works and looks pretty nice. Queries and loads are in place for the front-end, so it is time to visit one key feature of RavenDB - Caching.
Basic caching
The RavenDB Client API provides automatic out-of-the-box caching for all read operations. Every data request sent to the server is being remembered by the document store object, so subsequent read operation that are detected as identical can return immediately.
However, it is important to beware of common pitfalls which may cause you not to take advantage of this handy feature. While there's no real way to mess up with simple Load operations, it is very easy to do that when querying.
For example, the most common query in an application like EventsZilla is to get events starting before or after a certain point in time, usually DateTimeOffset.Now. However, a query like this is guaranteed to never use the cache, since it is virtually different every time it is called.
In EventsZilla we can fix this relatively easily, by lowering the DateTimeOffset resolution when querying. Another approach will be to round up (or down) the value. The actual resolution or rounding approach we use will determine how much of caching this query will take advantage of.
Relevant code can be found here.
Aggressive caching
Basic caching is very effective, requires no action from the user's end to work, and is a great feature for automatically improving your applications performance. However, a server query is still issued with every read operation to make sure the cache never goes stale. The actual benefit with basic caching is with getting back a quick response of a thin 304 (HTTP for "I haven't changed") instead of a complete 200 response with all the requested data.
At times, we load an object - or perform a query - that we really don't care if it changes for a certain period of time, or we just don't expect it to. If we choose to, we can tell RavenDB not to query the database at all if it has a cached response that is not older than a given point in time.
This feature is called an Aggressive Caching, being aggressive in the sense of not peeking outside the cache at all. Unlike basic caching it is an opt-in feature.
In EventsZilla, this is exactly the case with a website-wide config object. We don't expect it to change a lot, and when it changes, we can bear a certain amount of time until the changes are noticeable in our website.
All we need to do to make it happen is load that object within a context of an AggressiveCache, and the RavenDB Client API will take care of the rest for us.
Using Aggressive Caching is as simple as this:
using (RavenSession.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(30)))
{
var siteConfig = RavenSession.Load<SiteConfig>(SiteConfig.ConfigName);
}
More on caching
Is in the second part of the excellent RavenOverflow video, available here.
EventsZilla: RavenDB modeling walkthrough
I needed a simple event publishing application. I also felt like doing another RavenDB sample app and a RavenDB post on it. This is how EventsZilla came to life.
EventsZilla (full sources here: https://github.com/synhershko/eventszilla) is meant to be a simple web application to announce events along with a schedule, which is also capable of viewing past and future events. People should be able to register to an event without registering with the website, and also view slides and other content when it becomes available post-event.
This post is being written during development, describing each stage and the considerations leading to the next. As such, the code I link to does not necessarily work, although it should. I will probably have some fixes and amendments made to the code after publishing this post.
Initial modeling
When we speak of an events publishing application, what are we looking at? The most basic items are an Event with means of registration, and a list of sessions for each Event. Each event should have a registration window, and a venue in which it takes place, and obviously title and description.
For each session in an event we want to have a Presenter (possibly more than one), a title, a brief (aka abstract), and times in which each session starts and ends. We should note the start and end time of the event are going to be derived directly from the first and last sessions of the event. For now we call a session a "Schedule slot".
Unlike a relational model, with RavenDB we can sketch the entire thing as one class and just use it. There is one exception though - at this stage we already know venues and presenters might be showing several times in different events (maybe even the same presenter in multiple sessions in the same event), so we don't want to store them directly under the event, but rather link to them by storing their IDs only. They could be efficiently retrieved using the Includes feature.
We end up with this Event class:
public class Event
{
public Event()
{
Schedule = new List<ScheduleSlot>();
}
public int Id { get; set; }
public string Title { get; set; }
public string Slug { get; set; }
public string Description { get; set; } // markdown content
public string VenueId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset RegistrationOpens { get; set; }
public DateTimeOffset RegistrationCloses { get; set; }
public int AvailableSeats { get; set; }
public class ScheduleSlot
{
public List<string> PresenterIds { get; set; } // list of person IDs
public string Title { get; set; }
public string Brief { get; set; } // markdown
public DateTimeOffset StartingAt { get; set; }
public DateTimeOffset EndingAt { get; set; }
}
public List<ScheduleSlot> Schedule { get; set; }
public DateTimeOffset StartsAt
{
get
{
var firstSession = Schedule.OrderBy(x => x.StartingAt).FirstOrDefault();
return firstSession == null ? DateTimeOffset.MinValue : firstSession.StartingAt;
}
}
public DateTimeOffset EndsAt
{
get
{
var lastSession = Schedule.OrderByDescending(x => x.EndingAt).FirstOrDefault();
return lastSession == null ? DateTimeOffset.MaxValue : lastSession.EndingAt;
}
}
}
Since an event schedule has no meaning outside the scope of an event, it is best persisted there as well. It also means the whole schedule will be loaded with the event with each Load or Query operation this event will be part of. At this stage we are fine with that.
The StartsAt and EndsAt properties of the Event are persisted this way to take some pressure off the indexes we are going to create, so business logic will reside in the actual domain types instead of in the indexes as much as possible.
The Venue and Presenter classes are quite trivial ones, so won't be shown here.
The actual code for this phase is in this github commit.
Event registration
Registering to an event is quite a common operation in our system, and in a crowded website multiple registrations to the same event can be made at the same time.
Like an event schedule, an event registration has no meaning at all outside the scope of the event itself, at least as long as we don't try and keep track of attendees (which we don't). Unlike the schedule, the attendees list is going to change quite a lot, and often at the same time. For this reason, keeping this list within the Event object itself won't make sense, as it will require us to start thinking about conflict resolution, when 2 or more people try to register for the same event on the same time.
Another reason not to save registrations within the Event itself is we don't really care about it when we load an event, and that list can grow quite big for certain events. We want to make sure the Event object only holds data we are going to access frequently at that context; the registrants list is not that type of data.
To keep the registrants list separate, while still making sure we don't need to worry about possible conflicts, I created a simple EventRegistration class which will hold of all that data. We persist it exactly that way, and whenever we need to know the amount of people who registered to the event, we query the DB for a count of registrations for that event. That query is using a simple static index we defined upfront.
public class EventRegistration
{
public string EventId { get; set; }
public string RegistrantEmail { get; set; }
public string RegistrantName { get; set; }
public DateTimeOffset RegisteredAt { get; set; }
}
Actual code for this phase is in this commit.
Available seats
Still in the context of registrations, it is important to note that by design we are not necessarily blocking registration for an event the second it is full. The reason for this is that we are getting the number of people that registered to the event through an index query, and while RavenDB's indexing process is quite fast, it is possible on a busy websites this query will return a number that is not entirely up to date.
In EventsZilla, this is a design decision we made, not to think like a computer. Your PC knows to respect a hard limit, but in life, we hardly really do that. So if your event has 100 seats, wouldn't you be able to squeeze 10-15 more? and are you really that sure all of the original 100 registrants will indeed show up?
For that reason we don't care if the count we got back from our query is not the actual count at the point of time where we issued the query. This is quite a common practice with Eventual Consistency - we don't try hard to respect hard limits that don't really exist anyhow, and wonderful things happen.
RavenDB can tell us the count we got is stale. Waiting for non-stale results is possible, but in production is really not recommended, as it is going to significantly slow down your system. In some cases it can also result in an infinite wait. So don't do that.
If we really needed to keep to a hard limit, we could add a RegistrantsCount property to our Event class, and increment it with every registration. It requires a bit more work to make sure concurrent writes are detected, and one is delayed, so no incorrect counts happen, but it will ensure we can know the exact count at all times, since we could then retrieve that event using a Load operation, which is ACID.
Next in line
As time allows, I will explore our possibilities for times when we want to enable different tracks in the event, and to better support events with only one session. Also in my to do list is letting each schedule slot have materials such as slides, video, code samples etc. Stay tuned.
The Oredev “Lost Session”
This week I'm in Malmo, Sweden for Oredev - looking forward for a great conference.
Wednesday evening, about an hour after the last session for the day, I will be giving a RavenDB session in KAN's offices. There are a few seats available - more details and registration here: http://thelostsession.kan.se/.
If you live nearby, or attending the conference, we would love to see you there. The evening is free, and there will be food and beers.
Creating a documentation system – Part 1
A while ago we started revamping the documentation of RavenDB. The work on that resulted in quite a nice documentation system that will be described in general in this post, and more posts will follow as we make more progress and introduce new features to it.
For some time now it was clear that much more organized docs were needed for RavenDB, and it had to be complete too. A lot of content is scattered around the net on blogs and FAQs, and we started gathering it all, arranging it and rewriting the docs almost from scratch. It was also obvious some content is worthy of being available, but is not really a "documentation" content - so we had to figure out what to do with such content too.
Also, one of the reasons the old docs were scattered around in blogs and FAQs is the rapid development process of RavenDB. So to add to all that, we needed to find a way to keep up with that - for example, by having working code samples at all times.
Documentation changes over time as best practices change or new features are added in, and so we needed to take that into account as well - being able to version the docs and see past revisions. Another important factor was community content - we wanted to allow the community to be able to respond, suggest fixes or additions, and to offer new content, even if it is not "documentation" per se.
Wiki sounded a bit too much, and we didn't really want to build something of our own. We played with some ideas for a while, until we had it all figured out.
Don't reinvent the wheel
The most important rule of all - don't waste your time creating a tool that you already have on your belt. In our context those are git and Markdown.
With git, we get easy content versioning, and it gets extremely easy to accept patches from other writers (forks and pull requests). Versioning is simply a matter of branching or tagging - the master HEAD is always the latest docs for the latest version, and whenever we want to mark a version we just create a branch from the working copy, or tag it. Obviously, github plays a huge role here - our documentation system even has an issue tracker, for heaven's sake...
And of course, Markdown is a natural fit. A super-simple text-based markup language, which is very easy to write with. Combined with git it really shines and is able to produce very nice diffs. It never was so easy to track documentation revisions.
Markdown also allows us to export documentation in various formats. On our website we show it as HTML, but it can also be compiled to a PDF book and other e-book formats. We will touch this in detail later on the series.
This is how we got our 3rd party hosted full-featured Wiki for documentation, which is available on github: https://github.com/ravendb/docs.
Editorial notes
Now that we had the basics figured out, we needed to decide on a structure. This actually proved very easy to do. Since we use git, all changes, including moving files around, are recorded and can be tracked. So if we represent each documentation item as a file, and store files under hierarchical folders we are pretty much done.
At the time of this writing we still don't have all the terminology figured out - what is a section, sub-section or a chapter. And at this stage we don't really care about all that. We just write the docs as it appears to make sense, and when it will all be done we should be able to revisit that.
We also created a Knowledge-Base section on the website, where content that is not "documentation" per se can still be published and viewed. All content that is considered out of scope for the actual documentation will be posted there - official articles by Hibernating Rhinos side-by-side with user generated content. The KB is a simple web application and has nothing to do with the documentation system, but in the larger scope - the product - it is important to have, both as means of providing extra content and for interaction with the community.
Code samples QA
To make sure all the code samples are up to date, we created a project with all the sample code used in the docs, and compile it to test the code is valid. With every new official release, we update it there and compile again, to make sure nothing requires changing. This way the code samples are guaranteed to stay up to date and work - what could never be the case with code that is in-lined in the docs themselves.
We added our own Markdown syntax which points to the code file, and write the code snippets within named #region-s, to make it easier to track and identify the code relevant to each page. If you ever wondered what #regions in VS are for, now you know
In the documentation, it looks like this:
{CODE region_name@folder/file.cs /}
And an actual code file with such regions can be found here: https://github.com/ravendb/docs/blob/master/code-samples/Intro/BasicOperations.cs
The custom Markdown syntax is parsed when compiling the docs, using a tool we developed that is now part of the documentation repository, before we resolve the markdown itself. The tool will go to the source files directory, locate the file, parse out the requested region, normalize the line spacing and inject it to the markdown source. Only then the source will be compiled and saved to the specified output.
Next...
In the next posts we will look at how the docs are actually compiled, how they are browsed in the website, and how we allow for versioning of docs.
Whose bug is it anyway? Google vs Microsoft
Consider the following code (.NET):
public static IEnumerable<SyndicationItem> ReadFeed()
{
IEnumerable<SyndicationItem> ret;
using (var reader = XmlReader.Create(ListAtomUrl))
{
var feed = SyndicationFeed.Load(reader);
if (feed == null)
return null;
ret = feed.Items;
reader.Close();
}
return ret;
}
This is a a simple code to read an ATOM feed, using the relatively new .NET syndication API. When executing it on a Google groups ATOM feed (http://groups.google.com/group/ravendb/feed/atom_v1_0_topics.xml, for example), it would fail miserably with this error:
Error in line 10 position 22. An error was encountered when parsing a DateTime value in the XML. Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. Exception Details: System.Xml.XmlException: Error in line 10 position 22. An error was encountered when parsing a DateTime value in the XML.
The reason for this error is this XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?> <feed xmlns="http://www.w3.org/2005/Atom"> <updated>-0-0T::Z</updated> <generator uri="http://groups.google.com" version="1.99">Google Groups</generator> <entry> <author>
Notice the "updated" tag. This only happens for the "topics" feed, not the "new messages" feed Google provides for each group.
So whose bug is it? My bet is on Google. Skimming briefly over the ATOM RFC, I could find no mention of a "n/a" value for the "updated" field, so I can't tell if its legit, but this value just doesn't seem right.
However, Microsoft is at fault here too by not providing a way to tolerate those kind of errors. After all, the syndication API is meant to be used with _external_ services, such that the developer would not have access too, and this API renders useless on the slightest bug a feed provider has. Fact is, no other reader I use had problems reading that feed.
Some updates on NAppUpdate
After having several issues with their auto-update mechanism, 2 weeks ago the Hibernating Rhinos profilers were updated to use NAppUpdate. Once again it was proven to be a very flexible and robust library, and several updates were already pushed to hundreds (thousands?) of users without any problem.
Before the profilers could start using NAppUpdate I had to make some updates to the library, namely: catch and expose the last error thrown (if any); fix an issue with UAC popping for updates on Windows 7 and Vista; better support for promptly cancelling a download mid-way; and a few other fixes and updates. These fixes are already available on github, and probably invalidate the 0.1 release...
Implementing NAppUpdate required custom implementation of a FeedReader and a Task, and the whole process didn't take more than one hour to code (testing is another story...). The profiler's AutoUpdateFeedReader makes a simple check against a very simple one-liner feed with the profiler's current version, and the it's AutoUpdateTask downloads the latest build as a zip file from the server, extracts it to a temporary folder and when told to overwrites the old files with the new ones in a bulk.
The actual task looks something like this - note the logical separation into steps, which are executed sequentially:
public bool Prepare(IUpdateSource source)
{
// Clear temp folder
if (Directory.Exists(updateDirectory))
{
try
{
Directory.Delete(updateDirectory, true);
}
catch {}
}
Directory.CreateDirectory(updateDirectory);
// Download the zip to a temp file that is deleted automatically when the app exits
string zipLocation = null;
try
{
if (!source.GetData(LatestVersionDownloadUrl, string.Empty, ref zipLocation))
return false;
}
catch (Exception ex)
{
Log.Error("Cannot get update package from source", ex);
throw new UpdateProcessFailedException("Couldn't get Data from source", ex);
}
if (string.IsNullOrEmpty(zipLocation))
return false;
// Unzip to temp folder; no need to delete the zip file as this will be done by the OS
return Extract(zipLocation);
}
public bool Execute()
{
// since all we do is a cold update, nothing other than backup needs to happen here
return true;
}
public IEnumerator<KeyValuePair<string, object>> GetColdUpdates()
{
if (filesList == null)
yield break;
foreach (var file in filesList)
{
yield return new KeyValuePair<string, object>(file, Path.Combine(updateDirectory, file));
Log.DebugFormat("Registering file {0} to be updated with {1}", file, Path.Combine(updateDirectory, file));
}
}
Triggering the actual check for updates is a one-liner (after configuring the UpdateManager instance with a feed URL, a FeedReader and all that; the task is created and returned by the custom FeedReader):
UpdateManager.Instance.updateManager.CheckForUpdateAsync(StartDownloadingUpdate);
// ...
private void StartDownloadingUpdate(int updates)
{
if (updates == 0) // no updates are available
return;
if (updates < 0) // an error has occurred
{
Log.ErrorFormat("Error while checking for updates: {0}", UpdateManager.Instance.LatestError);
return;
}
// If updates are found, start downloading them async
UpdateManager.Instance.PrepareUpdatesAsync(success =>
{
if (!success)
{
if (UpdateManager.Instance.LatestError != null)
{
Log.ErrorFormat("Error downloading updates: {0}", UpdateManager.Instance.LatestError);
}
return;
}
// Notify the user of the update, and call UpdateManager.Instance.ApplyUpdates() when ready
});
}
It couldn't be simpler than that, and it just works...
This has triggered some interest in the project, and wheels are now in motion again and hopefully new features will be introduced soon, followed by a 0.2 release.
As always, you can grab the sources and file bugs here. Bugs and feature-requests can also be submitted to the mailing list.
SisoDB: The wrong solution to the wrong problems
Data structures are the corner stone of computing. If you get them done right, you will most probably succeed in your mission of delivering an application that uses its resources wisely and performs well.
In modern computing, most data is stored to and retrieved from databases. Databases are data structures' big brothers - they serve the same purpose but with added value. Choosing one wisely can greatly help you in some many ways; going with the wrong one would cost you too much.
This is why one should not take lightly the decision of which database solution to use.
Dealing with data explosion
Since the 70's, whenever data had to be persisted, RDBMSes were the most effective and trusted tools to use. Since OOP became dominant developers found it quite itching to stuff their hierarchical entities into the flat structure of Tables and Rows, which is the ABC of RDBMSes. This is how ORMs came to life.
Coming to think of it retrospectively, ORMs were never the solution. They just made the problem less itching. In practice, your data still had to go through quite an awful lot of processing until it was persisted to, or loaded from store. But as long as it was transparent for the developer, and he knew that loads of optimization is happening under the hood, it seemed like there's nothing to worry about.
Although the concepts were known since the 80's, it was not until recent years real object-, document-, and graph-databases came into life. It took big players like Facebook and Twitter to get those ideas to mature and become production ready. Someone (or a handful of them) realized a shift in thinking is essential, and real-world problems like replication and sharding suddenly seemed a lot less complicated. As a result the NoSQL movement (or whatever it has become) is now full-steam ahead, and data-access best practices are being re-written.
Each NoSQL brand introduces some cool unique features, never seen in RDBMSes before. Document-oriented databases introduced the "schema-less" concept. That is, unlike in traditional RDBMSes, defining a data scheme is no longer required. The DocDB would either figure it out on its own, or it wouldn't even bother to. Data schemes are required in RDBMSes to define the table structure and allow for efficient indexing; DocDBs have a different go at it - Map/Reduce.
SisoDB choosing the wrong battle
SisoDB is the new face in town, but it looks like it is choosing the wrong battle. The problems it tries to solve are not real problems. Let me explain.
The SisoDB website explains the motivation behind SisoDB: the need of a real schema-less solution for data storage, while at the same time making sure the powerful tools offered by SQL Server are still available. ORMs are deemed evil because they require mappings, which contradicts the notion of schema-less, and non-MS-SQL backend is probably deemed irrelevant too. This is probably why there are no providers for Oracle nor MySQL.
So, in SisoDB data is now schema-less, but it spans over 3 tables per entity. This is how it looks (taken from the SisoDB site):
And the question arises: if real schema-less database is what you're after, a direct-POCO-to-storage-and-back-again solution, why would you use SisoDB with SQL Server in the first place? You can just use a NoSQL schema-less database, and if you treasure MSSQL's reporting tools that much just find a way to still be able to use them! When resorting to not using a NoSQL database, you lose ALL the possible sweet spots such products have to offer - which MsSQL offers none. And there are so many of them.
Nowadays it doesn't make sense to use SisoDB, neither in new development nor in existing applications. It may feel like being schema-less, but its fundaments are too deep in the RDBMS world, and it shows - to name a few:
- Deep hierarchies and enumerables are not supported
- Entity ids ought to be named SisoDb, making it harder to integrate with existing code
- You can't specify string ids for entities (ids have to be int or Guids)
- You have to CREATE your databases
- For every model change you have to tell SisoDB to update the model; it will not be detected automatically, and a schema update is still required.
- Various SQL common faults, like SELECT N+1 or not batching where possible.
- Sharding and replication, other strong characteristics of NoSQL databases, are by definition one mile behind.
Some performance numbers were posted by the author comparing SisoDB and other ORMs for inserts. But queries are what you should really care about; and you are going to be disappointed. The most extensive indexing feature SQL has - relations between tables - is not being used in SisoDB by design. SisoDB doesn't define FKs, and doesn't operate JOINs. Put simply this means that by design SisoDB harms lookups performance, which is hands down the most crucial part of your application. You don't want this.
Just for comparison: RavenDB is a document database written in .NET, schema-less too and uses POCOs or raw JSON, with no mapping whatsoever, which uses Linq for querying. But it is real NoSQL, and as such it is offering much more natural replication and sharding functionality. Other features include full-text search out-of-the-box, entity versioning, REST API, complex and super-fast indexes, embedded mode, Silverlight support, and much more. And RavenDB comes with the ability to replicate its indexes to MsSQL so the reporting tools can still be used even though you're in NoSQL land.
If you were able to convince your bosses to use NoSQL, go with a real NoSQL solution. If not, try again. If you still fail, just keep using your favorite ORM and if mapping annoys you find ways to automate that process instead.
NAppUpdate now has a mailing list
I'm getting a lot of feedback on NAppUpdate, and obviously there's plenty to discuss on how to make it better and bugs-free.
So I went ahead and created a mailing list / Google group dedicated to NAppUpdate issues, suggestions and discussions. I'll be posting all future announcements to the list, too. All issues with implementing NAppUpdate in your solution, or thoughts on missing features, should be addressed there.
The group is here: http://groups.google.com/group/nappupdate.
All development work, and feature / issue tracking is on our github repository: https://github.com/synhershko/NAppUpdate. Source and binary downloads can be made from there.
Lets make application updates in .NET a breeze.
Thoughts on WPF / Silverlight
While developing quite a simple Silverlight application a few months ago I noticed how lagish it can become. Now I came across this article, and after realizing I wasn't imagining things I'm starting to realize how immature WPF and Silverlight are. And that is without mentioning some severe bugs WCF has - and you can't use Silverlight properly without a WCF host.
Here are a few snippets from the article and comments:
How many times have you had to scale back you UI because it was too jerky? How many times have you came up with the “groundbreaking new UX model” that you had to scrap because the technology couldn’t handle it? How many times have you told a customer they require a 2.4ghz quad core to get the full experience? I’ve been asked by customers why they cannot deliver the same fluid UX they have on their iPad application using WPF or Silverlight on a PC with four times the horses. This technology may be good enough for line-of-business applications, but it falls short of being able to deliver a next generationconsumer application.
I get the same question from other developers and management: “why do phone applications running on dinky ARM processors feel so much smoother?”
Then I will see some cool HTML5 canvas example posted on Reddit, check the CPU loading it causes, and think there’s no way I could do that in WPF without 2x the loading. Even jquery widgets seem to behave more responsively than my WPF app.
My 2 cents: if you're looking to create a rich UI for the desktop, take a look at HTMLayout (.NET bindings here); I find the way XAML data bindings work too clumsy anyway. For web experiences, you should be all set with jQuery and HTML5; if you're looking to create games you should use Flash.
NAppUpdate first release: 0.1
NAppUpdate (pronounced: nap-date), a small and flexible open-source library providing consumer applications with auto-update functionality, is now being officially released, and is ready for production.
About a month ago I released a first version of my local copy of the library, and posted this first post with some info and code samples showing how to use it from any .NET application. The response I received was tremendous, compared to the short time frame. I wish there was a way to know how wide its usage is now!
I would like to thank Andrew Rowson and Jamie Lennox for jumping in to make several code tweaks based on their experience with the library - mainly to resolve issues with non-privileged users. NAppUpdate is now confirmed to work very well on Windows 7, Vista and XP. Thanks guys!
The post mentioned above is a bit outdated now. Also, there are quite a few features I'd like to see added to the library, such as status reporting and a more flexible handling of tasks and rollbacks. Andrew, Jamie and I have already discussed several ways of tackling those features, hopefully you'll see them in soon. Right after the API stabilizes, I will work on creating some documentation in the library's git-wiki space on github, so anyone can jump-start easily with using it.
So far it has been a great experience. Hopefully the marking of version 0.1 will encourage more users and collaboration. See you again soon with 0.2!
NAppUpdate, available freely from: http://github.com/synhershko/NAppUpdate (ASL 2.0)
Binaries and a sample app with source: http://github.com/synhershko/NAppUpdate/downloads
(Edit) A user/developer discussion group is now available at: http://groups.google.com/group/nappupdate
