Skip to content
Najeeb Shaikh edited this page Aug 4, 2018 · 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 principal object down its entire hierarchy of dependents, each as a separate event to each 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 this better, for instance, we will take the example of a simple class ecosystem in an ecommerce scenario like as listed below.

  1. Tax
  2. Category
  3. Product

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.

The Tax class encapsulates every thing to do with taxation; the Category class represents product categories; while the Product class is the 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.

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


Sidebar: Please note that this library is still alpha. Which essentially means that there are still a whole load of bugs lurking around in there, so use it with extreme caution. Unfortunately, I have only so many spare hours and weekends available and it took me around 10 days to get it to alpha level.


Section 1. Using the Library

Autosave and Auto-Update

We will use a simple Person data transfer object-type class to demo this concept. To this end, we create a Person DTO class 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.

Every single Dto class that your business case dictates needs to implement the abstract method, OnPrincipalObjectUpdated from the ObjectDto class, which 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.

Every business data transfer and processing class (I will simply refer to them as _DTO_s henceforth) also needs to define a unique property, the value of which will not be repeated across any other 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 ObjectSto 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, thus the need for the cast ("as PersonDto").

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

Thenceforth, every single change that you make to the object will result in a new version of that object getting saved. Thus, 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.

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 instance, the new object version will have saved one president's name with another's birth date. 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 defined a function for that:

` string comment = "Changing one president's details with another's."; bool result = person.ModifyAtomic (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 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 your handle using which you will be able to extricate that object from the library.


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 dramatic change from where it now stands, and will naturally require significant time commitment from me, which unfortunately is currently in short supply.


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

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 getting saved, as in the code immediately above. 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 then add another PersonDto object like so:

` 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. `

However, 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 complain during runtime, like so:

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

You may 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 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 Mike like so:

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

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

Unique Index-Based Retrieval

You can also retrieve an object by querying for a specific, unique 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: 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 about 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 like taxes, categories, and products.

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

Tax => Category => Product

Any modification made to a Tax object should percolate down to all Category class objects which are directly affected by that change, and these changes themselves should further flow down to the Product class 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. Furthermore, Product objects are dependents of Category objects.

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 declaring their objects 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 products:

PersonDto 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;

PersonDto 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 above, the following objects will have auto-updated: categories c1 and c2, and products p1 and p2. (Just declaring a dependency results in an immediate auto-update for that object and all its dependents.)

And how should the dependent object update itself?

Very simply, the moment your DTO class inherits from the ObjectDto class, it needs to override the OnPrincipalObjectUpdated method. The dependent object handles its principals' change events in this handler like so:

` 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 dependent object is passed along to this notification event, as well as an optional string which can be used for any particular information that you may have wanted to associate with this updation 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 on completion of the OnPrincipalObjectUpdated method, and this object is then saved as its newest (head) version.

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

` 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. During such updates, the library will save useful information about the principal object's type owing to which the change was triggered, as well as its UUID. 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 achieve some of the 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.

Section 2. The Library

Persistence Engines

The library consists of a core part that makes use of an interface-based persistence system. The interface 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.

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 an db-agnostic way by the library.

The interface makes no particularly arcane demands from the implementing library. All the "specialization" 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.

Automation

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

Caution

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.

Yet another thing to bear in mind is that 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.

Yet another thing is that my code uses the K&R formatting style, which is a very "non-standard" format 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 styles gives you the jitters, I would recommend that you change the code format to a style more consistent with, well, your style.

Yet another thing to note is that I figuring out 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.

Credits & Attributions

I have used Fody in ObjectStore, which is released under the MIT license. You can read it here: Fody License.txt

While you are free to use the Fody library as you see fit, it should be noted that -- in the interests of legalities -- you are obliged to include Fody's license at the link above each time you use this library or create a derivative product, along with this library's license as well, of course.

Clone this wiki locally