Bring a bit of the Subsonic power to Entity Framework by adding automatic audit and logical delete fields

Here is the code to do the same thing than in my previous article “Bring a bit of the Subsonic power to Linq to sql by adding automatic audit and logical delete fields”


namespace YouNamespace.DAL
{
 using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Data;
 using System.Data.Common;
 using System.Data.Objects;
 using System.Linq;

 public partial class YOURCONTEXTEntities
 {
 /// <summary>
 /// System fields for automatic audit and logical delete
 /// </summary>
 private struct SystemFields
 {
 public const string CreatedOn = "CREATEDON";
 public const string ModifiedOn = "MODIFIEDON";
 public const string CreatedBy = "CREATEDBY";
 public const string ModifiedBy = "MODIFIEDBY";
 public const string IsDeleted = "ISDELETED";
 }

 /// <summary>
 /// Overriding the SaveChanges method to automaticaly set system fields if any.
 /// </summary>
 /// <param name="options"></param>
 /// <returns></returns>
 public override int SaveChanges(System.Data.Objects.SaveOptions options)
 {
 IEnumerable<ObjectStateEntry> newEntries = this.ObjectStateManager.GetObjectStateEntries(EntityState.Added);

 foreach (ObjectStateEntry entry in newEntries)
 {
 ReadOnlyCollection<FieldMetadata> fieldsMetaData = entry.CurrentValues
 .DataRecordInfo.FieldMetadata;

 FieldMetadata createdOnField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.CreatedOn, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (createdOnField.FieldType != null)
 {
 entry.CurrentValues.SetValue(createdOnField.Ordinal, DateTime.Now);
 }

 FieldMetadata createdByField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.CreatedBy, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (createdByField.FieldType != null)
 {
 entry.CurrentValues.SetValue(createdByField.Ordinal, "Sam");
 }

 FieldMetadata deletedField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.IsDeleted, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (deletedField.FieldType != null)
 {
 entry.CurrentValues.SetValue(deletedField.Ordinal, false);
 }
 }

 IEnumerable<ObjectStateEntry> modifiedEntries = this.ObjectStateManager.GetObjectStateEntries(EntityState.Modified);
 foreach (ObjectStateEntry entry in modifiedEntries)
 {
 ReadOnlyCollection<FieldMetadata> fieldsMetaData = entry.CurrentValues
 .DataRecordInfo.FieldMetadata;

 FieldMetadata createdOnField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.ModifiedOn, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (createdOnField.FieldType != null)
 {
 entry.CurrentValues.SetValue(createdOnField.Ordinal, DateTime.Now);
 }

 FieldMetadata createdByField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.ModifiedBy, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (createdByField.FieldType != null)
 {
 entry.CurrentValues.SetValue(createdByField.Ordinal, "Sam");
 }
 }

 IEnumerable<ObjectStateEntry> deletedEntries = this.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted);
 foreach (ObjectStateEntry entry in deletedEntries)
 {
 // change from deleted to modified (!important)
 this.ObjectStateManager.ChangeObjectState(entry.Entity, EntityState.Modified);

 ReadOnlyCollection<FieldMetadata> fieldsMetaData = entry.CurrentValues
 .DataRecordInfo.FieldMetadata;

 FieldMetadata deletedField = fieldsMetaData
 .Where(f => string.Equals(f.FieldType.Name, SystemFields.IsDeleted, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();

 if (deletedField.FieldType != null)
 {
 entry.CurrentValues.SetValue(deletedField.Ordinal, true);
 }
else
 {
 // change back from modified to deleted (!important)
 this.ObjectStateManager.ChangeObjectState(entry.Entity, EntityState.Deleted);
 }
 }

 return base.SaveChanges(options);
 }
 }
}

Incoming search terms:

  • entry currentvalues
  • subsonic audit
  • subsonic createdon
  • subsonic entity framework
  • subsonic Leave a Reply comment -captcha

Bring a bit of the Subsonic power to Linq to sql by adding automatic audit and logical delete fields

[Update]
You cand find the code to do the same with entity framework here
[/Update]

Recently I had to work on a project using Linq to sql  and I was suprised to see that Linq to SQL has no built-in auditing ability.

There is no support for logical deletion neither.

With the SubSonic’s ActiveReccord, all these features are built in (see the documentation page).

So, I decided to add this ability for the project, because I doesn’t want to change all the existing request to add these informations.

In my BLL layer, I have a manager object for my DAL Linq objects, so by Example if I have an “User” Object in my DAL layer, I have an “UserManager” object in my BLL layer.

All my managers objects inherit a “ManagerBase” Object, which contains only a “DataContext” Property, which can be used in the child classes.

So my first approach was to add a “SubmitChanges”  method in that class and call it from my child classes instead of directly call the one from the DataContext.

In my SubmitChanges method I add the needed logic before calling the one from the DataContext.

But I realized that when there are more than one developper working on an application, it can be difficult to ensure that everybody use the same technique. A person may not use the base class, another may forget to use the base class method, or other things..

Then I investigated the DataContext object with Reflector, and I noticed that the SubmitChanges(void) method directly call the SubmitChanges(ConflictMode) one


public void SubmitChanges()
{
 this.CheckDispose();
 this.SubmitChanges(ConflictMode.FailOnFirstConflict);
}

And if I take a look at the generated DataContext, I see that the DataContext is a partial class


public partial class MyContext: System.Data.Linq.DataContext
{
 // ...
}

So, I decided to add a new file in my DAL layer and extend the DataContext

partial class MyContext
{
// ...
}

Then override the SubmitChanges(ConflictMode) DataContext method


public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
 {
SetSystemFields(); // notice this method call !
base.SubmitChanges(failureMode);
 }

In the SetSystemFields method, I retrieve the pending changes


private void SetSystemFields()
 {
ChangeSet changeset = this.GetChangeSet();

// ...
}

Then I retrieve the user identity in order to be able to set the CreatedBy and ModifiedBy properties of my data objects


System.Security.Principal.WindowsIdentity identity = System.Security.Principal.WindowsIdentity.GetCurrent();
 string userName = "Unknow";
 if (identity != null)
 {
userName = identity.Name;
 }

Then I loop over the objects that have to be inserted


int maxInserts = changeset.Inserts.Count;
for (int i = 0; i < maxInserts; i++)
{

// ...

}

and I use reflection to get the audit properties and set their values


object entity = changeset.Inserts[i];
 Type objectType = entity.GetType();
 PropertyInfo[] properties = objectType.GetProperties();

foreach (PropertyInfo property in properties)
 {
switch (property.Name.ToUpper())
{
case SystemFields.CreatedOn:
case SystemFields.ModifiedOn:
{
property.SetValue(entity, DateTime.Now, null);
} break;
case SystemFields.CreatedBy:
case SystemFields.ModifiedBy:
{
property.SetValue(entity, userName, null);
} break;
case SystemFields.IsDeleted:
{
property.SetValue(entity, false, null);
} break;
default:
break;
}
 }

Remark : “SystemFields” is a struct I defined with the audit column names but you may as well make it come from a config file.


private struct SystemFields
{
 public const string CreatedOn = "CREATEDON";
 public const string ModifiedOn = "MODIFIEDON";
 public const string CreatedBy = "CREATEDBY";
 public const string ModifiedBy = "MODIFIEDBY";
 public const string IsDeleted = "ISDELETED";
}

I do the same treatment for the update and deletes (if I want to enable them).

Now everytime that the DataContext’s SubmitChanges method is called, the audit fields are automaticaly set!

Here is the full partial class for the DataContext

using System;
using System.Data.Linq;
using System.Reflection;

namespace MyNamespace.DAL
{
 partial class MyContext
 {
  /// <summary>
  /// system fields for automatic audit and logical delete
  /// </summary>
  private struct SystemFields
   {
        public const string CreatedOn = "CREATEDON";
        public const string ModifiedOn = "MODIFIEDON";
        public const string CreatedBy = "CREATEDBY";
        public const string ModifiedBy = "MODIFIEDBY";
        public const string IsDeleted = "ISDELETED";
  }

public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
{

SetSystemFields();

base.SubmitChanges(failureMode);
}

private void SetSystemFields()
{
ChangeSet changeset = this.GetChangeSet();

// retrieve the user name
System.Security.Principal.WindowsIdentity identity = System.Security.Principal.WindowsIdentity.GetCurrent();
string userName = "Unknow";
if (identity != null)
{
userName = identity.Name;
}

// insert
int maxInserts = changeset.Inserts.Count;
for (int i = 0; i < maxInserts; i++)
{
object entity = changeset.Inserts[i];
Type objectType = entity.GetType();
PropertyInfo[] properties = objectType.GetProperties();
foreach (PropertyInfo property in properties)
{
switch (property.Name.ToUpper())
{
case SystemFields.CreatedOn:
case SystemFields.ModifiedOn:
{
property.SetValue(entity, DateTime.Now, null);
} break;
case SystemFields.CreatedBy:
case SystemFields.ModifiedBy:
{
property.SetValue(entity, userName, null);
} break;
case SystemFields.IsDeleted:
{
property.SetValue(entity, false, null);
} break;
default:
break;
}
}
}

// update
int maxUpdates = changeset.Updates.Count;
for (int i = 0; i < maxUpdates; i++)
{
object entity = changeset.Updates[i];
Type objectType = entity.GetType();
PropertyInfo[] properties = objectType.GetProperties();
foreach (PropertyInfo property in properties)
{
switch (property.Name.ToUpper())
{
case SystemFields.ModifiedOn:
{
property.SetValue(entity, DateTime.Now, null);
} break;
case SystemFields.ModifiedBy:
{
property.SetValue(entity, userName, null);
} break;
default:
break;
}
}
}

// deletes (logical deletes)
int maxDeletes = changeset.Deletes.Count;
for (int i = 0; i < maxDeletes; i++)
{
object entity = changeset.Deletes[i];
Type objectType = entity.GetType();
PropertyInfo[] properties = objectType.GetProperties();
foreach (PropertyInfo property in properties)
{
switch (property.Name.ToUpper())
{
case SystemFields.ModifiedOn:
{
property.SetValue(entity, DateTime.Now, null);
} break;
case SystemFields.ModifiedBy:
{
property.SetValue(entity, userName, null);
} break;
case SystemFields.IsDeleted:
{
property.SetValue(entity, true, null);
} break;
default:
break;
}
}
}
}
}
}

Incoming search terms:

  • linq audit inserts

I want my foreign key to be mapped to a single object and not a collection

Say you have two tables:  Teams and Registrations

Registration has a foreign key for Teams :

image

In Subsonic 3, the Registration class will have a property called “Beach_Teams” which returns a IQueryable<Beach_Team> collection.

If we look at the generate class in the activerecord.cs file, there is a “Foreign Keys” region which contains the Beach_Teams property:


 #region ' Foreign Keys '
 public IQueryable<Beach_Team> Beach_Teams
 {
   get
   {
     var repo=LJR.DAL.Activities.Beach.Beach_Team.GetRepo();
     return from items in repo.GetAll()
          where items.TeamID == _TeamIDfk
          select items;
   }
 }

#endregion

It’s not ok for my use, I want a property returning only one “Beach_Team” object, so I can use it in my presentation layer with databound controls with no efforts

<asp:ListView runat="server" ID="LastTeams" DataSourceID="registrationsOds">
   <ItemTemplate>
     <%# Eval("Beach_Team.Name")%>
   </ItemTemplate>
 </asp:ListView>

So, what can I do ?

I can edit the ActiveRecord.cs file and add a property called “Beach_Team” and returning a Beach_Team object

 public Beach_Team Beach_Team
 {
   get
   {
     var repo = LJR.DAL.Activities.Beach.Beach_Team.GetRepo();
    return repo.GetByKey(_TeamIDfk);
   }
 }

It do the job !

But if I want to regenerate the ActiveRecord.cs file, my modification will be lost !

So the good solution is to modify the T4 template to generate this property.

Let’s do this :

Go to the ActiveRecord.tt file and search for ‘ Foreign Keys ‘.

Once you have find it look how it works:


#region ' Foreign Keys '
<#
 List<string> fkCreated = new List<string>();
 foreach(FKTable fk in tbl.FKTables)
 {
   if(IsTableOkToBeIncluded(fk.OtherTable))
   {
    string propName=fk.OtherQueryable;
    if(fkCreated.Contains(propName))
    {
      propName=fk.OtherQueryable+fkCreated.Count.ToString();
    }
    fkCreated.Add(fk.OtherQueryable);
#>
   public IQueryable<<#=fk.OtherClass #>> <#=propName #>
  {
  get
  {
     var repo=<#=Namespace #>.<#=fk.OtherClass#>.GetRepo();
     return from items in repo.GetAll()
         where items.<#=CleanUp(fk.OtherColumn)#> == _<#=CleanUp(fk.ThisColumn)#>
         select items;
   }
 }

<#
 }
 }
#>
 #endregion

In a nutshell, for each tables linked to the table we are on, we check if the access code to the table has to be generated (see my previous article : Subsonic : Specify the tables you really need ! ).

If the code has to be generated, we create a string variable named “propName” which will be the name of the property.

If this property already exists, we suffix the name to be sure we don’t have the same property twice.

Then we generate the IQueryable property.

Now, the first thing we want is to have the property name equals to the object name.

We could think that we don’t have to twist our minds because  we already have it with “fk.OtherClass” and i could be like that

