Let's move to the overriding of the Query methods.
According to step 3, we first override the Process method. In its body, we will generate the SQL query, based on the value that is passed with the bucket object (which is filled by the extender against the query expression). Then, we run the query against the database with a DataContext class. Finally, we build the T object and add it to the IModiy<T> items collection.
First, we need to know if any order by clause is provided in the query. If not, then we will perform an order by on a unique field. (This is a requirement when we are building a WITH statement that has the Over clause, but not for normal select statement).
StringBuilder orderByBuilder = new StringBuilder();
string orderbyField = bucket.UniqueItems != null ? bucket.UniqueItems[0] : string.Empty;
if (bucket.OrderByClause != null)
{
orderByBuilder.Append("ORDER BY ");
orderByBuilder.Append("\r\n");
orderByBuilder.Append(bucket.OrderByClause.FieldName + " " +
(bucket.OrderByClause.IsAscending ? "asc" : "desc"));
}
StringBuilder orderByBuilder = new StringBuilder();
string orderbyField = bucket.UniqueItems != null ? bucket.UniqueItems[0] : string.Empty;
if (bucket.OrderByClause != null)
{
orderByBuilder.Append("ORDER BY ");
orderByBuilder.Append("\r\n");
orderByBuilder.Append(bucket.OrderByClause.FieldName + " " +
(bucket.OrderByClause.IsAscending ? "asc" : "desc"));
}
IsAscending: false if order by descending is used in query, otherwise true
FieldName: the field name on which the order by is used. It can be, for example, order by book.id descending / order by "id" descending.
In the code, bucket.UniqueItems returns the array of property names on which UniqueAttribute or a child class is used.
Next, we have to build the select query, based on the expression items.
StringBuilder builder = new StringBuilder();
StringBuilder builder = new StringBuilder();
Let's examine the portion of the if block that will generate the SQL with a WITH statement if Take is provided in the query block. Note that when Skip > 0 but no take is provided, we need to show an exception as well (as currently it is not supported). There we need to provide the following exception:
if (bucket.ItemsToSkip > 0 && bucket.ItemsToTake == null)
{
throw new
ApplicationException("Skip with value > 0 but with no Take, is not supported");
}
if (bucket.ItemsToSkip > 0 && bucket.ItemsToTake == null)
{
throw new
ApplicationException("Skip with value > 0 but with no Take, is not supported");
}
Now, bucket.ItemsToTake is null if there is no Take in the query or less it will have numeric value. Note that I have declared itemToTake as Nullable, so that a user can distinguish between valued and not valued state of the property.
The whole if-else logic for processing SQL looks like the following:
if (bucket.ItemsToTake != null)
{
// 1.Build Select statement using WITH
}
else
{
// 2.General select statement followed by order by , if any
}
// 3. Run query and fill IModify<Items> collection.
if (bucket.ItemsToTake != null)
{
// 1.Build Select statement using WITH
}
else
{
// 2.General select statement followed by order by , if any
}
// 3. Run query and fill IModify<Items> collection.
1. Building the Select statement using WITH A typical WITH clause generated by this logic looks like so:
WITH FilteredList(Id,Author,Title,ISBN,LastUpdated, [RowNumber]) AS(
SELECT Id,Author,Title,ISBN,LastUpdated, Row_number()OVER(ORDER BY
LastUpdated asc) as [RowNumber] FROM Book where book.author = “Dan Brown”
)Select * from FilteredList WHERE [Rownumber] Between (3) and (7)
WITH FilteredList(Id,Author,Title,ISBN,LastUpdated, [RowNumber]) AS(
SELECT Id,Author,Title,ISBN,LastUpdated, Row_number()OVER(ORDER BY
LastUpdated asc) as [RowNumber] FROM Book where book.author = “Dan Brown”
)Select * from FilteredList WHERE [Rownumber] Between (3) and (7)
The first task is to get the list of property names for the T type (e.g. Book). One way to do it is by using Reflection to extract the names out of it. An easier way is to use bucket.Items to get the names, which basically is a IDictionary<string,BucketItem>, where string is the name of the property and BucketItem contains the extended information about the property and how it is used in the query expression.
Getting the property names the easy way is done like in the following code:
string[] names = bucket.Items.Select(item => item.Value.Name).ToArray();
string fields = string.Join(",", names);
string[] names = bucket.Items.Select(item => item.Value.Name).ToArray();
string fields = string.Join(",", names);
Then, we properly format the string with the fields that we just got.
builder.Append("WITH FilteredList(" + fields + ", [RowNumber]) AS(");
builder.Append("\r\n");
builder.Append("SELECT " + fields + ", Row_number()");
builder.Append("WITH FilteredList(" + fields + ", [RowNumber]) AS(");
builder.Append("\r\n");
builder.Append("SELECT " + fields + ", Row_number()");
Earliar, we have built the orderByBuilder StringBuilder, which we used here to create the OVER clause. Here, two things are possible: If any orderby is used in query, then do the orderby using the mentioned property or object value. Otherwise, by the default, use the unique property of the object.
if (orderByBuilder.Length > 0)
{
builder.Append("OVER(" + orderByBuilder.ToString() + ")");
}
else
{
if (bucket.UniqueItems == null)
{
throw new ApplicationException("There should be at least one Unique identifier property in order to limit items");
}
builder.Append("OVER( Order By " + orderbyField + " asc)");
}
if (orderByBuilder.Length > 0)
{
builder.Append("OVER(" + orderByBuilder.ToString() + ")");
}
else
{
if (bucket.UniqueItems == null)
{
throw new ApplicationException("There should be at least one Unique identifier property in order to limit items");
}
builder.Append("OVER( Order By " + orderbyField + " asc)");
}
Next, we have to append the entity that the query targets. Here, bucket.Name will give name of the object - or the user-defined name if OriginalNameAttribute is provided - that maps to the entity name.
builder.Append(" as [RowNumber] FROM " + bucket.Name + " ");
builder.Append(" as [RowNumber] FROM " + bucket.Name + " ");
Finally, we have to build the WHERE clause and append the final stuff of WITH. That is, select between items.
CreateWhereClauseIfPossible(bucket, builder);
builder.Append("\r\n");
builder.Append(")");
builder.Append("Select * from FilteredList WHERE [Rownumber] Between (" + (bucket.ItemsToSkip + 1) + ") and (" + (bucket.ItemsToSkip +
bucket.ItemsToTake) + ")");
CreateWhereClauseIfPossible(bucket, builder);
builder.Append("\r\n");
builder.Append(")");
builder.Append("Select * from FilteredList WHERE [Rownumber] Between (" + (bucket.ItemsToSkip + 1) + ") and (" + (bucket.ItemsToSkip +
bucket.ItemsToTake) + ")");
Here, CreateWhereClauseIfPossible is used to build the Where clause, which internally calls BuildClause. It basically builds the clause based on the query expressions. For that I haven't used any black arts; just iterated over Bucket.Items in the following way:
foreach (string propertyKey in bucket.Items.Keys)
{
string value = Convert.ToString( bucket.Items[propertyKey].Value);
value = value.Replace("'", "''"); // incase user use ‘ in query
if (!insert)
{
[part_1] // build the query with key value and operator
}
else
{
if (!bucket.Items[propertyKey].Unique)
{
builder.Append("'" + value + "',");
}
}
}
foreach (string propertyKey in bucket.Items.Keys)
{
string value = Convert.ToString( bucket.Items[propertyKey].Value);
value = value.Replace("'", "''"); // incase user use ‘ in query
if (!insert)
{
[part_1] // build the query with key value and operator
}
else
{
if (!bucket.Items[propertyKey].Unique)
{
builder.Append("'" + value + "',");
}
}
}
if (bucket.Items[propertyKey].Value != null)
{
builder.Append(bucket.Items[propertyKey].Name + GetEquavalentSqlOperator(bucket.Items[propertyKey].ReleationType) + "'" + value
");
}
if (bucket.Items[propertyKey].Value != null)
{
builder.Append(bucket.Items[propertyKey].Name + GetEquavalentSqlOperator(bucket.Items[propertyKey].ReleationType) + "'" + value
+ "' AND ");
}
Here, bucket.Items[propertyKey].Name is the name of the property or user-defined name. Earlier in the article I have talked about declaring the properties of a query object other than string as Nullable. This is where it comes useful with if (bucket.Items[propertyKey].Value != null), to check if the property is used in a query expression. Finally, bucket.Items[propertyKey].ReleationType contains the enum operator (Equal, LessThan,etc.) that is used against the property for filling the values in the where clause of the expression. GetEquavalentSqlOperator contains some switch statements that return SQL string operator values based on the enum type.
2. General Select Statement This is pretty simple, in contrast to the WITH statement construction.
builder.Append("SELECT * FROM " + bucket.Name + " ");
CreateWhereClauseIfPossible(bucket, builder);
if (bucket.OrderByClause != null)
{
builder.Append(orderByBuilder.ToString());
}
builder.Append("SELECT * FROM " + bucket.Name + " ");
CreateWhereClauseIfPossible(bucket, builder);
if (bucket.OrderByClause != null)
{
builder.Append(orderByBuilder.ToString());
}
3. Run query and Fill IModify collection. This is done through the following call:
FillObject(builder.ToString(), items, bucket.Items);
FillObject(builder.ToString(), items, bucket.Items);
Inside the method we create a db context and then we execute the query:
DatabaseContext context = new DatabaseContext()
IDataReader reader = context.ExecuteReader(CommandType.Text, sql);
DatabaseContext context = new DatabaseContext()
IDataReader reader = context.ExecuteReader(CommandType.Text, sql);
For each row, we create a new T type object and call its FillProperty to populate each property. Here bItems.Keys (Bucket.Items.Keys) gives a list of property names and bItems[key].Name (Bucket.Items[key].Name) either gives the property name or user defined name representing the entity column(by OriginalNameAttribute).
while (reader.Read())
{
T item = Activator.CreateInstance(typeof(T)) as T;
foreach (string key in bItems.Keys)
{
item.FillProperty(key , reader[bItems[key].Name]);
}
items.Add(item);
// extract data
}
reader.Close();
while (reader.Read())
{
T item = Activator.CreateInstance(typeof(T)) as T;
foreach (string key in bItems.Keys)
{
item.FillProperty(key , reader[bItems[key].Name]);
}
items.Add(item);
// extract data
}
reader.Close();
The final task is to override the 3.b -> AddItem and 3.c - > RemovItem. These are pretty simple as the fetched object is passed by user through context.Add and context.Remove calls. All is needed is to generate the Insert and Delete statement based on T property values.
For the Insert statement, the code looks like:
StringBuilder builder = new StringBuilder();
builder.Append("INSERT INTO " + item.Name); // item == Bucket
builder.Append("\r\n");
builder.Append("VALUES");
builder.Append("(");
BuildClause(item, builder, true); // shown in section 1
builder.Append(")");
ExecuteOnly(builder.ToString());
StringBuilder builder = new StringBuilder();
builder.Append("INSERT INTO " + item.Name); // item == Bucket
builder.Append("\r\n");
builder.Append("VALUES");
builder.Append("(");
BuildClause(item, builder, true); // shown in section 1
builder.Append(")");
ExecuteOnly(builder.ToString());
For the Delete statement, the code looks like:
StringBuilder builder = new
ringBuilder();
builder.Append("DELETE FROM " + item.Name); // item == Bucket
builder.Append("\r\n");
builder.Append("WHERE");
builder.Append(" ");
BuildClause(item, builder, false); // shown in section 1
ExecuteOnly(builder.ToString());
StringBuilder builder = new
StringBuilder();
builder.Append("DELETE FROM " + item.Name); // item == Bucket
builder.Append("\r\n");
builder.Append("WHERE");
builder.Append(" ");
BuildClause(item, builder, false); // shown in section 1
ExecuteOnly(builder.ToString());
Going back to BuildClause, we checked whether the query was an insert or not. We did it because the same routine is reused for insert, delete and select, which generates slightly different SQL for insert statements.
That's it, we are ready to roll. The DataContext class requires a simple config entry, so in the app/web.config file we need to have the following lines:
<configuration>
<configSections>
<section name="customDataConfig" type="OpenLinqToSql.Configuration.OpenLinqDataProviderConfiguration, OpenLinqToSql" />
</configSections>
<customDataConfig provider="System.Data.SqlClient " c/>
<!-- For SqlCe , it looks like -->
<customDataConfig provider="System.Data.SqlServerCe" c/>
</configuration>
<configuration>
<configSections>
<section name="customDataConfig" type="OpenLinqToSql.Configuration.OpenLinqDataProviderConfiguration, OpenLinqToSql" />
</configSections>
<customDataConfig provider="System.Data.SqlClient " c/>
<!-- For SqlCe , it looks like -->
<customDataConfig provider="System.Data.SqlServerCe" c/>
</configuration>
For parsing the configuration, I have created an OpenLinqDataProviderConfiguration class, which loads the settings in the constructor of DataContext, which I left to you to explore.
Summary We have created a sort of custom LinqToSql provider, without using any expression processing and Reflection. We also showed how LinqExtender proves to be useful in this case. It can be used for external source based providers (e.g. Flickr) in the same way. You can take a live preview of that at
www.codeplex.com/linqflickr. Also, don't forget to check out
www.codeplex.com/linqextender to download OpenLinqToSql for a more in-depth look of the LinqExtender in action.