Reputation: 1274
Background: Hello I am trying to build a windows workflow like state engine. I have a basic engine set up with Action
's and Trigger
's - Actions do custom code, triggers are external events that allow the state engine to move from one state to another. Trigger
's hold many Actions
' which fire when a Trigger
's bool isMet()
condition is true.
The coding problem that I am having is I need to abstract the isMet()
method of the Trigger
class. The reason for this is that I have many sub-Trigger
classes e.g. isPaperworkCompletedTrigger
that inherit from the base Trigger
class and they each contain their own custom isMet()
code. The only complication I'm having in implementing this is that the whole engine e.g. Trigger
's and Action
's need to be stored in a database. I built the engine tables in SQL first, then used LINQ-to-SQL to build my Action
and Trigger
objects. LINQ-to-SQL does allow you to expand on auto generated class objects by using the partial
class method which I have used to add a isMet()
method to my Trigger
class, I cannot make this isMet()
method abstract because the auto-generated Trigger
class is not abstract ( for obvious reasons ).
I have tried 'soft overriding' the isMet()
method by inheriting the base Trigger
class in my sub-classes e.g. isPaperworkCompletedTrigger
and creating a method called isMet()
, intellisense complains about this a little bit and tells me to stop intellisense from complaining to use the 'new' keyword on the method. As expected this method of 'soft-overriding' does not work.
When the Trigger
objects are pulled out of the database and the isMet()
method is called naturally the base method isMet()
method is called ( from the Trigger
class, and not the sub-class ), this makes sense as the database has no way of knowing which child of Trigger
to call the isMet()
method on.
The obvious solution to this is to stick a TriggerName
field in the Triggers
table, and do a good old switch case on this field, calling the isMet()
method of the corresponding sub-class of Trigger
based on what the name field is. This is something I want to avoid.
I would like this project to be able to allow users to 'plug-in' Trigger
's and Action
's. The way I plan to accomplish this is to allow user's to drop their own custom Trigger
derived classes as a DLL into a specified folder, and have the workflow engine able to use these without a re-deploy or rebuild ( which rules out the massive switch case statements on static strings ).
The core of this problem is working out how to read in all the Trigger
modules ( one DLL is one Trigger
module ), and call a isMet()
method on this object ( without having access to its class code ).
I suspect that the point-of-attack to solve this lies within making the Trigger
class isMet()
method abstract OR Putting some kind of converter class to convert from the database Trigger
class to an 'offline' Trigger
class and making that offline class abstract ( which I can override from ).
Can anybody help with this problem.
Very sorry for my novel-lengthed question but the issue does require quite a lot of information in order for anybody to understand the question.
Thanks
Upvotes: 4
Views: 390
Reputation: 9533
Instead of making the isMet()
method on the base Trigger
class abstract
, make it virtual
with perhaps the default value being false. Then you can override
the method in the derived classes using the override
keyword.
Your second problem concerns serializing and deserializing the triggers to a database. When you deserialize, you want to make sure you're getting back the derived trigger type, not the base. I don't know how you've chosen to serialize your objects to the databse, but you'll need a way to store the type. Lets take the DataContractSerializer
for example. It takes in a Type
as it's first parameter. If you store the typeof(DerivedTrigger) to another field in your database when you serialize your triggers, you can deserialize the Type and use that to deserialize the Trigger to the correct derived type. Then calling your isMet()
method should call the derived overriden value. Here is a short example using static variables in place of a database:
[DataContract]
partial class Trigger
{
public virtual bool isMet()
{
return false;
}
}
[DataContract]
class DerivedTrigger : Trigger
{
public object DataElement1 { get; set; }
//and other properties to serialize.
public override bool isMet()
{
return true;
}
}
void Main()
{
DerivedTrigger t = new DerivedTrigger();
Serialize(t);
((Trigger)Deserialize()).isMet(); // returns True!
}
public static void Serialize<T>(T source)
{
MemoryStream ms = new MemoryStream();
Type serializedObjectType = typeof(T);
DataContractSerializer dcsObject = new DataContractSerializer(serializedObjectType, null, int.MaxValue, false, true, null);
dcsObject.WriteObject(ms, source); //serialize the object
byte[] buffer = new byte[1024] //TODO: adjust size
ms.Position = 0;
ms.Read(buffer, 0, 1024);
//TODO: write buffer to database colObject here
ms.Position = 0;
DataContractSerializer dcsType = new DataContractSerializer(typeof(Type), null, int.MaxValue, false, true, null);
dcsType.WriteObject(ms, serializedObjectType.DeclaringType);
buffer = new byte[1024]
ms.Position = 0;
ms.Read(buffer, 0, 1024);
//TODO: write buffer to database colType here
}
public static object Deserialize()
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte[1024];
//TODO: read colType into buffer here
ms.Write(buffer, 0 1024);
ms.Position = 0;
DataContractSerializer dcsType = new DataContractSerializer(typeof(Type), null, int.MaxValue, false, true, null);
Type serializedObjectType = dcs.Read(ms);
//TODO: read colObject into buffer here
DataContractSerializer dcs = new DataContractSerializer(serializedObjectType, null, int.MaxValue, false, true, null);
return dcs.ReadObject(serializedObject);
}
EDIT
Ok, using the MemoryStream seems to have confused the situation. The MemoryStream isn't what gets stored in the database, it IS the database.
The whole reason for having the serializedObjectType
is because, like you say, using typeof(Trigger)
for the type in a DataContractSerializer won't deserialize objects that are actually derived triggers. Therefore, you need to store the derived type along with the object in the database.
You haven't said what dbms you're using but I would use a blob to represent the Trigger column, and either a varbinary or a blob to represent the serializedObjectType
column, ie the actual most derived type of the Trigger. Serialize the type with a hardcoded type serializer. ie DataContractSerializer(typeof(Type), ...)
and serialize the object with a DataContractSerializer(typeof(T), ...)
where T is the derived trigger type which you can get with the generic type variable.
When you deserialize, do it in reverse. First deserialize the type using a hardcoded type serializer. ie DataContractSerializer(typeof(Type), ...)
and then deserialize the object with the results of the deserialized type. I've updated my code snippets to hopefully better illustrate my proposed strategy. Sorry for the lateness in my response.
EDIT 2
Typically, when talking about serializing, you serialize only the values in an object, since that is what separates one object from another. You don't need to serialize the method body to the database because that is stored in the assemblies in the file system (the plug-able dlls you mention in your question). The loading of these dlls is a separate step all-together. look into System.Reflection.Assembly.LoadFile()
.
From these two statements in your question:
Trigger's and Action's need to be stored in a database.
and
...user's to drop their own custom Trigger derived classes as a DLL into a specified folder..
I assumed (perhaps incorrectly) that the definition for the class would be stored on the fs and the data that goes into each class's objects would be stored in the database. If your derived isMet()
methods are static (maybe not explicitly, but doesn't have any associated state), then there isn't going to be anything to store in the database. However, it sounds like you're setting it up so that a Trigger
holds a collection of Actions
. In that case, those Actions
are what gets serialized to the database. Just mark them public if your class is Serializable
or else mark the collection directly as Serializable
. The size of the memory stream, then, is going to be proportional to the number of Actions
each trigger holds. Clear as mud?
Upvotes: 2
Reputation: 23064
You're just trying to do inheritance within Linq-to-Sql, right?
Thats not unusual. You need a base class with an IsMet method and then sub-classes that override it with the appropriate logic.
The trick is, getting Linq-to-Sql to instantiate the right sub-class when it fetches the data.
I belive that can be done with this:
How to: Map Inheritance Hierarchies (LINQ to SQL)
http://msdn.microsoft.com/en-us/library/bb399352.aspx
edit: Ok, maybe that won't work because you need to know all the classes in advance to fill in the [InheritanceMapping]
attributes. So what you are asking for is dynamic inheritance within Linq-to-SQL, where the compiler does not know in advance what the sub-classes are going to be. I'm not sure if thats possible.
edit2: To do what you're asking dynamically, I don't think Linq-to-sql-inheritance will cut it. Or partial methods. Perhaps Reflection is your best bet. That is: One main Trigger class with a monster of an IsMet() method that reads a TriggerType string and then looks for Assemblies in a folder, loads the one with the matching name and finds the appropriate class and calls an IsMet method on the class using reflection, then returns the result ... etc ...
edit3: or, use a different ORM instead of linq-to-sql. There's a chance the NHibernate might be able to do dynamic inheritance. E.g. see this question
edit4: in fact, here's someone writing about using NHibernate to do pretty much what you're trying to do:
"Creating a dynamic state machine with C# and NHibernate"
http://blog.lowendahl.net/?p=164
Upvotes: 0