XML documents as a data store

In this recipe, we are going to look at a non-SQL-oriented solution. There are times when you might not need a full blown SQL server backend or you might just not have access to one. In those times, storing the data on the filesystem might be more appropriate. For that reason we will take a look at how to remove SQL server from the picture and plug in an XML data store in its place. We will see that even with this drastically different infrastructure on the backend, we can still work within the boundaries of our existing solution and easily plug in this new data store.

Getting ready

The only thing we need to do to get started with this recipe is prepare our new data storage. We want to swap out our existing SQL server database and make an XML data store. To do this, all we need to do is create an XML folder inside of the App_Data folder. This will be the location we will write our XML documents to.

With that out of the way, we next need to discuss how these XML documents will look. In this recipe, we will write one XML file per Post. This Post document will contain a collection of Comments inside of it. The reason for this is that a Post is important on its own. A Comment by itself means nothing without its parent Post. When talking about document stores, especially where the item being stored is as simple as our Comment, we can store our value types (Comment) with our entities (Post) directly.

Working with .NET like we are, creating XML directly from our objects is as easy as serializing them directly to the filesystem. When we need to read the XML back into our program, we simply reverse the process and de-serialize our file back into an object. In order to locate the appropriate post, we will use the Post's Id, which is a Guid as the filename.

Keep in mind that the system that we are creating in this recipe is not an absolute best practice. It is prone to several issues, such as file locking and write-access restrictions. Also, finding a Post is easy enough, as we named the file with the Post's identifier. But locating other data, such as a specific Comment, becomes problematic (we will look at this shortly). Having said that, this solution may be just the one you need! Just make sure you weigh the pros and cons prior to choosing the filesystem for your data storage mechanism.

