Walkthrough: Porting Asp.Net MVC Website to Mono 2.6.1 and MySql on Linux Apache - Porting to Mono Part 3 of 3

So for the third and final part of this series, I want to walk through how I ported a very small project I posted on CodePlex awhile back called SiteManager. It is an extremely simplified CMS application that I wrote using Linq to Sql and MVC in order to provide a very basic and easy website tool. It uses membership provider for authentication and Linq to Sql Classes with its database in MS Sql Server. For this walkthrough, I’ll be converting the database to MySql and adapting Linq to Sql to Dblinq so it can completely run on Mono and Linux.

Index

Getting Started

Since I’ve already ported this application, it is very easy to identify any of the problem points. But for any new project, I would identify the basic items related to common issues with changing platforms like: data access, authentication, and any operating system specific code. Using the MoMA tool is hugely beneficial to see how your code base might fare in the Mono framework. It will point out any specific code you’re using that could cause problems.   You should also browse the Mono site and learn some basics about the framework since they have quite a few useful extended frameworks that are not part of the Windows .Net runtime.  They also have another nice guide out there for porting Asp.Net applications where they use PostgreSql and Blog Starter Kit.

 

Database Migration

You should know that Microsoft Sql Server works fine with Mono and works well when you are in a Windows environment and want to run Mono applications against existing Sql Servers. Since we’re running this application completely on a Linux server, as you might do in a hosted Linux environment, leaving our database in Sql Server isn’t an option.  I chose to migrate to MySql purely for personal preference since I have a little more experience with this one rather than others like PostgreSql or Sqlite. However with Dblinq, you will have quite a few options.

Create an Empty MySql Database and an Application User

If you recall from my first post in this series, we installed and configured MySql server.   We also discussed creating new users. You will want to create a new database with a new user who has access to it from the local machine. (Note that this can also be performed with the MySql Workbench GUI Tools).

So to begin, using Putty, SSH into your server.  Then run:

root@server: mysql -u admin_username -p  
Enter password: ********  
create database sitemanager_mono;  
grant all privileges on sitemanager_mono.* to 'aspnet'@'localhost' identified by 'pass';  
quit

You may want to replace the username and password I used here with something more secure, but for this demo I kept it simple. You can do that by replacing ‘aspnet’ with a username of your choice and ‘pass’ with a password of your choice.

You now have a new empty database with its own power user account that can only access it from the local machine. This will be our application user so keep the credentials handy for later when we edit the config file.

Migrating Sql Server Schema to MySql

I used the MySql Migration Toolkit (part of the GUI Workbench) to perform my migration. It actually worked out pretty well with the exception of a few data type conversions. I like to use varchar(max) fields in Sql server. I also use bit fields for booleans since they parse right over to C# booleans.  The migration tool didnt’ like varchar(max) and converted them to varchar(-1). It also converted my bit fields to tinyint(4). So during the migration, I was able to make edits to a few of my tables that contained those fields. For varchar(max), you have a choice. You can: use a smaller field like varchar(5000), use a Text field, or you can choose to store your text data in a binary blob and encode/decode it to UTF8.  For this, application, I went the route of selecting a smaller varchar field for simplicity; but if my content pages were large, I would probably go the route of text. I found some interesting discussion in this post on the topic.

So in order, here are the steps I took using the migration tool:

Step 1  
Step 2

Choose your source database.

Step 3

Choose your target database.

Step 4 Select the source schema.
Add exclusions. I chose to exclude my membership tables since I configured those to use a different catalog.
Step 6 Utf-8 was fine for my solution
Step 7 Mapping error review
Step 8 Execution step
Step 9 Execution results
Step 10

Script errors will appear on the last screen.  Make the necessary changes, click Apply Changes, and Recreate Objects for each error.  When finished, click next to finish.

This is where you’ll see varchar(-1) for conversion from varchar(max) fields.  Change this to text or varchar(n).

Note that sql bit fields convert to smallint(4) if you wish to change that back to bit.

 

Linq to Sql Classes to Dblinq

After fixing errors and executing the script, you should now have your full schema migrated to MySql. Now you need to convert your Linq to Sql to Dblinq. Mono uses Dblinq as its Linq to Sql replacement internally. I chose to manually use DBlinq because I wasn’t sure how much of the Dblinq code was available in Mono.  I downloaded the current version of the Dblinq project from their public Subversion repository.

