Skip to content
Najeeb Shaikh edited this page Apr 6, 2021 · 14 revisions

Welcome to the ObjectStore wiki!

Introduction

What is ObjectStore?

ObjectStore is an object versioning, dependency management, and cascading event propagation library written entirely in C# on the .Net platform. The library mainly does three things:

  1. It auto-versions objects as they are created and modified;
  2. It lets the caller create dependencies between objects; &
  3. It propagates modifications made to any object as events down its entire hierarchy of objects that depend on it, as a separate event to each such dependent object.

The endeavor has been to make the library as declarative as possible, which it does more or less without any explicit code on the part of the client. In that sense it is a tad opinionated.

To understand the ObjectStore library better, we will take the example of a simple class ecosystem in an e-commerce scenario as listed below.

  1. Tax
  2. Category
  3. Product

The Tax class encapsulates every thing to do with taxation (or at least to the extent that is required for this library to be demo'ed). Similarly, the Category class represents product categories, while the Product class is a simple analog of an actual product. In this rather simple example, we assume that all categories have one or more taxes associated with them, and every product falls into exactly one category.

Other than these classes, we will also be using a couple of other simple classes like Person and Student to understand some of the other concepts embodied in this library.

We'll next understand each of the three use cases enumerated in the introduction.

Section 1. Using the Library

Autosave and Auto-Update

We will use a simple data transfer object-type class representing a real-life person to demo this concept. To this end, we create a DTO (data transfer object) class, PersonDto like so:

[Serializable]
public class PersonDto : ObjectDto {
    public PersonDto () : base ("R") {
    }

    [Unique]
    public string FullName { get; set; }
    public DateTime DoB { get; set; }

    public override string ToString () {
        string ftr = "Obj: `{0}` / Version#: `{1}` / WhenAdded: `{2}` / Full name: `{3}` / DoB: `{4}`";
        return string.Format (ftr, WhoAmI (), VersionIndex, WhenAdded, FullName, DoB);
    }

    public override ObjectDto OnPrincipalObjectUpdated (ObjectDto updatedPrincipalObj, string optionalArg) {
        // Do whatever you need to do here to manage this change.
        // Do NOT attempt to save this object, else it will result in undefined behaviour; just manage
        // the change in the object and that's it. The library will ensure that the object is auto-saved.
        // Avoid doing any disk/network io here.

        return this;
    }
}

All business data objects that would like to avail of the library need to inherit from the abstract ObjectDto class, and send across a string to it via the constructor; the string thus sent will be prepended to all the UUIDs (Universally Unique IDs) generated within the ObjectDto class. The "universal" in UUID is not really universal though; I deliberately avoided using GUIDs since they are too large and unwieldy for my liking. You can have a look at the StringUtils class that contains an explanation of how I generate 12-char unique UUIDs.

Don't bother too much about the OnPrincipalObjectUpdated method for now. We will talk more about it in a latter section on event propagation. However, it has to be borne in mind that every single Dto class that your business case dictates needs to implement this abstract method from the ObjectDto class. OnPrincipalObjectUpdated declares two parameters, the first being the principal object that is responsible for this method getting called, and the other being any optional string that may have been specified by the client pertaining to that particular dependency. However we're getting ahead of ourselves; more on this later.

Every business data transfer and processing class (I will simply refer to them as DTOs henceforth) also needs to declare a unique property, the value for which will not be duplicated across any object of that class. This property can (currently) be a string, an int, or just about any other primitive type. In the case of the PersonDto class above, we have declared FullName to be the unique property for that class.

Creating Object Versions

Very simply, in our main program, we can instantiate any ObjectDto subclass like so:

PersonDto person = new PersonDto ();

Or even:

PersonDto person = new PersonDto () {
    FullName = "Abraham Lincoln",
    DoB = new DateTime (1809, 2, 12),
};

The object is not "ready" yet however, and you can make it ready like so:

person.SetObjectAsReady ();

Or alternatively you can even add a comment like so:

person.SetObjectAsReady ("First commit for Mr. Lincoln!");

Or to sum it all up in a single expression:

PersonDto person = new PersonDto () {
    FullName = "Abraham Lincoln",
    DoB = new DateTime (1809, 2, 12),
}.SetObjectAsReady ("First commit for Mr. Lincoln!") as PersonDto;

The method SetObjectAsReady returns an ObjectDto, hence the need for a cast ("as PersonDto").

That's it: the library will save that as the first version of this object. As mentioned earlier, we try to be as declarative as possible: when the object is flagged as ready, it is saved as its very first version. As such, there is no need to explicitly call any method like, for instance, Save() as one would presume.

Thenceforth, every single change made to the object will result in a new version of that object getting persisted. For instance, a single change like

person.FullName = "Thomas Jefferson";

will immediately result in the object getting saved as the current object's subsequent version with the FullName property taking the value as specified in the main program.

You may also add a comment before making the change like so:

person.VersionComment = "Changing one president's details with another's.";
person.FullName = "Thomas Jefferson";

And the modification will be saved as the next object version along with the comment as specified.

Of course, there is also the case of a single modification to any object resulting in a state of inconsistency; in our case, for example, the new object version will have saved one president's name with another's birth date, resulting in an inconsistent object state. For this, you can do an atomic modification like so:

string comment = "Changing one president's details with another's.";
bool result = person.ModifyAtomic<PersonDto> (() => {
    person.FullName = "Thomas Jefferson";
    person.DoB = new DateTime (1743, 4, 13);
    return true;
}, comment);

Or if you have explicitly defined a function for that:

string comment = "Changing one president's details with another's.";
bool result = person.ModifyAtomic<PersonDto> (ModifyAtomically, comment);

private bool ModifyAtomically (PersonDto person) {
    person.FullName = "Thomas Jefferson";
    person.DoB = new DateTime (1743, 4, 13);

    return true;
}

This will result in an atomic commit, that is, the new version of the object will have all the details as specified in the atomic lambda method employed for this.

Object Version Retrieval

You can later retrieve:

  1. all versions of the object;
  2. the head version;
  3. the N-eth version

To this end, you will need to use the static OdCepManager class -- which stands for the very imaginatively named Object Dependency & Cascading Events Propagation Manager -- like so. (It should be noted that version indexes for all objects are zero-based.)

// Get 1-eth version:
PersonDto p1 = (PersonDto) OdCepManager.Versioning.GetNEthVersion (typeof (PersonDto), person.Uuid, 1, out string comment);

Or if you are not interested in the comment:

PersonDto p1 = (PersonDto) OdCepManager.Versioning.GetNEthVersion (typeof (PersonDto), person.Uuid, 1);

Bear in mind that every single object that you create will bear a UUID string that will be "universally" unique (or at least "universal" insofar as the library is concerned), and that becomes the object handle using which you will be able to extricate it from the store.


Sidebar: I realize that the code looks a bit unwieldy currently with casts getting thrown all over the place. I am exploring the possibility of developing a generic version of the library. That however, will be a radical change from where it now stands, and will naturally require a significant time commitment from me, which unfortunately is currently in short supply.


You can also get the head version of the object like so:

// The last arg in the method is optional: you may skip it if you are not interested in the comment
PersonDto personHead = (PersonDto) OdCepManager.Versioning.GetHeadVersion (typeof (PersonDto), person.Uuid, out string comment);

Or even every single version of it, like so:

List<KeyValuePair<ObjectDto, string>> versions = OdCepManager.Versioning.GetAllVersions (typeof (PersonDto), person.Uuid);

You will need to iterate through each key/value (dto object/comment) pair to examine each of the versioned objects.

Indexing

We can also have unique indexes of all objects that we create. Thus, we can have multiple objects like so:

PersonDto p1 = new PersonDto () {
    FullName = "Mike",
    DoB = new DateTime.Now,
}.SetObjectAsReady () as PersonDto;

Sidebar: It is not a requirement to specify a comment each time you save an object, or should I say when the object is implicitly saved, as in the code samples listed here. In the event that you don't pass along a comment of your own, the library will add its own comments from its internal list of comment templates. More on this later when we talk of object dependencies and cascading event notifications.


You can subsequently add another PersonDto object as shown below.

PersonDto p2 = new PersonDto () {
    FullName = "Dave",
    DoB = new DateTime.Now,
}.SetObjectAsReady () as PersonDto;

PersonDto p3 = new PersonDto () {
    FullName = "Maria",
    DoB = new DateTime.Now,
}.SetObjectAsReady () as PersonDto;

// And so on.

It should be kept in mind that our DTO's definition explicitly forbids us from having duplicate FullName values across more than one object. (See the Unique attribute in the PersonDto class definition.) Therefore, the following code

PersonDto p4 = new PersonDto () {
    FullName = "Mike",
    DoB = new DateTime.Now,
}.SetObjectAsReady () as PersonDto;

will result in a runtime exception like so:

System.ArgumentException: Duplicate value for unique property PersonDto.FullName: Mike

You may also not change the value later to another value which collides with another object with the same value as its unique attribute. Thus,

p3.FullName = "Mike"; // same value as that of p1.FullName

will also result in a similar exception.

Of course, if you have modified object p1 like so:

p1.FullName = "Grace";

you are free to rename person p3.FullName to p1's previous value like so:

p3.FullName = "Mike"; // No worries now

Please note that every ObjectDto class must have exactly one, and only one, unique property. In case multiple properties have the Unique attribute set on them, it may result in undefined behaviour.

Unique Index-Based Retrieval

You can also retrieve an object by querying for a specific value on its unique property like so.

// This line should return the UUID of p2:
string retrUuid = OdCepManager.Indexing.GetUuidForUniqueValue (typeof (PersonDto), "Dave");

// Pull out the entire object using its UUID:
if (retrUuid != null) {
    PersonDto retrObj = (PersonDto) OdCepManager.Versioning.GetHeadVersion (typeof (PersonDto), retrUuid);
}

Object Dependencies

You can also specify object dependencies so that any modification made to an object should immediately result in all objects dependent upon its state, getting informed of that change. That is, any change made an object ("principal") should cascade across the entire hierarchy of objects directly or indirectly affected by that change ("dependents"). Harking back to our ecommerce example from the introduction, let's take the example of an ecosystem of classes as analogs of real world concepts like taxes, categories, and products.

Event dependencies for these classes would typically be as embodied in the dependency depicted below.

Tax => Category => Product

That is, any modification made to a Tax object should percolate down to all Category objects which are directly affected by that change, and these changes themselves should further flow down to the Product objects for those categories, and so on all the way down the entire dependency tree.

To this end, we can say that a Tax object is the principal of one or more Category objects, or stated vice versa, Category objects are its dependents. Going further, Product objects are dependents of Category objects. And so on all the way down.

All three classes are listed below.

[Serializable]
public class TaxDto : ObjectDto {
    public TaxDto () : base ("T") {
    }

    [Unique]
    public string TaxTitle { get; set; }
    public decimal Rate { get; set; }

    public override string ToString () {
        string ftr = "Obj: {0} / Version#: {1} / WhenAdded: {2} / Title: {3} / Rate: {4}";
        return string.Format (ftr, WhoAmI (), VersionIndex, WhenAdded, TaxTitle, Rate);
    }

    public override ObjectDto OnPrincipalObjectUpdated (ObjectDto updatedPrincipalObj, string optionalArg) {
        // Do whatever you need to do here to manage this change.
        // Do NOT attempt to save this object, else it will result in undefined behaviour; just manage
        // the change in the object and that's it. The library will ensure that the object is auto-saved.
        // Do not do any disk/network io here.

        return this;
    }
}

[Serializable]
public class CategoryDto : ObjectDto {
    public CategoryDto () : base ("C") {
    }

    [Unique]
    public string CategoryTitle { get; set; }
    public string CategoryDesc { get; set; }
    public decimal TaxRate { get; set; }

    public override string ToString () {
        string ftr = "Obj: {0} / Version#: {1} / WhenAdded: {2} / Title: {3} / Desc: {4} / Tax rate: {5}";
        return string.Format (ftr, WhoAmI (), VersionIndex, WhenAdded, CategoryTitle, CategoryDesc, TaxRate);
    }

    public override ObjectDto OnPrincipalObjectUpdated (ObjectDto updatedPrincipalObj, string optionalArg) {
        // Do whatever you need to do here to manage this change.
        // Do NOT attempt to save this object, else it will result in undefined behaviour; just manage
        // the change in the object and that's it. The library will ensure that the object is auto-saved.
        // Do not do any disk/network io here.

        if (updatedPrincipalObj is TaxDto) {
            TaxDto taxDto = updatedPrincipalObj as TaxDto;
            TaxRate = taxDto.Rate;
        }

        return this;
    }
}

[Serializable]
public class ProductDto : ObjectDto {
    public ProductDto () : base ("P") {
    }

    [Unique]
    public string ProductTitle { get; set; }
    public decimal BaseCost { get; set; }
    public decimal TaxComponent { get; set; }

    public override string ToString () {
        string ftr = "Obj: {0} / Version#: {1} / WhenAdded: {2} / Title: {3} / Base cost: {4} / Tax: {5}";
        return string.Format (ftr, WhoAmI (), VersionIndex, WhenAdded, ProductTitle, BaseCost, TaxComponent);
    }

    public override ObjectDto OnPrincipalObjectUpdated (ObjectDto updatedPrincipalObj, string optionalArg) {
        // Do whatever you need to do here to manage this change.
        // Do NOT attempt to save this object, else it will result in undefined behaviour; just manage
        // the change in the object and that's it. The library will ensure that the object is auto-saved.
        // Do not do any disk/network io here.

        if (updatedPrincipalObj is CategoryDto) {
            CategoryDto categoryDto = updatedPrincipalObj as CategoryDto;
            TaxComponent = BaseCost * categoryDto.TaxRate / 100;
        }

        return this;
    }
}

Once we have created all three classes, we can start creating their objects, and also declaring their dependencies like so:

// First the tax objects:
TaxDto t1 = new TaxDto () {
    TaxTitle = "GST for Red Widgets",
    Rate = 10,
}.SetObjectAsReady () as TaxDto;

TaxDto t2 = new TaxDto () {
    TaxTitle = "GST for Blue Widgets",
    Rate = 12,
}.SetObjectAsReady () as TaxDto;

TaxDto t3 = new TaxDto () {
    TaxTitle = "GST for All Color Widgets",
    Rate = 5,
}.SetObjectAsReady () as TaxDto;

// And then the category objects:
CategoryDto c1 = new CategoryDto () {
    CategoryTitle = "Red Widgets",
    CategoryDesc = "Red widgets only",
    Rate = 0, // Don't bother, this will auto-update once the dependencies are in place
}.SetObjectAsReady () as CategoryDto;

CategoryDto c2 = new CategoryDto () {
    CategoryTitle = "Blue Widgets",
    CategoryDesc = "Blue widgets only",
    Rate = 0, // Don't bother, this will auto-update once the dependencies are in place
}.SetObjectAsReady () as CategoryDto;

// And finally the product objects:

ProductDto p1 = new ProductDto () {
    ProductTitle = "Shiny Red Widget",
    BaseCost = 100,
    TaxComponent = 0, // Don't bother, this will auto-update once the dependencies are in place
}.SetObjectAsReady () as ProductDto;

ProductDto p2 = new ProductDto () {
    ProductTitle = "Shiny Blue Widget",
    BaseCost = 120,
    TaxComponent = 0, // Don't bother, this will auto-update once the dependencies are in place
}.SetObjectAsReady () as ProductDto;

// Declare the dependencies:
// Assuming there could be multiple taxes for each category:
c1.AddPrincipalDependency (t1);
c1.AddPrincipalDependency (t3);

c2.AddPrincipalDependency (t2);
c2.AddPrincipalDependency (t3);

p1.AddPrincipalDependency (c1);

p2.AddPrincipalDependency (c2);

Once you have declared the dependencies, all dependent objects will auto-update as newer versions in a cascading manner. Thus, at the end of all the dependency declarations, the following objects will have been auto-updated: categories c1 and c2, and products p1 and p2. (Merely declaring a dependency results in an immediate auto-update for that object and all its dependents, and all their dependents, and so on. Thus it makes sense to add dependencies in the same order in which it should logically flow down to avoid extra events and unnecessary object versions.)

And how should a dependent object update itself?

As mentioned earlier, when your DTO class inherits from the ObjectDto class, it needs to implement the abstract ObjectDto.OnPrincipalObjectUpdated method. The dependent object handles its principals' change events in this handler like so:

// This method is from the ProductDto class:
public ObjectDto OnPrincipalObjectUpdated (ObjectDto updatedPrincipalObj, string optionalArg) {
    // Do a downcast after checking object type:
    if (updatedPrincipalObj is CategoryDto) {
        CategoryDto categoryDto = updatedPrincipalObj as CategoryDto;
        TaxComponent = BaseCost * categoryDto.TaxRate / 100;
    }

    return this;
}

The updated principal object is passed along to this notification event, as well as an optional string which may be used for any particular information that may need to be associated with this update action. This optional string is specified while declaring the dependency.

Please note that even if multiple principal objects are modified together owing to changes in their own principal object, either directly or indirectly, each modification will arrive as a separate event to the dependent object.

Post modification, the updated dependent object needs to return its own reference ("return this") on completion of the OnPrincipalObjectUpdated method, and this object is then saved as its newest (head) version.

Caveat: Naturally, it would be expected by the client that the object in the running program be internally updated with the latest head version on auto-modification. Unfortunately, I have still not implemented an in-place update for dependent objects, so you will need to explicitly get the head version from the library as demonstrated in the code below. Admittedly, to some extent this does break the declarative nature of the library.

c1 = (CategoryDto) OdCepManager.Versioning.GetHeadVersion (typeof (CategoryDto), c1.Uuid);
c2 = (CategoryDto) OdCepManager.Versioning.GetHeadVersion (typeof (CategoryDto), c2.Uuid);

p1 = (ProductDto) OdCepManager.Versioning.GetHeadVersion (typeof (ProductDto), p1.Uuid);
p2 = (ProductDto) OdCepManager.Versioning.GetHeadVersion (typeof (ProductDto), p2.Uuid);

Auto-Update Comments

Naturally, you will not be able to specify comments on auto-updations since they happen internally. During such updates, the library will save useful information about the auto-modification, and this includes the principal object's type, as well as its UUID owing to which the change event was triggered. This can later be examined by the client since these auto-updated changes are saved as object versions along with the auto-generated comments.

Library & Dependencies

I have been designing systems for a while now, and as far as possible my endeavor is to hide the complexities of the system from the calling code. This adds to the overall declarative quality of the code (as it were) since the library or system "just works," as indeed it should, without the client having to explicitly specify as much. Along the same lines, I try as far as possible to avoid spillovers of any kind of artifacts from the library into the client code. That said, there are inevitable situations where pragmatism dictates otherwise. ObjectStore is no exception to this as well.

I have used the Fody IL interweaving library to implement the library's declarative features, and the client code will also need to have a reference to that if it wants these features to work for its own DTO classes. And of course, once you add the library, it makes a small demand from you, like adding an xml file to the project, else it will refuse to compile. Not such a harsh inconvenience after all, though, and I guess most of us can live with it. The FodyWeavers.xml file should be copied as is from the test project to your own project's root folder. Make sure that you also add this xml file via Visual Studio to your project.

Section 2. The Library

Persistence Engines

The library consists of a core part that makes use of an interface-based persistence system. The interface (IPersister class in the PersistenceBase project) declares all the methods that are required to be implemented by any persistence provider. I have only implemented the interface for MySQL, which is the persistence store in the code as it is currently checked in. I plan to separate the persistence logic from the library completely, so that the library user can just drop an implementing DLL, which implements the interface of course, and the library will then dynamically resolve the new persistence engine without having to be re-compiled. I will be adding this soon.

It is left to the library user to decide on their choice of persistence engine, be it MySQL, MSSQL, Sqlite, or even a NoSQL database like Cassandra or MongoDB. As long as the driver implements the interface properly, the specific engine can be used in a db-agnostic way by the library.

The interface makes no particularly arcane demands from the implementing library. All the "specialization" required by ObjectStore library like type info, as well as the specific structures employed by the core library are hidden from the persistence engine, and it only has to deal with primitive types or at most well-known constructs from the collections library (like KeyValuePair<>, for instance) that come as part of the framework.

Configuration

The details required by the persistence system are injected into it using a callback, which I was forced to employ since there is considerable difficulty involved in libraries being able to have their own config files. Thus, the library will call the client via the callback and get specific configuration details from it as and when required.

The test project (ObjectStore.Tests) has a base class from which all tests suites inherit, and this base class has a TestFixtureSetUp method that is called before the test suite starts running the tests. In this method we invoke the ConfigRegistrar.RegisterConfigMethod method and pass along our callback, which is then used by the library to get config details from our client program as and when required by it. In the test suite this callback has been more or less appropriately named AppConfigSimulator, since it takes the place of an actual config file reader method; that is, the string values that it returns are expected to come from a config file. The callback takes a string ("key") as its argument and returns the config value sought by the library. You would need to run through the switch-case statement and replace the various values returned to those that are specific to your development environment before you run the test suite.

Automation Mechanics

I have employed reflection as well as concepts like IL interweaving extensively throughout the library.

Caution

A few things to keep in mind:

  1. One final word: let me reiterate that there are still way too many bugs in the code which I have yet to identify. Other than these hidden bugs which I am not aware of as I write this, you should also bear in mind that I have checked in this code even though the test suite is far from complete.

  2. The code as it currently stands is not terribly efficient: I code as I think, and it is often that after I have implemented a feature, I realize it may not be so feasible after all, and I need to backtrack. Thusly, the code will not win many prizes for being clean and readable. Code review and cleaning is a chore that needs to be done, and I hope to be able to do this as and when I get around to doing it.

  3. ObjectStore uses the K&R formatting style, which is "non-standard" in the .Net world. This is a throwback from my C++ days when I would code in the vi editor and conserving every possible line was of immense value. I just cannot shake it off. So in case my coding style gives you the jitters, I would recommend that you change the code format to a style more consistent with, well, your style.

Roadmap

  1. I will later separate the concrete persistence engine modules into separate projects so that a concretely implemented persistence DLL can be "dropped" into an engines/ folder and the ObjectStore library will then dynamically load that engine from there, so that there is no need to re-compile the ObjectStore library each time a new persistence engine is added.

  2. I am considering how to make this library work with generics so that we can rid the client code of all the casting that I have had to do. Though this will be done eventually and not immediately.

Licenses, Credits, Attributions

ObjectStore has been released under the Apache license.

The following 3rd-party libraries have been used in ObjectStore:

  1. Newtonsoft Json, which is released under the MIT license. You can read Newtonsoft's license here.
  2. Fody, which is also released under the MIT license. You can read Fody's license here.
  3. I have used NUnit for the test project, though I am a bit confused about their license. You can read their license(s) here, here, and here. However, all their licenses seem to be pretty permissive, so no worries there. (Though I should add that I am not a lawyer, so you will need to read it yourself and figure it out.)

While you are free to use the any of these libraries as you see fit, it should be noted that -- in the interests of legalities -- you are obliged to include all 3rd-party licenses at the link above each time you use any of these libraries or create a derivative product, along with ObjectStore library's license as well, of course.