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