Skip to content

2007

Programmatically creating tables in SQL Server using .NET and SMO

SMO (SQL Server Management Objects) is a .NET based object library for programming all aspects of managing Microsoft SQL Server. Replication Management Objects (RMO) is another library that encapsulates SQL Server replication management.

SMO assemblies are shipped with SQL Server 2005 and can be used to connect with SQL Server 7, 2000 or 2005. The assemblies are located in the following folder C:\Program Files\Microsoft SQL Server\90SDKAssemblies.

  • Microsoft.SqlServer.Smo.dll
  • Microsoft.SqlServer.ConnectionInfo.dll
  • Microsoft.SqlServer.SmoEnum.dll
  • Microsoft.SqlServer.SqlEnum.dll

With SMO you can do all kind of management on a SQL Server, namely: tables, columns, indexes, stored procedures, service broker, backup and restore, managing users/roles and logins, scheduling, etc. Here you can find some specific tasks that can be done with SMO.

Below you find an example how you can create a table with SMO:

using System.Data.SqlClient;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;

public class Sample
{
    public void Create(string connectionstring)
    { 
        SqlConnection connection = new SqlConnection(connectionstring);
        Server server = new Server(new ServerConnection(connection));

        Database database = server.Databases["MyDatabase"];

        // Create table, called Customer
        Table table = new Table(database, "Customer");

        // Add 'ID' column which is the primary key
        Column idColumn = new Column(table, "ID");
        idColumn.DataType = DataType.Int;
        idColumn.Identity = true;
        idColumn.IdentitySeed = 1;
        idColumn.IdentityIncrement = 1;

        // Create a primary key index
        Index index = new Index(table, string.Format("PK_{0}", table.Name));
        index.IndexKeyType = IndexKeyType.DriPrimaryKey;
        index.IndexedColumns.Add(new IndexedColumn(index, "ID"));
        table.Indexes.Add(index);                        

        // Add 'Name' column
        Column nameColumn = new Column(table, "Name");
        nameColumn.DataType = DataType.VarChar(50);

        // Add colums to table
        table.Columns.Add(idColumn);
        table.Columns.Add(nameColumn);

        table.Create();
    }
}

Introduction to MyMeta

MyMeta is an open-source API that allows you to get meta-data from your database. MyMeta is part of MyGeneration, a free code generator hosted on Sourceforge. The MyMeta API can be downloaded separately here (filename is called mymeta_installer.exe).

MyMeta supports the following databases. Note that the API is extensible and that you can provide your own plug-ins

  • Advantage
  • Delimited Text
  • Firebird
  • IBM DB2
  • IBM iSeries (AS400)
  • Interbase
  • Microsoft Access
  • Microsoft SQL CE
  • Microsoft SQL Server
  • MySQL
  • MySQL2
  • Oracle
  • Pervasive
  • PostgreSQL
  • PostgreSQL 8+
  • SQLite
  • VistaDB
  • Xsd3b (xml,xsd,uml,er)

Below you find a code snippet that will iterate, for a SQLite database, all tables, columns and indexes.

string connectionstring = @"data source=SQLiteDatabase.DB";

MyMeta.dbRoot myMeta = new MyMeta.dbRoot();
myMeta.Connect(MyMeta.dbDriver.SQLite, connectionstring);

IDatabase db = myMeta.DefaultDatabase;

foreach (MyMeta.ITable table in db.Tables)
{
    Console.WriteLine("{0} ({1})", table.Name, table.Columns.Count);
    Console.WriteLine("tCOLUMNS");

    foreach (MyMeta.IColumn column in table.Columns)
    {
        Console.WriteLine("tt{0} ({1}), Nullable:{2}",
                 column.Name, column.DataTypeName, column.IsNullable);
    }

    Console.WriteLine("tINDEXES");

    foreach (MyMeta.IIndex index in table.Indexes)
    {
        Console.WriteLine("tt{0}, Unique:{1}", index.Name, index.Unique);
    }
}