So the basic idea here is that we’re going to swap out L2S with Dblinq in a nearly transparent way.  We’ll drop the existing DBML file from our project, use DBMetal.exe to regenerate new classes, and then update our DBFactory utility function to create a new data context from the DBlinq side.   Essentially, this will allow us to re-use the same operational syntax in the Repository class with minimal changes.

For generating new classes, I used the DBMetal utility that comes with Dblinq. Here's the batch file I setup to generate my code:

REM: note that the '-sprocs' option is turned on

"DbMetal.exe" --provider=MySql -database:sitemanager_mono -server:carbuncle -user:user -password:pass "-namespace:IWS.SiteManager.Core.Model" -code:Generated.cs -sprocs --pluralize

After generating the classes, I replaced the DBFactory GetProvider function with this:

public static Model.SiteManager GetProvider()  
{
    var con_string = GetDefaultConnectionString();
    return new Model.SiteManager(new MySql.Data.MySqlClient.MySqlConnection(con_string));
}

I used a repository class as a helper to access my data model. Each repository class contains its own instance of the data context (in this case, Data.SiteManager). During construction of the repository class, it creates a new instance of the data context so any of the factory functions are able to use it without maintaining their own context. So I replaced all the member variable types for existing data contexts with the new one generated from Dblinq. I also noticed Dblinq had different casing than the original column names.  So where I had columns like, “ModifiedUTC” before, it was “ModifiedUtc” now. This was a pretty painless fix with find/replace.

You’ll notice here that the Linq syntax itself didn’t change at all.

public class ContentRepository  
{
    SiteManager _DB;

    public ContentRepository()
    {
        _DB = DBFactory.GetProvider();
    }

    /// <summary>
    /// gets a page by its id
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public Content GetContent(int id)
    {
        return _DB.Contents.Where(o => o.ID == id).SingleOrDefault();
    }
    /// <summary>
    /// Load a page by its permalink. 
    /// </summary>
    /// <param name="permalink"></param>
    /// <returns></returns>
    public Content GetPage(string section, string permalink)
    {
        return _DB.Contents.Where(o => o.SectionID == section && o.Permalink == permalink.Trim().ToLower()).SingleOrDefault();
    }
    /// <summary>
    /// gets the configured default page.
    /// </summary>
    /// <returns></returns>
    public Content GetIndexPage(string section)
    {
        var data = _DB.Sections.Where(o => o.ID == section).SingleOrDefault();

        Content page = null;

        if (data != null && data.DefaultContentID.HasValue)
            page = _DB.Contents.Where(o => o.ID == data.DefaultContentID.Value).SingleOrDefault;

        return page;
    }
    public string GetSectionIDForPage(int id)
    {
        var section = _DB.Contents.Where(o => o.ID == id).Select(o => o.SectionID).SingleOrDefault();
        return section;
    }
    public void SaveContent(Content content)
    {
        Section section = null;

        if (_DB.Sections.Where(o => o.ID == content.SectionID).Count() == 0)
        {
            section = new Section();
            section.ID = content.SectionID;
            section.Description = "Auto-generated section for new page.";
            _DB.Sections.InsertOnSubmit(section);
            _DB.SubmitChanges();
        }

        //take current version, push it to content history, overwrite current with new. 
        Content old = this.GetContent(content.ID);
        if (old != null)
        {
            //archive old version. 
            ContentHistory row = new ContentHistory();
            row.ContentID = content.ID;
            row.Body = old.Body;
            row.CreatedUtc = DateTime.UtcNow;
            _DB.ContentHistories.InsertOnSubmit(row);
            _DB.SubmitChanges();


            old.LoadFromExisting(content); //load current values
        }
        else
        {
            old = content;
            _DB.Contents.InsertOnSubmit(old);
        }


        _DB.SubmitChanges();

        if (section != null) //was a new one. set the default content id.
        {
            section.DefaultContentID = content.ID;
            _DB.SubmitChanges();
        }
    }
    public void DeleteContent(int id)
    {
        var data = _DB.Contents.Where(o => o.ID == id).SingleOrDefault();
        if (data != null)
        {
            //remove conflicts first. 
            var section = this.GetSectionByID(data.SectionID);
            if (section != null)
            {
                section.DefaultContentID = null;
                _DB.SubmitChanges();
            }

            _DB.Contents.DeleteOnSubmit(data);
            _DB.SubmitChanges();
        }
    }
}

