Wednesday, January 12, 2011

FluentMigrator: Extending the Framework

So after the basics and after you understood the framework lets look at extending it a bit.

We shall begin with something simple: creating ArcSde domains (not assigning the domain)

Before we run to the code – what do we want to achieve?

Create.Domain(“DomainName”).OfType(TypeEnum.SomeType)

.WithKey(ObjectKey).WithValue(StringValue)

…WithKey(ObjectKey).WithValue(StringValue);

That was what I thought should be the raw basic design.

How can we improve it? Well I don’t like using objects so:

Create.Domain<DomainType>(“DomainName”)

.WithKey(DomainTypeKey).WithValue(StringValue)

…WithKey(DomainTypeKey).WithValue(StringValue);

Isn’t that better? It will need more work but even the newest programmer won’t abuse it and cause you those white hairs…

Now let think about <DomainType> what can it be? Domain could be of the types:

  1. public enum esriFieldType
  2. {
  3.     esriFieldTypeSmallInteger,
  4.     esriFieldTypeInteger,
  5.     esriFieldTypeSingle,
  6.     esriFieldTypeDouble,
  7.     esriFieldTypeString,
  8.     esriFieldTypeDate,
  9.     esriFieldTypeOID,
  10.     esriFieldTypeGeometry,
  11.     esriFieldTypeBlob,
  12.     esriFieldTypeRaster,
  13.     esriFieldTypeGUID,
  14.     esriFieldTypeGlobalID,
  15.     esriFieldTypeXML,
  16. }

<DomainType> could be struct but struct doesn’t include string…

I actually don’t mind limiting the users a bit and giving them the options of only short (SmallInteger), int, float, double and string. All the other options are too specific for a Domain. And string is still a problem! For this example we will stick with struct…