MyMeta can map database types to specific ADO.NET data types and language types (C#, VB.NET, etc.). MyMeta has a set of XML files (included in the setup) that contains these mappings. Namely the Languages.xml and DbTargets.xml. Below you find a snippet of the two XML files:

Languages.xml
<Languages>
    ...
    <Language From="SQL" To="C#">
        <Type From="bigint" To="long" />
        <Type From="binary" To="object" />
        <Type From="bit" To="bool" />
        <Type From="char" To="string" />
        <Type From="datetime" To="DateTime" />
        <Type From="decimal" To="decimal" />
        <Type From="float" To="double" />
        <Type From="image" To="byte[]" />
        <Type From="int" To="int" />
        <Type From="money" To="decimal" />
        <Type From="nchar" To="string" />
        <Type From="ntext" To="string" />
        <Type From="numeric" To="decimal" />
        <Type From="nvarchar" To="string" />
        <Type From="real" To="float" />
        <Type From="smalldatetime" To="DateTime" />
        <Type From="smallint" To="short" />
        <Type From="smallmoney" To="decimal" />
        <Type From="text" To="string" />
        <Type From="timestamp" To="byte[]" />
        <Type From="tinyint" To="byte" />
        <Type From="uniqueidentifier" To="Guid" />
        <Type From="varbinary" To="byte[]" />
        <Type From="varchar" To="string" />
        <Type From="xml" To="string" />
        <Type From="sql_variant" To="object" />
    </Language>
    ...
    <Language From="SQLITE" To="C# (SQLite v3.x)">
        <Type From="CHAR" To="string" />
        <Type From="DATETIME" To="DateTime" />
        <Type From="DATE" To="DateTime" />
        <Type From="TIMESTAMP" To="DateTime" />
        <Type From="TIME" To="TimeSpan" />
        <Type From="DECIMAL" To="decimal" />
        <Type From="VARCHAR" To="string" />
        <Type From="NVARCHAR" To="string" />
        <Type From="TEXT" To="string" />
        <Type From="INTEGER" To="long" />
        <Type From="INT" To="long" />
        <Type From="FLOAT" To="float" />
        <Type From="BOOLEAN" To="bool" />
        <Type From="CLOB" To="string" />
        <Type From="BLOB" To="byte[]" />
        <Type From="NUMERIC" To="decimal" />
        <Type From="VARYINGCHARACTER" To="string" />
        <Type From="NATIONALVARYINGCHARACTER" To="string" />
    </Language>
    ...
</Languages>
DBTargets.xml
<DbTargets>
    ...
    <DbTarget From="SQL" To="SqlClient">
        <Type From="bigint" To="SqlDbType.BigInt" />
        <Type From="binary" To="SqlDbType.Binary" />
        <Type From="bit" To="SqlDbType.Bit" />
        <Type From="char" To="SqlDbType.Char" />
        <Type From="datetime" To="SqlDbType.DateTime" />
        <Type From="decimal" To="SqlDbType.Decimal" />
        <Type From="float" To="SqlDbType.Float" />
        <Type From="image" To="SqlDbType.Image" />
        <Type From="int" To="SqlDbType.Int" />
        <Type From="money" To="SqlDbType.Money" />
        <Type From="nchar" To="SqlDbType.NChar" />
        <Type From="ntext" To="SqlDbType.NText" />
        <Type From="numeric" To="SqlDbType.Decimal" />
        <Type From="nvarchar" To="SqlDbType.NVarChar" />
        <Type From="real" To="SqlDbType.Real" />
        <Type From="smalldatetime" To="SqlDbType.SmallDateTime" />
        <Type From="smallint" To="SqlDbType.SmallInt" />
        <Type From="smallmoney" To="SqlDbType.SmallMoney" />
        <Type From="text" To="SqlDbType.Text" />
        <Type From="timestamp" To="SqlDbType.Timestamp" />
        <Type From="tinyint" To="SqlDbType.TinyInt" />
        <Type From="uniqueidentifier" To="SqlDbType.UniqueIdentifier" />
        <Type From="varbinary" To="SqlDbType.VarBinary" />
        <Type From="varchar" To="SqlDbType.VarChar" />
        <Type From="xml" To="SqlDbType.Xml" />
        <Type From="sql_variant" To="SqlDbType.Variant" />
    </DbTarget>
    ...
    <DbTarget From="SQLITE" To="SQLite.NET v3.x">
        <Type From="CHAR" To="DbType.String" />
        <Type From="DATETIME" To="DbType.DateTime" />
        <Type From="DATE" To="DbType.DateTime" />
        <Type From="TIMESTAMP" To="DbType.DateTime" />
        <Type From="TIME" To="DbType.Time" />
        <Type From="DECIMAL" To="DbType.Decimal" />
        <Type From="VARCHAR" To="DbType.String" />
        <Type From="NVARCHAR" To="DbType.String" />
        <Type From="TEXT" To="DbType.String" />
        <Type From="INTEGER" To="DbType.Int64" />
        <Type From="INT" To="DbType.Int32" />
        <Type From="FLOAT" To="DbType.Single" />
        <Type From="BOOLEAN" To="DbType.Boolean" />
        <Type From="CLOB" To="DbType.String" />
        <Type From="BLOB" To="DbType.Binary" />
        <Type From="NUMERIC" To="DbType.Decimal" />
        <Type From="VARYINGCHARACTER" To="DbType.String" />
        <Type From="NATIONALVARYINGCHARACTER" To="DbType.String" />
    </DbTarget>
    ...
</DbTargets>

The xml files can be loaded by setting the LanguageMappingFilename and DbTargetMappingFilename. Setting the right target can be done by the properties Language and DbTarget.

string connectionstring = @"data source=SQLiteDatabase.DB";

MyMeta.dbRoot myMeta = new MyMeta.dbRoot();
myMeta.Connect(MyMeta.dbDriver.SQLite, connectionstring);

myMeta.LanguageMappingFileName = @"C:Program FilesMyGenerationsSettingsLanguages.xml";
myMeta.DbTargetMappingFileName = @"C:Program FilesMyGenerationsSettingsDbTargets.xml";

myMeta.Language = "C# (SQLite v3.x)";
myMeta.DbTarget = "SQLite.NET v3.x";

IDatabase db = myMeta.DefaultDatabase;

foreach (MyMeta.ITable table in db.Tables)
{
    Console.WriteLine("{0} ({1})", table.Name, table.Columns.Count);
    Console.WriteLine("tCOLUMNS");

    foreach (MyMeta.IColumn column in table.Columns)
    {
        Console.WriteLine("tt{0} ({1}), DBTargetType:{2}, LanguageType:{3}",
            column.Name, column.DataTypeName, column.DbTargetType, column.LanguageType);
    }
}

The mapped types can be found in the properties DbTargetType and LanguageType on the IColumn interface.

Visual Studio 2008 Released

Jihaaa, Visual Studio 2008 has been released on MSDN. If you want to have a nice overview of the new features in VS2008, take a look at this post from ScottGu.

Take a look at the following downloads available for VS2008

TechEd 2007

Yesterday I arrived in Barcelona for TechEd 2007. I followed already some interesting sessions about LINQ, it brings you closer how you can access data in a more intuitive and more object-oriented way (data = objects). I really like LINQ to SQL, one of the drawbacks, is that it can only be used against a SQL Server database. If you need to target another database and program against a conceptual model (not a 1-1 mapping with your datatable structure and objects) you can have a look at LINQ to Entities.

Reflection and Generics

On my current project I had the need to iterate through the properties of an object with reflection and to check if one of the properties is a generic List type, e.g. IList, IList, etc. To check through reflection on a generic type, you need to use the GetGenericTypeDefinition method.

foreach (PropertyInfo propertyInfo in entity.GetType().GetProperties())
{
   if (propertyInfo.PropertyType.IsGenericType &&
       typeof(List<>).IsAssignableFrom
              (propertyInfo.PropertyType.GetGenericTypeDefinition()))
   {
      IEnumerable enumerable = propertyInfo.GetValue(entity, null) as IEnumerable;
      IEnumerator enumerator = enumerable.GetEnumerator();

      while (enumerator.MoveNext())
      {
         // do something
      }
   }
}

Linksys WRT54GL

Last week, I purchased a Linksys WRT54GL at RouterShop.nl. The service at Routershop.nl was really fast: at around 1:00pm I created the order and it was delivered the very next day!

The big advantage of WRT54GL is that you can upgrade the firmware of the device. Some popular firmware's are DD-WRT, OpenWRT and Tomato. I am using Tomato, of which today a new version has been released, i.e. Tomato 1.09. Tomato has a very nice interface, many interesting features and great-looking graphs. Below you will find a nice graph concerning the bandwidth:

linksysbandwidthmonitor

Note that you need to install the Adobe SVG Viewer in order to view the graphs.

CeBIT 2007

Last weekend I visited CeBIT 2007 in Hannover. We rented a house for the weekend in Steinhude which is not far from CeBIT and can easily be reached by car and train. If you are looking to stay overnight in Steinhude I would recommend this house, all comfort is available (TV, shower, kitchen, etc.).

Like every year, there were lots of brands and new products to discover. One of the cool things at CeBIT, is that you can gather a lot of gadgets :). An eyecatcher was this modding project for the World Cyber Games 2007 which is a 200 hours project! And I found that server rack of IBM also impressive.