 public <#=fk.OtherClass #> <#=fk.OtherClass #>
 {
  get
  {
   var repo=<#=Namespace #>.<#=fk.OtherClass#>.GetRepo();
   return repo.GetByKey(_<#=CleanUp(fk.ThisColumn)#>);
  }
 }

But what if we have two foreign keys to the same table ? We will have the same property twice! and we have to avoid it!

So, we have to check if this property isn’t already generated.

To do this, we can use the list “fkCreated” already present in the code, and create a “secondPropName” string variable, assign it the value of the OtherClass and check if it’s not already created :

 string secondPropName=fk.OtherClass;
 if(fkCreated.Contains(secondPropName))
 {
   secondPropName+=fkCreated.Count.ToString();
 }
 fkCreated.Add(secondPropName);

There is the final code


#region ' Foreign Keys '
<#
 List<string> fkCreated = new List<string>();
 foreach(FKTable fk in tbl.FKTables)
 {
   if(IsTableOkToBeIncluded(fk.OtherTable))
   {
    string propName=fk.OtherQueryable;
    if(fkCreated.Contains(propName))
    {
     propName=fk.OtherQueryable+fkCreated.Count.ToString();
    }
   fkCreated.Add(fk.OtherQueryable);

   string secondPropName=fk.OtherClass;
   if(fkCreated.Contains(secondPropName))
   {
     secondPropName+=fkCreated.Count.ToString();
   }
   fkCreated.Add(secondPropName);
#>
  public IQueryable<<#=fk.OtherClass #>> <#=propName #>
  {
    get
    {
      var repo=<#=Namespace #>.<#=fk.OtherClass#>.GetRepo();
      return from items in repo.GetAll()
        where items.<#=CleanUp(fk.OtherColumn)#> == _<#=CleanUp(fk.ThisColumn)#>
        select items;
    }
  }

  public <#=fk.OtherClass #> <#=secondPropName #>
  {
    get
    {
      var repo=<#=Namespace #>.<#=fk.OtherClass#>.GetRepo();
      return repo.GetByKey(_<#=CleanUp(fk.ThisColumn)#>);
    }
  }

<#
 }
 }
#>
 #endregion

Now I can use the linked object directly !

You can donwload my T4 templates here.

Subsonic : Specify the tables you really need !

With the Subsonic’s T4 Templates you can specify the tables you don’t want the access code to be generated.

But what if you want to choose the tables for which the mapping classes will be created ?

Actually you can have many reasons to do that : having an application using only specific tables from a big system or having the ability to separate distinct parts of a system by example.

In my case I wanted to use only three tables from my database to build an ASP.NET membership provider. This is the example I’ll use for this article.

So, in my database I have at least three tables :

  • Users
  • Roles
  • UsersInRoles

Now I want to generate the DAL layer only for these tables, so I create a new code library project, a add an app.config file containing the connection string and add the Subsonic.Core reference.

The I have to add my T4 template, but they are not ready. I have to modify them to allow the selection of my tables.

To do that, I go to the T4 template directory and edit the “Settings.ttinclude” and the “Context.tt” files :

T4TemplatesDirectory

First, check how it works

In the “Settings.ttinclude” file, line #30 we see the definition of the tables to be excluded:


//this is a list of tables you don't want generated
string[] ExcludeTables = new string[]{
"sysdiagrams",
"BuildVersion"
};

In the “Context.tt” file, il we search for “ExcludeTables”, we find three part of code using it in the same way:

// line 135

<#  foreach(Table tbl in tables){
if(!ExcludeTables.Contains(tbl.Name))
{

// ...

// line 251

foreach(Table tbl in tables)
{
if(!ExcludeTables.Contains(tbl.Name))
{

// ...

// line 271

foreach(Table tbl in tables)
{
if(!ExcludeTables.Contains(tbl.Name))
{

// ....

Now that we know how it’s working, we can modify the T4 templates to add the ability to choose the tables we want

The simplest option would be to replace the “ExcludeTables” array by “IncludeTables” and to remove the logical NOT operator (!) in the conditions and the job will be done.

But if we think a bit, we realize that there is many ways to choose the tables we want.
We can decide to choose :

  • All tables
  • All tables except the specified ones
  • Only the specified tables
  • All tables matching a pattern (example : starting by “MyApplication_”)
  • An other choice method

My solution :

In the “Settings.ttinclude” file:

Add a string array named “IncludeTables” containing the tables you want :


// this is the list of the tables you want to be generated
string[] IncludeTables = new string[]{
"Users",
"Roles",
"UsersInRoles"
};

Add a string variable named TablePrefix

 // the prefix of the table you want to be generated
 string TablePrefix="";

Add an enumeration named “TableChoiceMethod” and containing the different ways to choose the tables:


enum TableChoiceMethod
 {
SpecifiedExcludes,
SpecifiedIncludes,
SpecifiedPrefix,
All
 }

Add a variable for this enum, set to the value you want


// define the table choice method
 TableChoiceMethod tableChoiceMethod = TableChoiceMethod.SpecifiedIncludes;

Create  a ““IsTableOkToBeIncluded” method taking care of the choice we made in the activerecord file and returning whether or not the table is ok to be generated.


bool IsTableOkToBeIncluded(string tableName)
{
 switch(tableChoiceMethod)
 {
  case TableChoiceMethod.SpecifiedIncludes:
  {
   return IncludeTables.Contains(tableName);
  }
  case TableChoiceMethod.SpecifiedExcludes:
  {
   return !ExcludeTables.Contains(tableName);
  }
  case TableChoiceMethod.SpecifiedPrefix:
  {
  return tableName.StartsWith(TablePrefix, StringComparison.InvariantCultureIgnoreCase);
  }
  default: // assume TableChoiceMethod.All
  {
   return true;
  }
 }
}

In the Context.tt, ActiveRecord.tt and Struct.tt files :

Replace the condition”if(!ExcludeTables.Contains(tbl.Name))” by this method


<#  foreach(Table tbl in tables){
if(IsTableOkToBeIncluded(tbl.Name))
{

// ...

Now specify the Namespace, ConnectionStringName and DatabaseName in the Settings.ttinclude file.

Add the template to your DAL project,

Generate

You are done !

You can download my templates here.

Remarks :

  • The Subsonic version I use is 3.0.0.4.
  • I’m running Windows 7 64 bits and I had  unblock the files in windows explorer.
  • I had to run Visual Studio as an administrator.
  • If you are not familiar with subsonic, I invite you to watch the 5 minute demo and read the startup tutorial from the subsonicproject website.

Incoming search terms:

  • subsonic 3 0 tablename s

Custom paging with Subsonic 3 and ObjectDataSource in ASP.NET

Here is how I created a paged items list in ASP.NET 3.5 using Subsonic for data access, and ASP.NET Controls for the display.

I use Subsonic 3.0.0.4 with ActiveRecord and ASP.NET 3.5 SP1 webforms.

For this example, we are creating a blog post list and that the DAL is created (the data class we use is “DAL.BlogPost”).

The DAL.BlogPost object has three properties we need : CreatedOn, Subject and Body.

First step : prepare your data manager class


namespace MyBlog.BLL
{
   public class PostManager
   {
     // ...
   }
}

(keep in mind the namespace and the class for the nexts steps)

In this class, two methods are required,

The first returns the total number of items


public int GetPostCount()
{
  return DAL.BlogPost.All().Count(x => x.IsDeleted != true);
}

The second one returns the items contained in a defined range (start index and then nomber of elements), so this method needs to know where to start and how many items to get.


public PagedList<DAL.BlogPost> GetLastestPosts(int startRowIndex, int maximumRows)
{
   if (startRowIndex != 0)
   {
     startRowIndex =(startRowIndex/maximumRows);
   }

    IOrderedQueryable<DAL.BlogPost> yourQuery = DAL.BlogPost.All().OrderByDescending(x => x.CreatedOn);
    return new PagedList<DAL.BlogPost>(yourQuery, startRowIndex, maximumRows);
}

One important part to remember when using Subsonic is


if (startRowIndex != 0)
{
   startRowIndex = startRowIndex / maximumRows;
}

This conversion is necessary because subsonic and the ObjectDataSource doesn’t share the same opinion about the paging method.
Subsonic says

“I want the third page containing five elements”

ObjectDataSource says

“I want the five elements following the third element”

So the conversion is mandatory if you want a correct paging.

Second step : create the aspx page.

Declare an ObjectDataSource:

<asp:ObjectDataSource runat="server" ID="postsDataSource"
 TypeName="Example.BLL.PostManager"
 SelectMethod="GetLastestPosts"
 EnablePaging="true"
 SelectCountMethod="GetPostCount" >
 </asp:ObjectDataSource>

Note the TypeName attribute : the value is the namespace + the class name of our datamanager class.
We set the EnablePaging attribute to true and we specify the SelectCountMethod and the SelectMethod.

We don’t need any parameters for the paging, by default the MaximumRowsParameterName and StartRowIndexParameterName are set to “maximumRows” and “startRowIndex”.
If we want other parameters names in our datamanager class, we can define them in the ObjectDataSource with these two parameters.

Add an asp ListView element

<asp:ListView runat="server" ID="postsListview"
     DataSourceID="postsDataSource">
 <LayoutTemplate>
    <asp:PlaceHolder ID="itemPlaceHolder"
         runat="server" />
 </LayoutTemplate>
 <ItemTemplate>
    <h2><%# Eval("CreatedOn","{0:d}")%> :
     <%# Eval("Subject") %></h2>
    <br />
    <p>
     <%# Eval("Body") %>
    </p>
    <hr />
 </ItemTemplate>
</asp:ListView>

Set the DataSourceID attribute to the ObjectDataSource ID parameter.

Define a LayoutTemplate and an ItemTemplate with the elements we want to display.

Then add an asp datapager control in the LayoutTemplate


<LayoutTemplate>

  <asp:DataPager ID="postsDataPager" runat="server"
       PageSize="3">
    <Fields>
      <asp:NextPreviousPagerField ButtonType="Link"
           FirstPageText="<<" PreviousPageText="<"
           ShowNextPageButton="false"
           ShowFirstPageButton="true" />
      <asp:NumericPagerField PreviousPageText="..."
           NextPageText="..." ButtonCount="10" />
      <asp:NextPreviousPagerField ButtonType="Link"
           LastPageText=">>" NextPageText=">"
           ShowPreviousPageButton="false"
           ShowLastPageButton="true" />
     </Fields>
   </asp:DataPager>

   <asp:PlaceHolder ID="itemPlaceHolder" runat="server" />
 </LayoutTemplate>

Here is the result :
blogposts

Remark about the data access method

It’s possible to avoid the start index value conversion !

Check the PagedList first constructor code (code is available on the subsonic git repository)


public PagedList(IQueryable<T> source, int index, int pageSize)
{
   int total = source.Count();
   TotalCount = total;
   TotalPages = total / pageSize;

   if(total % pageSize > 0)
     TotalPages++;

   PageSize = pageSize;
   PageIndex = index;
   AddRange(source.Skip(index * pageSize).Take(pageSize).ToList());
}

As you can see, Subsonic use the Skip and the Take methods from Linq

So, you can modify the GetLastestPosts method to return a strongly typed List in place of the PagedList:


public List<DAL.BlogPost> GetLastestPosts(int startRowIndex, int maximumRows)
{
   return DAL.BlogPost.All().
          Skip(startRowIndex).
          Take(maximumRows).
          OrderByDescending(x => x.CreatedOn).
          ToList<DAL.BlogPost>();
}

With this method, no more conversion needed

Incoming search terms:

  • how to use subsonic in visual studio 2015