At this point, the project should be compilable. You may need to make a few tweaks to get the loose ends tied up.

 

Membership Provider

After following the instructions from the last post, you should get your membership provider setup and configured. For Mono, I think the biggest hang up for me was not being able to use hashed passwords. Alternatively, you can investigate using encrypted passwords. For simplicity, I used clear text passwords here.

 

Membership Authorization

You should now have a runable MVC application. Almost everything should work with exception to how security applies rules from the web.config.  I noticed Mono doesn’t apply the same security authorization rules to paths for MVC like it does for web forms. I removed <location path=””> tags from my config file and placed the authorization restrictions to my controller actions instead.

So for example this config section would be used to restrict access to all actions in the Manage controller on Windows.

<location path="Manage">  
    <system.web>
        <authorization>
            <allow roles="Admins,Editors"/>
            <deny users="*"/>
        </authorization>
    </system.web>
</location>

So on Mono, we remove it from web.config and move to the manage controller as an attribute on ALL actions you want to restrict. This can become a maintenance hassle, but they may eventually fix this for MVC in Mono.

[Authorize(Roles="Admins,Editors")]  
public ActionResult Index()  
{
    return View();
}

 

Deployment

Don’t forget to make changes to your web.config file. Include the credentials you setup for your database earlier.  Then using Build->Publish, publish the site to a local folder. Don’t include the App_Data folder and have it delete all files before publishing. Then using FileZilla over SFTP, connect to your server and upload the published website to the web folder you configured. With the latest Mono, the website should automatically reload the new assemblies. If you have any doubt about this, you can always SSH into the server and restart apache.

To restart apache, gain root access using su and then:

service apache2 restart

Optionally, if restarting apache doesn’t seem to work, you can also search and kill off the mono processes:

diabolos:/home/nathan # ps -A | grep mono  
 2162 ?        00:00:03 mono
 2164 ?        00:00:08 mono
diabolos:/home/nathan # kill 2162  
diabolos:/home/nathan # kill 2164

The next request to the server will restart these processes as needed.

 

Quick Guide to VIM

Every once in awhile, you may need to edit files that live on the server without going through the whole process of publishing, deploying, etc. To do this, I like to use a tool called VIM. So what is VIM?  For those of you haven’t used it, it’s a simple text editor in Linux that comes with most distributions. You can think of VIM like “edit” for DOS on steroids. The OpenSuse version even has code completion and color-coded syntax, which is very neat.

To open a file with VIM, SSH into your server and run this command:

vi filename

When you open the file, there isn’t a menu, but you’ll see ~ on the left and the text of your file.  VIM has modes (much more than I’ll discuss here), but you need to know of at least two:  insert mode and append mode.  This will allow you to insert or append text starting at the cursor position. So move your cursor to any location and press i for insert mode. You will be able to freely type almost anything. To exit insert mode, hit Esc. Append mode uses the same behavior by pressing a. You will append text starting at the cursor position. Entering :d will delete the current line. Be sure not to use arrow keys while in an editing mode.

To save a file, press Esc to exit editing mode, and then press :w to write the file. You can then enter :q to quit.   And to save and quit, use :wq.  You can get more info by entering man vi at the command prompt.

 

Wrap Up

So that’s it. This is a tiny project, but you may be able to apply these ideas to your own projects.  For me, this gets my imagination fired up about what kinds of solutions I can take advantage of using Mono. I think there’s a lot of potential here.  I’ve recently begun using interfaces and dependency injection with my projects for data frameworks to avoid the mess we covered in the Linq to Sql to Dblinq section. This means I can write a decoupled application that can optionally use Linq to Sql or Dblinq as a pluggable assembly that is injected using settings in my config file. This will make the transition between Windows and Mono much less painful.

 

Get the Source

To obtain the source, head over to sitemanager.codeplex.com and download the latest source.  You’ll find the mono ported code under the /branches/mono-mysql-fork branch.

 

Useful Links

comments powered by Disqus