CeBit2007

One of the things that really touched me - a trip down memory lane -, was a working Commodore 64 of the good old days. There was a room that showed some computer history.

CeBit2007_02

If I remember correctly, I have had the following computers with which I grew up, and I have to admit, it was mainly for playing games :-)

And when I came of age, I switched to the traditional personal computer. But I must say that there is really nothing compared to those old skool arcade games, such as

Integrating Validation Application Block of Enterprise Library with CSLA.NET

CSLA.NET framework from Lhotka contains a lot of mechanisms for adding validations and business rules. Through CSLA.NET you can easily provide your own custom rules. Enterprise Library v3.0 now also contains a validation application block (VAB) that can be used through attributes and even from a configuration file.

The two validation mechanisms of validation are complementary. This can be done by adding a custom rule that uses the ValidationFactory of the VAB. This means we have something like:

public class VABRules
{
    public class VABRuleArgs : RuleArgs
    {
        private string _ruleset;

        public string Ruleset
        {
            get { return _ruleset; }
        }


        public VABRuleArgs(string propertyName) : this(propertyName, null)
        {
        }

        public VABRuleArgs(string propertyName, string ruleset) : base(propertyName)
        {
            _ruleset = ruleset;
        }
    }

    public static bool VABValid<T>(object target, RuleArgs e)
    {
        Validator<T> validator = ValidationFactory.CreateValidator<T>(((VABRuleArgs)e).Ruleset);

        if (validator == null)
            return true;

        ValidationResults results = validator.Validate(target);

        if (results == null)
            return true;

        foreach (ValidationResult result in results)
        {
            if (result.Key == e.PropertyName)
            {
                e.Description = result.Message;
                return false;
            }
        }

        return true;
    }
}

Having the VAB rule we simply need to decorate our properties with the validation attributes of VAB and an override of the AddBusinessRules method is needed to take into account the VAB rules. For example we can define a customer business object as follow:

[Serializable()]
public class Customer : Csla.BusinessBase<Customer>
{
    private int _id = 0;
    private string _firstName = string.Empty;
    private string _email = string.Empty;
    private int _rewardPoints;
    private string _countryCode = string.Empty;

    [Browsable(false), System.ComponentModel.DataObjectField(true, true)]
    public int Id
    {
        get
        {
            CanReadProperty("Id", true);
            return _id;
        }
    }

    [NotNullValidator(MessageTemplate="First Name may not be empty")]
    [StringLengthValidator(1, 60, MessageTemplate = "First Name must be between 1 and 60 characters long")]
    public string FirstName
    {
        get
        {
            CanReadProperty("FirstName", true);
            return _firstName;
        }
        set
        {
            CanWriteProperty("FirstName", true);
            if (!_firstName.Equals(value))
            {
                _firstName = value;
                PropertyHasChanged("FirstName");
            }
        }
    }

    [RegexValidator(@"w+([-+.']w+)*@w+([-.]w+)*.w+([-.]w+)*")]
    public string Email
    {
        get
        {
            CanReadProperty("Email", true);
            return _email;
        }
        set
        {
            CanWriteProperty("Email", true);
            if (!_email.Equals(value))
            {
                _email = value;
                PropertyHasChanged("Email");
            }
        }
    }

    [Int32RangeValidator(0, 1000000, MessageTemplate = "Rewards points cannot exceed 1,000,000")]
    public int RewardPoints
    {
        get
        {
            CanReadProperty("RewardPoints", true);
            return _rewardPoints;
        }
        set
        {
            CanWriteProperty("RewardPoints", true);
            if (!_rewardPoints.Equals(value))
            {
                _rewardPoints = value;
                PropertyHasChanged("RewardPoints");
            }
        }
    }

    [NotNullValidator(MessageTemplate = "Country may not be empty")]
    public string CountryCode
    {
        get
        {
            CanReadProperty("CountryCode", true);
            return _countryCode;
        }
        set
        {
            CanWriteProperty("CountryCode", true);
            if (!_countryCode.Equals(value))
            {
                _countryCode = value;
                PropertyHasChanged("CountryCode");
            }
        }
    }

    protected override object GetIdValue()
    {
        return _id;
    }

    protected override void AddBusinessRules()
    {
        ValidationRules.AddRule(VABRules.VABValid<Customer>, new VABRules.VABRuleArgs("FirstName"));
        ValidationRules.AddRule(VABRules.VABValid<Customer>, new VABRules.VABRuleArgs("Email"));
        ValidationRules.AddRule(VABRules.VABValid<Customer>, new VABRules.VABRuleArgs("RewardPoints"));
        ValidationRules.AddRule(VABRules.VABValid<Customer>, new VABRules.VABRuleArgs("CountryCode"));
    }
}

AcceptButton and CancelButton on a UserControl

In many enterprise applications there is the need that, regardless on which control you have the focus, that you can hit the Enter and/or Esc key to perform a default action. This behaviour is also common to web applications. On the Form control you find properties like AcceptButton and CancelButton, whereas the UserControl doesn't have these properties. The code below contains an AcceptButton and CancelButton that allows you to define a default action when the Enter or Esc key is pressed respectively.

public class UserControlEx : System.Windows.Forms.UserControl
{    
    private Button _acceptButton;
    private Button _cancelButton;

    public event EventHandler<EventArgs> AcceptEvent;
    public event EventHandler<EventArgs> CancelEvent;

    [Browsable(true)]
    public Button AcceptButton
    {
        get { return _acceptButton; }
        set { _acceptButton = value; }
    }