From that we can do the design of CreateDomainExpression:

  1. public class CreateDomainExpression<T> : SdeMigrationExpressionBase
  2.     where T:struct
  3. {
  4.     public string DomainName { get; set; }
  5.     private readonly List<DomainValue<T>> _domainValues = new List<DomainValue<T>>();
  6.  
  7.  
  8.     public List<DomainValue<T>> DomainValues
  9.     {
  10.         get { return _domainValues; }
  11.     }

Where DomainValue is defined as (KeyValuePair is not changeable after construction):

  1. public class DomainValue<T>
  2. {
  3.     public T DomainKey { get; set; }
  4.     public string Value { get; set; }
  5. }

Now since we don’t want to change too much of the FluentMigrator framework we will use SdeCreate instead of Create, and use SdeMigration abstract class instead of Migration.

The only change in FM framework you need to do is change IMigrationContext to protected in Migration:

  1. namespace FluentMigrator
  2. {
  3.     public abstract class Migration : IMigration
  4.     {
  5.         protected IMigrationContext _context;

The reason for that is that our expressions need to be in the MigrationContext when the Runner executes them (part 5 of the previous post).

  1. public abstract class SdeMigration : Migration
  2. {
  3.     public ISdeCreateExpressionRoot SdeCreate
  4.     {
  5.         get { return new SdeCreateExpressionRoot(_context); }
  6.     }

  1. public interface ISdeCreateExpressionRoot : IFluentSyntax
  2. {
  3.     ICreateDomainSyntax Domain<T>(string domainName)
  4.         where T:struct ;


  1. public interface ICreateDomainSyntax<T>
  2.     where T: struct
  3. {
  4.     ICreateDomainValueSyntax<T> WithKey(T key);
  5. }

  1. public interface ICreateDomainValueSyntax<T>
  2.     where T:struct
  3. {
  4.     ICreateDomainSyntax<T> WithValue(string domainValue);
  5. }

The end result for the look and feel of the Builder:

  1. [Migration(002)]
  2. public class Migration_002 : SdeMigration
  3. {
  4.     public override void Up()
  5.     {
  6.         SdeCreate.Domain<short>("DomainName")
  7.             .WithKey(1).WithValue("1")
  8.             .WithKey(2).WithValue("2");

Looks like the wanted fluent code of FM.

 

Now lets implement some code:

  1. public class CreateDomainExpression<T> : SdeMigrationExpressionBase
  2.     where T:struct
  3. {
  4.     private esriFieldType ExtractType()
  5.     {
  6.         var defaultT = default(T);
  7.         if (defaultT is short)
  8.             return esriFieldType.esriFieldTypeSmallInteger;
  9.         if (defaultT is float)
  10.             return esriFieldType.esriFieldTypeSingle;
  11.         if (defaultT is double)
  12.             return esriFieldType.esriFieldTypeDouble;
  13.         if (defaultT is int)
  14.             return esriFieldType.esriFieldTypeInteger;
  15.         if (defaultT is string)//TODO: will never be string
  16.             return esriFieldType.esriFieldTypeString;
  17.  
  18.         throw new NotImplementedException("The only types implemented are short,float,double,int and string.");
  19.     }
  20.  
  21.     private List<KeyValuePair<object , string>> ExtractDomainValues()
  22.     {
  23.         return DomainValues.Select(domainValue => new KeyValuePair<object, string>(domainValue.DomainKey, domainValue.Value))
  24.             .ToList();
  25.     }
  26.  
  27.     #region Implementation of ICanBeValidated
  28.  
  29.     public override void CollectValidationErrors(ICollection<string> errors)
  30.     {
  31.         if (String.IsNullOrEmpty(DomainName))
  32.             errors.Add("The domain's name cannot be null or an empty string");
  33.     }
  34.  
  35.     #endregion
  36.  
  37.     #region Implementation of IMigrationExpression
  38.  
  39.     public override void ExecuteWith(DeploymentWorkspaceUtils utils)
  40.     {
  41.         utils.CreateDomain(DomainName, ExtractType(), ExtractDomainValues());
  42.     }
  43.  
  44.     public override IMigrationExpression Reverse()
  45.     {
  46.         return new DeleteDomainExpression
  47.         {
  48.             DomainName = DomainName
  49.         };
  50.     }
  51.  
  52.     #endregion

Where SdeMigrationExpressionBase is an implementation of MigrationExpressionBase that parses the ConnectionString from processor to a SDE connection string and creates a DeploymentWorkspaceUtils from it:

  1. public abstract class SdeMigrationExpressionBase : MigrationExpressionBase
  2. {
  3.     public abstract void ExecuteWith(DeploymentWorkspaceUtils utils);
  4.  
  5.     public override void ExecuteWith(IMigrationProcessor processor)
  6.     {
  7.         var utils = DeploymentWorkspaceProvider.Instance.GetWorkspace((SqlServerProcessor)processor);
  8.         ExecuteWith(utils);
  9.     }
  10. }

  1. public class DeploymentWorkspaceProvider
  2. {
  3.     #region Singleton
  4.  
  5.     private static readonly DeploymentWorkspaceProvider instance = new DeploymentWorkspaceProvider();
  6.  
  7.     // Explicit static constructor to tell C# compiler
  8.     // not to mark type as beforefieldinit
  9.  
  10.     private DeploymentWorkspaceProvider()
  11.     {
  12.     }
  13.  
  14.     public static DeploymentWorkspaceProvider Instance
  15.     {
  16.         get { return instance; }
  17.     }
  18.  
  19.     #endregion
  20.     
  21.     public DeploymentWorkspaceUtils GetWorkspace(SqlServerProcessor processor)
  22.     {
  23.         string connectionString = processor.Connection.ConnectionString;
  24.         return
  25.             WorkspaceProvider.Instance.GetDeploymentWorkspace(
  26.                 ConnectionStringUtils.GetSdeConnectionString(connectionString));
  27.     }
  28. }

The basic parser

 

//TODO: Continue this!

 

 

//TODO: replace FM with FluentMigrator

//TODO: replace the links at the top to the real DllShepherd.net blog site

Resources:

Github: FluentMigrator Project (the source code)

Keywords: FluentMigrator , framework, expression, nutshell, design, Fluent, Domain, C#, ArcSde, ArcObjects

FluentMigrator: Understanding the Framework Code

On the last post of this session: FluentMigrator: Introduction we saw how to use the FluentMigrator framework. Today I will dip into the code so you might understand how it was created.

Part 1: Expression Builders

Well remember this:

FluentMigrator-framework-create-tableFluentMigrator-framework-create-table-in-schema

Will you believe that we are looking at the IntelliScense of the same class?

The only difference is in the interface being returned by the methods, the class that implements them is the same class.

When you call InSchema(“GIS”) you set a field inside the class with the schema name and return the current class back. And that is the whole Fluent way of writing the migration in a nutshell.

You create expressions of the work you want the FluentMigrator to do and then the Runner go over those expressions and runs them.

Part 2: Expression Data (implements IMigrationExpression)

Expression Data is the data being set by the user it also contains the code to execute the changes in the DB. For example when creating a table and the table name and the table schema are stored inside an Expression Data of type CreateTableExpression:

  1. public ICreateTableWithColumnSyntax InSchema(string schemaName)
  2. {
  3.     Expression.SchemaName = schemaName;
  4.     return this;
  5. }

Part of the Expression Data definition:

  1. public class CreateTableExpression : MigrationExpressionBase
  2. {
  3.     public virtual string SchemaName { get; set; }
  4.     public virtual string TableName { get; set; }
  5.     public virtual IList<ColumnDefinition> Columns { get; set; }

Executing the data:

  1. public override void ExecuteWith(IMigrationProcessor processor)
  2. {
  3.     processor.Process(this);
  4. }

The processor has a generator that generates SQL according to the used DB and then it executes the SQL.

Part 3: IMigrationContext

Every expression builder uses IMigrationContext to store the expression data. The IMigrationContext has a collection of IMigrationExpression:

  1. public interface IMigrationContext
  2. {
  3.     IMigrationConventions Conventions { get; }
  4.     ICollection<IMigrationExpression> Expressions { get; set; }
  5.     IQuerySchema QuerySchema { get; }
  6. }

The MigrationRunner will execute that collection using a Processor.

Part 4: Migration

The classes that creates the Expression Data using Expression Builders. The examples in the previous post was of implementing this class. Using the properties of Create/Delete/Rename/… actually creates a factory class that will build a new Expression builder class using the IMigrationContext this class has as a private field:

  1. public abstract class Migration : IMigration
  2. {
  3.     private IMigrationContext _context;

  1. public ICreateExpressionRoot Create
  2. {
  3.     get { return new CreateExpressionRoot(_context); }
  4. }

CreateExpressionRoot factory class for the Expression builders:

  1. public class CreateExpressionRoot : ICreateExpressionRoot
  2. {
  3.     private readonly IMigrationContext _context;
  4.  
  5.     public CreateExpressionRoot(IMigrationContext context)
  6.     {
  7.         _context = context;
  8.     }
  9.  
  10.     public void Schema(string schemaName)
  11.     {
  12.         var expression = new CreateSchemaExpression { SchemaName = schemaName };
  13.         _context.Expressions.Add(expression);
  14.     }
  15.  
  16.     public ICreateTableWithColumnOrSchemaSyntax Table(string tableName)
  17.     {
  18.         var expression = new CreateTableExpression { TableName = tableName };
  19.         _context.Expressions.Add(expression);
  20.         return new CreateTableExpressionBuilder(expression, _context);
  21.     }

Schema is a unique case where there is only one variable. Table is a more usual case, CreateTableExpression is the Expression Data class (it is inserted into the IMigrationContext field) and an Expression Builder is returned.

Part 5: MigrationRunner

This is the class that actually runs the migration. The Up/Down methods create the MigrationContext and pass it to the Migration class which builds the Expression data using the Expression Builders. The MigrationRunner apply the IMigrationConventions on the Expression data and then uses a Processor class to execute the DB changes written in the IMigrationContext object

Part 6: IMigrationConventions

This class is used to create missing fields not set in the Expression data. For example you can choose to create a primary key without specifying the name of the key:

  1. public ICreateColumnOptionSyntax PrimaryKey()
  2. {
  3.     Expression.Column.IsPrimaryKey = true;
  4.     return this;
  5. }
  6.  
  7. public ICreateColumnOptionSyntax PrimaryKey(string primaryKeyName)
  8. {
  9.     Expression.Column.IsPrimaryKey = true;
  10.     Expression.Column.PrimaryKeyName = primaryKeyName;
  11.     return this;
  12. }

One of the conventions is:

  1. public static string GetPrimaryKeyName(string tableName)
  2. {
  3.     return "PK_" + tableName;
  4. }

Meaning the primary key will be created with the name PK_[TableName]

Part 7: IMigrationProcessor

This interface has many implementations – one for each DB. For example I use SQL Server 2008 so I use SqlServerProcessor. The most important funtion in it is:

  1. protected override void Process(string sql)

Which opens a connection to the DB (if it’s not opened), creates a command and executes it.

The Processor uses a Generator class which generates the SQL used in the various basic create/rename/delete/… functionalities.

Part 8: IMigrationGenerator

Again there are many implementations of this, this time one for each implementation of a DB. For example there are three implementations for SQL Server – 2000,2005,2008 and they are not duplicates (the rename stored procedure in 2008 was different from previous versions.

//TODO: change the link to the real DllShepherd.net blog site

//TODO: add a basic drawing of all the interfaces involved – this is a bit too much mess!

Resources:

Github: FluentMigrator Project (the source code)

Keywords: FluentMigrator , framework, expression, nutshell