I have been interested in learning C# and applying it to a real application for a while. Since I work in a Java/J2EE shop, the closest I got were doing some toy example apps in my spare time. However, an opportunity came up recently to integrate a prebuilt C#/ASP.NET application into our site, and I had a chance to help out with it. This article describes a small C# application with ADO.NET to connect to a MySQL database and ASP.NET for the web pages that I set up and developed with MonoDevelop on Linux using Mono. I did this to refresh my understanding of how to use these components to stitch together a useful .NET application. Because my basic worldview is that of a Java programmer, the article draws parallels with Java frameworks. I am hoping that this would be of use to Java programmers who are looking to venture out into C# and .NET applications using Mono on Linux.
C#/.NET code should be quite easy to pick up for Java/J2EE developers since the concepts are quite similar, although the terminology is different. Overall, I think .NET provides a tighter stack with fewer moving parts, because they take an intrinsic "convention over configuration" approach with their components. I guess it's easier for Microsoft to do this because they provide all the components in the stack, unlike the plug-and-play approach taken by Java developers. But on to the terminology differences - here are some I found:
C#/.NET Term | Java/J2EE Term | Comment |
CLR (Common Language Runtime) | JVM (Java Virtual Machine) | CLR targets multiple languages on a single platform (now multiple with Mono running on Linux, Windows and Mac OSX), and the JVM targeting one language on multiple platforms (although that may be changing too, with the scripting support JSR). |
ASP.NET | JSP | This is only partially true, because ASP.NET has more functionality built in. The closest analogy on the Java side I can think of is Tapestry. Like Tapestry with its .java, .page and .html files, ASP.NET has the .aspx.cs and .aspx files. The .aspx.cs file is a C# subclass of Page which contains logic and references to we controls such as buttons, text boxes, etc. These controls and all methods in the .aspx.cs file can be accessed from the .aspx file. |
ADO.NET | JDBC | Again, only partially true. The JDBC abstraction, ie the ability to write the same code regardless of the target database, which Java programs all know and love, is available in the form of an OLE database driver in .NET but not widely used because of speed issues. The standard practice in ADO.NET is to use drivers with native support for different databases, so your code will be different if you were using the ByteFX drivers for MySQL versus the Microsoft SqlClient driver for MS-SQL. |
Assembly | JAR/WAR | The deployable unit for a .NET application is a .DLL or an .EXE, depending on whether they are libraries which will be included as references into another project, or standalone executable applications. |
GAC (Global Assembly Cache) | Your Maven2 repository, if you use one | Some people use Maven, so this is roughly equivalent to the Maven2 repository. Redhat also includes a Java RPM with JARs for the Apache commons projects and some others. |
mcs | javac | The C# compiler from Mono. |
mono | java -jar | Allows you to run "executable" JAR files (the ones with the main method specified in the META-INF/MANIFEST.MF file). |
There are other one-to-one mappings, such as monodoc for javadoc. For building .NET apps from the command line, you can use NAnt, which is Ant for .NET. Nunit, the unit testing framework for .NET is based on JUnit, but has much richer annotation support than the commonly used JUnit 3.8. JUnit 4.0, which was in beta last I looked, looks a lot like Nunit. I haven't used NAnt or Nunit myself, though.
As for the language itself, C# is also very similar to Java, although conventions are different, such as uppercasing the first character of all method calls. There is also a much greater reliance on operator overloading (using == instead of Java's .equals() for equality checks and map["key"] instead of Java's map.get(key), for example). There are also other little things such as the "using" keyword, which is kind of like the "static import" in Java 1.5, and "namespace" instead of "package". The get/set notation in C# is also quite cool, saves a lot of typing.
But enough generalities. Let me tell you what I did, which was the objective of this article anyway. I set out to develop a simple database backed web application with 2-3 ASPX pages. My database consisted of a single MySQL table of book reviews, with the following (non-normalized for simplicity) schema.
1 2 3 4 5 6 7 8 9 | mysql> describe books;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | bigint(20) | | PRI | NULL | auto_increment |
| name | varchar(255) | | | | |
| author | varchar(255) | | | | |
| review | text | | | | |
+-------------+--------------+------+-----+---------+----------------+
|
My first step was to create a Console application in MonoDevelop to wrap the table with a DAO (Data Access Object). The DAO will provide methods to add rows to the table, return all rows, and find reviews based on reviewer. Note that creating an empty application in MonoDevelop is not a good idea, because then the Assembly.cs file will not be created. The Assembly.cs file is similar to the META-INF/MANIFEST.MF file, and is required. Here is the DAO code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | // BookReviewDao.cs
using System;
using System.Data;
using ByteFX.Data.MySqlClient;
namespace BookReviews {
/// <summary>
/// This class interfaces with the MySQL database containing
/// the book review data.
/// </summary>
public class BookReviewDao {
string dbHost = "localhost";
string dbName = "bookshelfdb";
string dbUser = "root";
string dbPass = "mysql";
MySqlConnection conn;
// Construct an instance of the BookReviewDao object.
public BookReviewDao() {
string connectionString = String.Format("Persist Security Info=False;Server={0};Database={1};User Id={2};Password={3}", dbHost, dbName, dbUser, dbPass);
Console.Out.WriteLine(connectionString);
conn = new MySqlConnection(connectionString);
}
// Returns all book review records from the database.
public DataSet FindAll() {
DataSet ds = new DataSet("FindAll");
try {
conn.Open();
MySqlDataAdapter adapter = new MySqlDataAdapter("select name, author, review from books", conn);
adapter.Fill(ds);
conn.Close();
} catch (Exception e) {
Console.Error.WriteLine(e);
} finally {
if (conn.State == ConnectionState.Open) {
conn.Close();
}
}
return ds;
}
// Returns all book review records by the specified author.
public DataSet FindByUser(string authorName) {
DataSet ds = new DataSet("FindByUser");
try {
String selectQuery = String.Format("select name, author, review from books where author='{0}'", authorName);
conn.Open();
MySqlDataAdapter adapter = new MySqlDataAdapter(selectQuery, conn);
adapter.Fill(ds);
conn.Close();
} catch (Exception e) {
Console.Error.WriteLine(e);
} finally {
if (conn.State == ConnectionState.Open) {
conn.Close();
}
}
return ds;
}
// Inserts a single book review record into the database.
public void AddData(String bookName, String authorName, String review) { try {
MySqlCommand command = new MySqlCommand("insert into books(name, author, review) values (@name, @author, @review)", conn);
conn.Open();
command.Parameters.Add("@name", bookName);
command.Parameters.Add("@author", authorName);
command.Parameters.Add("@review", review);
command.ExecuteNonQuery();
conn.Close();
} catch (Exception e) {
Console.Error.WriteLine(e);
} finally {
if (conn.State == ConnectionState.Open) {
conn.Close();
}
}
}
// Returns true if user is already in the system, else returns false.
public bool IsValidUser(String authorName) {
Int64 numRows = 0;
try {
MySqlCommand command = new MySqlCommand(String.Format("select count(*) from books where author='{0}'", authorName), conn);
conn.Open();
numRows = (Int64) command.ExecuteScalar(); // bug in ByteFX, returns Int64 instead of Int8 (int)
conn.Close();
} catch (Exception e) {
Console.Error.WriteLine(e);
} finally {
if (conn.State == ConnectionState.Open) {
conn.Close();
}
}
return (numRows > 0);
}
}
}
|
One thing to note is that ByteFX works with the authentication system for MySQL 4.0, so if you are using a higher version, you will need to downgrade its authentication to the "old-password" mode. Here is how this is done, I found this in a newsgroup posting on the web.
1 2 3 | mysql> use mysql;
mysql> set password for root@localhost=old_password('mysql');
mysql> flush privileges;
|
Since my application is still a console application, I could call the MainClass.Main method to add my records and test the functioning of the other methods. Here is the MainClass.cs code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | // Main.cs
using System;
using System.Data;
namespace BookReviews {
class MainClass {
private void Find(string authorName) {
BookReviewDao dao = new BookReviewDao();
DataSet ds = dao.FindByUser(authorName);
Format(ds);
}
private void FindAll() {
BookReviewDao dao = new BookReviewDao();
DataSet ds = dao.FindAll();
Format(ds);
}
private void Format(DataSet ds) {
DataTable books = ds.Tables[0]; // bug in ByteFX, return "Table" for TableName.
for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
Console.Out.Write(books.Columns[curCol].ColumnName.Trim() + "\t");
}
Console.Out.WriteLine();
for (int curRow = 0; curRow < books.Rows.Count; curRow++) {
for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
Console.Out.Write(books.Rows[curRow][curCol].ToString().Trim() + "\t");
}
Console.Out.WriteLine();
}
}
private void Add() {
for (;;) {
Console.Out.Write("Book name:");
string bookName = Console.In.ReadLine();
Console.Out.Write("Author:");
string authorName = Console.In.ReadLine();
Console.Out.Write("Review:");
string review = Console.In.ReadLine();
BookReviewDao dao = new BookReviewDao();
dao.AddData(bookName, authorName, review);
Console.Out.Write("Add another (y/n)[y]?");
string yorn = Console.In.ReadLine();
if (yorn.StartsWith("n")) {
break;
} else {
continue;
}
}
}
private bool IsValid(string authorName) {
BookReviewDao dao = new BookReviewDao();
return dao.IsValidUser(authorName);
}
private void Help() {
Console.Out.WriteLine("Welcome to BookReviews");
Console.Out.WriteLine("Valid options are:");
Console.Out.WriteLine("--find: find by author");
Console.Out.WriteLine("--findall: find all records");
Console.Out.WriteLine("--add: add new record");
Console.Out.WriteLine("--help: display this message");
}
public static void Main(string[] args) {
MainClass bookReviews = new MainClass();
if (args.Length == 0) {
bookReviews.Help();
} else {
if (args[0] == "--find") {
string authorName = args[1];
bookReviews.Find(authorName);
} else if (args[0] == "--findall") {
bookReviews.FindAll();
} else if (args[0] == "--add") {
bookReviews.Add();
} else if (args[0] == "--valid") {
string authorName = args[1];
bookReviews.IsValid(authorName);
} else {
bookReviews.Help();
}
}
}
}
}
|
Using this, I populated the table with some test data. Here is how it was called.
1 2 | $ cd bin/Debug
$ mono BookReviews.exe --add
|
Since I really want the DAO to be used by my ASP.NET web application, I converted this to be a library. You can do this by right clicking on project, selecting Configurations, change Compile_Target from Executable to Library. Code generated in bin/Debug/*.exe changes to bin/Debug/*.dll.
The next step is to create the ASPX pages and the corresponding ASPX.CS (called Code-behind) C# class code. I visualized a mini-app with a login page which shows a list of reviews made by you. This page has a link to all reviews. The all reviews page has a link back to the review by the particular logged in user. So I needed a Index.aspx page for the login page and the List.aspx page for the list pages. Mono has no template for a Web Project (unlike the Microsoft Visual Studio IDE on Windows). I copied some boilerplate code from Charlie Calvert's page for Global.asax.cs and Index.aspx.cs and List.aspx.cs, and added the required event handlers to the last two files. Here is the code for the .aspx pages and the corresponding .aspx.cs pages.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!-- Index.aspx -->
<%@ Page language="c#" Codebehind="Index.aspx.cs" AutoEventWireup="false" Inherits="BookReviews.Index" %>
<Html>
<Head>
<Title>BookReviews Login</Title>
</Head>
<Body>
<center><asp:Label id="lblMessage" runat="server" text="Please login" /></center>
<form id="loginForm" method="post" runat="server">
<asp:Label id="lblUsername" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 164px" runat="server" text="Username:" />
<asp:TextBox id="txtUsername" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 164px" runat="server" />
<asp:Label id="lblPassword" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 228px" runat="server" text="Password:" />
<asp:TextBox id="txtPassword" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 228px" runat="server" />
<asp:Button id="btnLogin" style="Z-INDEX: 101; LEFT: 144px; POSITION: absolute; TOP: 292px" runat="server" text="Login" />
<asp:Button id="btnCancel" style="Z-INDEX: 101; LEFT: 256px; POSITION: absolute; TOP: 292px" runat="server" text="Cancel" />
</form> </Body>
</Html>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | // Index.aspx.cs
using System;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace BookReviews {
// <summary>
// Code-behind component for the Index.aspx page. Defines event
// handlers for this page.
// </summary>
public class Index : Page {
protected Label lblUsername;
protected Label lblPassword;
protected TextBox txtUsername;
protected TextBox txtPassword;
protected Button btnLogin;
protected Button btnCancel;
protected Label lblMessage;
// generated code, if mono could do this. This is copied from this site: // http://bdn.borland.com/article/0,1410,32057,00.html
private void Page_Load(object sender, System.EventArgs e) {
// user code to initialize the page
}
protected override void OnInit(System.EventArgs e) {
InitializeComponent();
base.OnInit(e);
}
protected void InitializeComponent() {
this.btnCancel.Click += new System.EventHandler(this.BtnCancel_Click);
this.btnLogin.Click += new System.EventHandler(this.BtnLogin_Click); this.Load += new System.EventHandler(this.Page_Load);
}
// end of generated code
// <summary>
// Acts just like a reset, except that this is done in C#
// code, on response to a click event on this cancel button.
// </summary>
private void BtnCancel_Click(object sender, System.EventArgs e) {
txtUsername.Text = "";
txtPassword.Text = "";
}
// <summary>
// Fired in response to a click event on the login button. Sends
// off to the BookReviewDao for authentication, and redirects to
// the list page if successful, else, displays an error and re-prompts
// for user name and password. If login is successful, then it
// also puts the user name in the session. We later look it up as
// needed for any personalization in the other pages.
// </summary>
private void BtnLogin_Click(object sender, System.EventArgs e) {
BookReviewDao dao = new BookReviewDao();
if (dao.IsValidUser(txtUsername.Text)) {
// we dont do much password validation, all
// we care about is that he is already in our
// system
Session["userName"] = txtUsername.Text;
Response.Redirect("/List.aspx?author=" + txtUsername.Text);
} else {
lblMessage.Text = "Invalid Login, please try again";
lblMessage.ForeColor = Color.Red;
}
}
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- List.aspx -->
<%@ Page language="c#" Codebehind="List.aspx.cs" AutoEventWireup="false" Inherits="BookReviews.List" %>
<Html>
<Head>
<Title>BookReviews List</Title>
</Head>
<Body>
<form runat="server">
<asp:DataList runat="server" id="bookReviewDl">
<HeaderTemplate>List of book reviews</HeaderTemplate>
<ItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "name") %></td>
<td><%# DataBinder.Eval(Container.DataItem, "author") %></td>
<td><%# DataBinder.Eval(Container.DataItem, "review") %>
</ItemTemplate>
<FooterTemplate />
</asp:DataList>
<a href="<%= GetOtherListHyperLink().NavigateUrl %>"><%= GetOtherListHyperLink().Text %></a>
</form>
</Body>
</Html>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | // List.aspx.cs
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace BookReviews {
// <summary>
// Code-behind component of the List.aspx page.
// </summary>
public class List : Page {
protected HyperLink lnkOtherList;
protected DataList bookReviewDl;
// generated code, if mono could do this. This is copied from this site: // http://bdn.borland.com/article/0,1410,32057,00.html
public void Page_Load(object sender, System.EventArgs e) {
// user code to initialize the page
if (!IsPostBack) {
// scan the parameters looking for author
NameValueCollection parameters = Request.Params;
ICollection keys = (ICollection) parameters.Keys;
string author = null;
foreach (string key in keys) {
if (key == "author") {
author = (string) parameters["author"];
break;
}
}
BookReviewDao dao = new BookReviewDao();
DataSet reviewDs;
if (author == null) {
reviewDs = dao.FindAll();
lnkOtherList = new HyperLink();
lnkOtherList.Text = "Show book reviews by " + ((string) Session["userName"]);
lnkOtherList.NavigateUrl = "/List.aspx?author=" + ((string) Session["userName"]);
} else {
reviewDs = dao.FindByUser(author);
lnkOtherList = new HyperLink();
lnkOtherList.Text = "Show all book reviews";
lnkOtherList.NavigateUrl = "/List.aspx";
}
bookReviewDl.DataSource = createDataSource(reviewDs);
bookReviewDl.DataBind();
}
}
protected override void OnInit(System.EventArgs e) {
InitializeComponent();
base.OnInit(e);
}
protected void InitializeComponent() {
this.Load += new System.EventHandler(this.Page_Load);
}
// end of generated code
// <summary>
// Creates a in-memory Collection that can be used by the aspx
// page based on the DataSet object returned from the Dao.
// </summary>
public ICollection createDataSource(DataSet ds) {
DataTable books = ds.Tables[0]; // bug in ByteFX, return "Table" for TableName.
bool debug = true;
if (debug) {
for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
Console.Out.Write(books.Columns[curCol].ColumnName.Trim() + "\t");
}
Console.Out.WriteLine();
for (int curRow = 0; curRow < books.Rows.Count; curRow++) {
for (int curCol = 0; curCol < books.Columns.Count; curCol++) {
Console.Out.Write(books.Rows[curRow][curCol].ToString().Trim() + "\t");
}
Console.Out.WriteLine();
}
}
return new DataView(books);
}
// <summary>
// Returns a reference to the HyperLink control.
// </summary>
public HyperLink GetOtherListHyperLink() {
return lnkOtherList;
}
}
}
|
Basically, the Page.Page_Load method is called when the page is loaded and the *_Click events are triggered from the mouse clicks on the page. Not very hard to understand if you've already been on the Tapestry learning curve, but may require a bit of unlearning for classic J2EE programmers.
The Web.config file I used is minimal, copied from the demo web.config file from the xsp sample directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?xml version="1.0" encoding="utf-8"?>
<!-- Web.config -->
<configuration>
<system.web>
<customErrors mode="Off"/>
<authentication mode= "None">
</authentication>
<httpModules>
</httpModules>
</system.web>
<appSettings>
<add key="MonoServerDefaultIndexFiles" value="index.aspx, Default.aspx, default.aspx, index.html, index.htm" />
</appSettings>
</configuration>
|
To run the pages, I used the xsp server, which is distributed as part of Mono. You can also add the mod_mono to the Apache httpd webserver to serve ASP pages in a production environment. To do this, make sure that the .DLL file generated by MonoDevelop when you build your project is in the bin/ directory of your project. It is usually in bin/Debug, but I would just move it before starting xsp. xsp should be started in your project directory.
1 2 3 4 5 6 7 8 | $ pwd
/home/sujit/Projects/BookReviews
$ ls
AssemblyInfo.cs BookReviews.mdp Global.asax.cs List.aspx Web.config
bin BookReviews.mds Index.aspx List.aspx.cs
BookReviewDao.cs BookReviews.pidb Index.aspx.cs Main.cs
$ mv bin/Debug/BookReviews.dll bin/
$ xsp
|
The application is now browseable at http://localhost/Index.aspx. Since I was running on Linux, I used Mozilla Firefox. I mention this only because there is a general belief that ASP pages cannot be used from browsers other than MS Internet Explorer. This may be true if you were using client side Visual Basic scripts instead of Javascript, but since ASPX is all done server-side, pages can be served to any regular browser.
My final step was to import the project I was going to integrate into MonoDevelop on my Linux desktop. MonoDevelop has a "Import Visual Studio project" option. The important thing to note is that you will need to select the .csproj file, which is where Visual Studio keeps its project information. If the imported project consists of multiple subprojects, as mine did, each of them needs to be imported separately as solutions. The dependencies will be need to be set up manually by compiling the low level modules separately into DLLs, and then adding them as references to the main solution.
Here are some resources I found useful. There are a lot of resources on the internet dealing with .NET development, some of it served by Microsoft (MSDN), and some from the Code Project.
- Video tutorials on using Mono on Knoppix.
- Tutorial on developing your first application with MonoDevelop.
- Article by Charlie Calvert on the boilerplate ASP components needed for a web project.
I also found the following books useful. Of course, I had bought these some time back, hoping to use them one day, but never did. So arguably, I used these since these were the ones I had lying around, but they are worth looking at if you want to start out with C#/.NET development.
- Mono - A Developer's Notebook by Edd Dumbill and Niel M Bornstein - A great book if you want to get up and running with C# and .NET using Mono really quickly. It lacks depth though, so be prepared to do a little work looking up details on the web.
- C# and the .NET Platform by Andrew Troelsen - The link is to the second edition. I have the first (beta) edition, and admittedly, there are some places where its dated (the code does not work). But it provides a very detailed introduction to the various components that make up the .NET framework.
Compared to Visual Studio, which I also installed on my Windows computer, MonoDevelop is faster to get up and running. But Visual Studio is much more slicker, although as an Eclipse user, I think it could benefit from some of the features available there.
I think Mono would be very useful as a simple (simpler than C/C++) language to build interfaces to system tools on Linux. Apparently the Mono developers seem to think so too, as evidenced by their greater support for Gtk# and Glade# project templates. RedHat seems to have standardized on Python for this work, but Novell is making some steps in that direction with SuSE.