    [Browsable(true)]
    public Button CancelButton
    {
        get { return _cancelButton; }
        set { _cancelButton = value; }
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (msg.WParam.ToInt32() == (int)Keys.Enter)
        {
            OnAcceptEvent(EventArgs.Empty);

            if (_acceptButton != null)
                _acceptButton.PerformClick();
        }

        if (msg.WParam.ToInt32() == (int)Keys.Escape)
        {
            OnCancelEvent(EventArgs.Empty);

            if (_cancelButton != null)
                _cancelButton.PerformClick();
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }

    protected virtual void OnAcceptEvent(EventArgs args)
    {
        if (AcceptEvent != null)
            AcceptEvent(this, args);
    }

    protected virtual void OnCancelEvent(EventArgs args)
    {
        if (CancelEvent != null)
            CancelEvent(this, args);
    }
}

Geographical data and ASP.NET AJAX

In some enterprise applications you need to show geographical data such as countries and postcodes. Most of the time you need it for a registration page, where the user need to fill in the country and postcode/area.

Geonames is a free geographical database that contains over 8 million geographical names and it can be accessed through a number of webservices. For example the url http://ws.geonames.org/countryInfo? gives an xml with all countries, whereas the following request http://ws.geonames.org/postalCodeSearch?placename=be gives us all postcodes for a particular country (e.g. Belgium).

Most likely you need two dropdown lists, one for countries and one for postcodes, where the postcode dropdown is dependent from the country dropdown list. This is a very good example to introduce AJAX by using the CascadingDropdown that is included in ASP.NET AJAX.

To implement this functionality we need to implement two methods on a webservice, namely GetCountries and GetPostalCodesByCountry. The GetCountries simply returns all countries sorted by name and looks like this:

[WebMethod]
public CascadingDropDownNameValue[] GetCountries()
{
    List<CascadingDropDownNameValue> list = new List<CascadingDropDownNameValue>();

    CountryItemCollection countries = IStaySharp.Geonames.GeonamesService.GetAllCountries();

    for (int i = 0; i < countries.Countries.Length; i++)
    {
        list.Add(new CascadingDropDownNameValue(
            countries.Countries[i].CountryName,
            countries.Countries[i].CountryCode));
    }

    list.Sort(CompareCascadingDropDownNameValueByName);
    return list.ToArray();
}

Note that the list need to be converted to an array of CascadingDropDownNameValue objects. Note that we also sort the list by implementing a delegate named CompareCascadingDropDownNameValueByName.

private static int CompareCascadingDropDownNameValueByName(CascadingDropDownNameValue x, CascadingDropDownNameValue y)
{
    if (x == null && y == null)
        return 0;
    else if (x == null && y != null)
        return -1;
    else if (x != null && y == null)
        return 1;
    else
        return x.name.CompareTo(y.name);
}

The other webservice method, called GetPostalCodesByCountry, need to retrieve all postcodes for a particular country. The signature of the method is very strict. The parameter names must be named knownCategoryValues and category, otherwise it will fail!

[WebMethod]
public CascadingDropDownNameValue[] GetPostalCodesByCountry(string knownCategoryValues, string category)
{
    List<CascadingDropDownNameValue> list = new List<CascadingDropDownNameValue>();

    StringDictionary kv = CascadingDropDown.ParseKnownCategoryValuesString(knownCategoryValues);

    if (kv.ContainsKey("Country"))
    {
        string countryName = kv["Country"];

        PostalCodeItemCollection postalCodes = IStaySharp.Geonames.GeonamesService.GetPostalCodes(countryName);

        for (int i = 0; i < postalCodes.PostalCodes.Length; i++)
        {
            list.Add(new CascadingDropDownNameValue(
                string.Format("{0} ({1})", postalCodes.PostalCodes[i].PostalCode, postalCodes.PostalCodes[i].Name),
                postalCodes.PostalCodes[i].PostalCode));
        }
    }

    list.Sort(CompareCascadingDropDownNameValueByName);
    return list.ToArray();
}

In order to complete the webservice, the attribute ScriptService (line 3) need to be included so that a client javascript proxy can be generated. You can test this by calling your webservice like this http://localhost:9999/GeonamesService.asmx/js.

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService()]
public class GeonamesService : System.Web.Services.WebService
{
   ...
}

Finally we only need to add two CascadingDropDown controls on our aspx page with the following settings:

<asp:ScriptManager ID="scriptManager" runat="server" />

<asp:DropDownList ID="countriesDropDown" runat="server"/>
<ajaxToolkit:CascadingDropDown
    ID="countriesCascadingDropDown"        
    TargetControlID="countriesDropDown"
    Category="Country" 
    PromptText="Please select a country" 
    LoadingText="[Loading countries...]" 
    ServicePath="/GeonamesService.asmx"
    ServiceMethod="GetCountries"
    runat="server"/> 

<br/><br/>

<asp:DropDownList ID="postalCodesDropDown" runat="server"/>
<ajaxToolkit:CascadingDropDown
    ID="postalCodesCascadingDropDown"
    TargetControlID="postalCodesDropDown"
    Category="PostalCode" 
    PromptText="Please select postalcode" 
    LoadingText="[Loading postalcodes...]" 
    ServicePath="GeonamesService.asmx"
    ServiceMethod="GetPostalCodesByCountry"
    ParentControlID="countriesDropDown"
    runat="server"/>

The source code can be downloaded here: IStaySharp.AJAXSample