How to do it...

  1. Create a copy of the previous recipe's solution.
  2. Open the solution. Then expand the App_Data folder. Create a new folder inside there called XML.
  3. Next, we need to add a new method to the ConfigurationService class called GetXmlConnectionString. This method will determine the file path to the root of our application. It will then append App_DataXml to that path, so that we can get to our XML store.

    Models/ConfigurationService.cs:

    ...
    public string GetXmlConnectionString() {
    string path = HttpContext.Current .Request.PhysicalApplicationPath + "App_Data\Xml\";
    return path;
    }
    ...
    
  4. In an XML-based data store, we don't really need a Connection object. Instead we need something to manage access to the filesystem. When dealing with files, we don't want more than one thread accessing a file at a time (as that will throw an exception). In that case, we need to create a class that controls the number of allowed instances that are created across our entire application. We will use the singleton pattern to do this. Create a new class called XmlStore in a new Models/Repository/XmlRepository directory. Create a private constructor to ensure that no instances can be created directly. Also, create a static property to control the instance that is initially created and create a lock to allow us to check and create our instance.

    Models/Repository/XmlRepository/XmlStore.cs:

    public sealed class XmlStore {
    static XmlStore instance = new XmlStore();
    static readonly object padlock = new object();
    XmlStore() { }
    public static XmlStore Instance {
    get {
    lock (padlock) {
    return instance;
    }
    }
    }
    }
    
  5. With our class controlling the number of instances that are created, we can now move to creating some helper methods to control accessing the filesystem. The first method that we will create is a Delete method, which will take the ID of the Post to be deleted.

    Models/Repository/XmlRepository/XmlStore.cs:

    ...
    public void Delete(Guid postId) {
    string file = new ConfigurationService().GetXmlConnectionString() + postId + ".xml";
    if (File.Exists(file)) {
    File.Delete(file);
    }
    }
    ...
    
  6. The next thing that we need is the ability to write a file to the filesystem. This will be handled with a Write method. This method will create a serializer to handle our Post object. Then we will attempt to serialize the object to the filesystem using a TextWriter.

    Models/Repository/XmlRepository/XmlStore.cs:

    public void Write(Post post) {
    string file = new ConfigurationService().GetXmlConnectionString() + post.PostID + ".xml";
    // Force Domain Model to check for comments first.
    var comments = post.Comments;
    var s = new XmlSerializer(typeof(Post));
    var fs = new FileStream(file, FileMode.Create);
    TextWriter w = new StreamWriter(fs, new UTF8Encoding());
    try {
    s.Serialize(w, post);
    }
    catch (Exception e) {
    Console.WriteLine(e.Message);
    }
    finally {
    w.Close();
    }}
    
  7. The Read method is basically Write in reverse. We create a FileStream to consume the XML file and an XmlSerializer to deserialize the resulting stream into our Post object.

    Models/Repository/XmlRepository/XmlStore.cs:

    public Post Read(Guid id) {
    var file = new ConfigurationService().GetXmlConnectionString() + id.ToString() + ".xml";
    // Assume Comments precache
    if (!File.Exists(file))
    return new Post { PostID = id, Comments = new List<Comment>() };
    var stream = new FileStream(file, FileMode.Open);
    Post result;
    try {
    result = (Post)new XmlSerializer(typeof(Post)).Deserialize(stream);
    }
    catch (Exception e) {
    result = new Post();
    Console.WriteLine(e.Message);
    }
    finally {
    stream.Close();
    stream.Dispose();
    stream = null;
    }
    return result;
    }
    
  8. Now that we are able to create a file, delete a file, and read a file, you might think we have all that we need. However, we need to also be able to read all of the files in the store. This ReadAll method will simply iterate through all the files in the directory and reconstitute each of them as Post objects.

    Models/Repository/XmlRepository/XmlStore.cs:

    public List<Post> ReadAll() {
    List<Post> result = new List<Post>();
    string path = new ConfigurationService().GetXmlConnectionString();
    DirectoryInfo di = new DirectoryInfo(path);
    FileInfo[] rgFiles = di.GetFiles("*.xml");
    foreach (FileInfo fi in rgFiles) {
    result.Add(Read(Guid.Parse(fi.Name.Split('.')[0])));
    }
    return result;
    }
    
  9. The final method that we need to create is one to allow us to look beyond posts. We also need the ability to locate a comment within a post. This is where our current data store scheme starts to let us down a bit. If this were a real-world application, we might create a Lucene index, which would allow us to search for a CommentID and map it to our PostID, thereby allowing us to load the specific file that contains the comment we are interested in. But as this is a simple demo, we will instead need to iterate through each Post and search the collection of comments within the Post. If we find the comment, we then return it.

    Models/Repository/XmlRepository/XmlStore.cs:

    public Comment SearchForComment(Guid commentId) {
    Comment result = null;
    string path = new ConfigurationService().GetXmlConnectionString();
    DirectoryInfo di = new DirectoryInfo(path);
    FileInfo[] rgFiles = di.GetFiles("*.xml");
    foreach (FileInfo fi in rgFiles) {
    Post post = Read(Guid.Parse(fi.Name.Split('.')[0]));
    result = post.Comments
    .Where(c => c.CommentID == commentId).FirstOrDefault();
    if (result != null)
    return result;
    }
    return result;
    }
    
  10. Now we are finally ready to stand up our Post and Comment repositories. Start by creating the XmlPostRepository and XmlCommentRepository classes. Next, configure those classes to implement the IPostRepository and ICommentRepository interfaces.
  11. Now create an XmlRepositoryConfiguration file and set it to implement the IRepositoryConfiguration interface. Then generate the appropriate methods by making the XmlRepositoryConfiguration class implement the interface.
  12. With the base framework in place for our new XmlRepository, we now need to configure StructureMap, so that it is aware of our new repository. Do this by creating a new class called XmlRepositoryRegistry class in the Models/StructureMap directory. In this class, we will configure all the mappings that are needed for our host application.

    Models/StructureMap/XmlRepositoryRegistry.cs:

    public class XmlRepositoryRegistry : Registry {
    public XmlRepositoryRegistry() {
    For<ICommentRepository>().Use<XmlCommentRepository>();
    For<IPostRepository>().Use<XmlPostRepository>();
    For<IRepositoryConfiguration>() .Use<XmlRepositoryConfiguration>();
    }
    public static void Register() {
    ObjectFactory.Initialize(x => x.AddRegistry( new XmlRepositoryRegistry()));
    }
    }
    
  13. With our registry created, we can then map the StructureMap registry into the start of our application.

    Global.asax.cs:

    protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    XmlRepositoryRegistry.Register();
    //run any data access configuration that might be needed
    ObjectFactory.GetInstance<IRepositoryConfiguration>() .Configure();
    }
    
  14. With all of our framework and plumbing set up, we are now ready to add the appropriate implementation to our repository classes. With the majority of the work performed by our XmlStore, the actual code in our repository classes is on the light side.

    Models/Repository/XmlRepository/XmlPostRepository.cs:

    public class XmlPostRepository : IPostRepository {
    private XmlStore _xmlStore;
    public XmlPostRepository() {
    _xmlStore = XmlStore.Instance;
    }
    public void AddPost(Post post) {
    post.PostID = Guid.NewGuid();
    post.CreateDate = DateTime.Now;
    _xmlStore.Write(post);
    }
    public void UpdatePost(Post post) {
    _xmlStore.Write(post);
    }
    public void DeletePost(Post post) {
    _xmlStore.Delete(post.PostID);
    }
    public Post GetPost(Guid postId) {
    return _xmlStore.Read(postId); ;
    }
    public List<Post> GetPosts() {
    return _xmlStore.ReadAll();
    }
    }
    
  15. Now we can build the XmlCommentRepository. Oddly enough, there is more actual work to do in the comment repository, as we need to find the right post file to perform the appropriate actions on a comment.

    Models/Repository/XmlRepository/XmlCommentRepository.cs:

    public class XmlCommentRepository : ICommentRepository {
    private IPostRepository _postRepository;
    private XmlStore _xmlStore;
    public XmlCommentRepository(IPostRepository postRepository) {
    _postRepository = postRepository;
    _xmlStore = XmlStore.Instance;
    }
    public void AddComment(Comment comment) {
    Post post = _postRepository.GetPost(comment.PostID);
    List<Comment> comments = post.Comments;
    comments.Add(comment);
    post.Comments = comments;
    _postRepository.UpdatePost(post);
    }
    public void DeleteComment(Comment comment) {
    Post post = _postRepository.GetPost(comment.PostID);
    List<Comment> comments = post.Comments;
    comments = comments.Where(c => c.CommentID != comment.CommentID).ToList();
    post.Comments = comments;
    _postRepository.UpdatePost(post);
    }
    public Comment GetComment(Guid commentId) {
    return _xmlStore.SearchForComment(commentId);
    }
    public List<Comment> GetCommentsByPostID(Guid postId) {
    Post post = _postRepository.GetPost(postId);
    if (post != null)
    return post.Comments;
    return new List<Comment>();
    }
    public void DeleteComments(Guid postId) {
    Post post = _postRepository.GetPost(postId);
    post.Comments = new List<Comment>();
    _postRepository.UpdatePost(post);
    }
    }
    
  16. Because we are not working with a database here, I also found that thevDeleteComment view doesn't quite provide us with enough information to allow us to quickly locate the appropriate post in our XML data store. For that reason, I had to add one more hidden field in the DeleteComment view exposing the PostId in the form post.

    Views/Home/DeleteComment.aspx:

    ...
    <% using (Html.BeginForm()) { %>
    <p>
    <%= Html.HiddenFor(m=>m.PostID) %>
    <input type="submit" value="Delete" />
    |
    <%: Html.ActionLink("Back to List", "Index") %>
    </p>
    <% } %>
    ...
    
  17. A small change is also required to the home controller.

    Controllers/HomeController.cs:

    [HttpPost]
    public ActionResult DeleteComment(Guid id, Guid postId) {
    ObjectFactory.GetInstance<ICommentRepository>().
    DeleteComment(new Comment { CommentID = id, PostID = postId });
    return RedirectToAction("Index");
    }
    
  18. You should now be able to run the application and manage posts and comments as you were able to in any of the other recipes prior to this one.

How it works...

In this recipe, we swapped out our previous use of a SQL server database for data storage on the filesystem using XML files. The key to this process is the ability to serialize our existing POCO classes directly to the filesystem.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset