Showing posts with label Entity Framework. Show all posts
Showing posts with label Entity Framework. Show all posts

Monday, 13 February 2012

Basic Auditing for Dynamic Data with Entity Framework 4.x

This is my first article of 2012 and I thought I had published an article on this previously but apparently not Crying face so I will now rectify that oversight.

The first this we need out audit fields, these are added to every entity we need to audit, next we need an Interface to allow us to fine entities with Audit fields.

AuditFields

Figure 1 – Audit Fields

public interface IAuditable
{
String CreatedByUserID { get; set; }
String CreatedDateTime { get; set; }
String UpdatedByUserID { get; set; }
String UpdatedDateTime { get; set; }
}

Listing 1 – IAuditable interface

We then need to add this to each entity that will be audited, this is just applied to you metadata classes on the Partial Class NOT the buddy class.

[MetadataType(typeof(AppAlterMetadata))]
public partial class AppAlter : IAuditable
{
internal class AppAlterMetadata
{
public Object ID { get; set; }

// other field deleted for brevety

// auditing fields
public Object CreatedByUserID { get; set; }
public Object CreatedDateTime { get; set; }
public Object UpdatedByUserID { get; set; }
public Object UpdatedDateTime { get; set; }
}
}

Listing 2 – the interface applied to each entity that requires auditing

Now for the code that does the auditing automatically,

public partial class MyEntities
{
/// <summary>
/// Called when [context created].
/// </summary>
partial void OnContextCreated()
{
// Register the handler for the SavingChanges event.
this.SavingChanges += new EventHandler(context_SavingChanges);
}


/// <summary>
/// Handles the SavingChanges event of the context control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private static void context_SavingChanges(object sender, EventArgs e)
{
var objects = ((ObjectContext)sender).ObjectStateManager;

// handle auditing
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Added));
AuditingHelperUtility.ProcessAuditFields(objects.GetObjectStateEntries(EntityState.Modified), InsertMode: false);
}


/// <summary>
/// Auditing helper utility class
/// </summary>
internal static class AuditingHelperUtility
{
internal static void ProcessAuditFields(IEnumerable<Object> list, bool InsertMode = true)
{
foreach (ObjectStateEntry item in list)
{
var appUserID = GetUserId();
// deal with insert and update entities
var auditEntity = item.Entity as IAuditable;
if (auditEntity != null)
{

if (InsertMode)
{
auditEntity.CreatedByUserID = appUserID;
auditEntity.CreatedDateTime = DateTime.Now;
}

auditEntity.UpdatedByUserID = appUserID;
auditEntity.UpdatedDateTime = DateTime.Now;
}

}
}
}


public static String GetUserId()
{
return System.Web.HttpContext.Current.User.Identity.Name;
}
}

Listing 3 – the Audit code

Lets break this down into three sections

Section 1

Here we wire-up the SavingChanges handler in the OnContextCreated() partial method to do this we first need to create a partial class from out entities for you look in the EDMX code behind file you will see something like this;

EFClasses

Figure 2 – Entities classes

so we add a new class to the the project, make sure it has the same namespace as the EDMX code behind file (this is pretty much the same as for out metadata classes) and then we add the partial class same as the MyEntities (this will be the name you gave it when creating but it is there in the code behind you can’t miss it) class, see Listing 3.

The method is wired up with this line of code:

this.SavingChanges += new EventHandler(context_SavingChanges);

Section 2

Now in the context_SavingChanges method we simply get the ObjectStateManager  which has all the objects that are being added, updated and deleted, here we are only interested in the Added and Modified items. All we do is call our helper with each collection of objects.

Section 3

Looking at Listing 3 you will see the AuditingHelperUtility and it’s ProcessAuditFields method, here we first of all cast the each entity to the IAuditable interface and check for null if it isn't then set the appropriate properties and exit.

Finally

This can be expanded to cover many different requirements, I have maintained a separate audit table using this method with the addition of a little reflection.

Saturday, 24 July 2010

Using Entity Framework from a different DLL in Dynamic Data

I just thought I should document what we learned in this thread here Entity Framework: Entity Model in a separated DLL first of all it is easy to do in Dynamic Data 4 and needs a fix on each page for Dynamic Data 1 (this version that shipped with Visual Studio 2008 SP1 and .Net 3.5 SP1)

Note: see also Rick Andersons blog post here Explicit connection string for EF and L2S

Adding Model from DLL or Class Library

Sample Project

Figure 1 – sample project with Model in Class Library

To use the Northwind Model in our project we need to get a new instance of the Northwind context, to do this we could add the code to instantiate it in the Glabal.asax.cs file. But for me the object of having it in an external class library/DLL is to keep it reusable, so I had the idea of building the connection string in the external class library rather than locally. We add Listing 1 to the class library, as a partial class of the NorthwindEntities class, pass-in the server name (we could pass-in more but the server name will do here).

public partial class NorthwindEntities
{
    /// <summary>
    /// Gets a new NorthwindEntities object.
    /// </summary>
    public static ObjectContext GetContext(String server, String database)
    {
        EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder();
        entityBuilder.Provider = "System.Data.SqlClient";
        entityBuilder.ProviderConnectionString =
            String.Format("server={0};database={1};Integrated Security=SSPI;MultipleActiveResultSets=True",
            server,
            database);
        entityBuilder.Metadata = @"res://*";
        var objContext = new NorthwindContext.NorthwindEntities(entityBuilder.ToString());

        return objContext;
    }
}

Listing 1 – GetContext helper method

So now all we need to add this to our Dynamic Data app is use this in the RegisterContext method in the Global.asax.cs

DefaultModel.RegisterContext(() => NorthwindContext.NorthwindEntities.GetContext("aragorn","Northwind"),
            new ContextConfiguration()
            {
                ScaffoldAllTables = true
            });

Listing 2 – Using the GetContext helper

Fixing it up to work in Original version of Dynamic Data

This work beautifully in Dynamic Data 4, but not in the previous release Sadwe need the fix mentioned in Rick Andersons post

GridDataSource.ContextCreating += delegate(object ceSender,
    EntityDataSourceContextCreatingEventArgs ceArgs)
    {
        ceArgs.Context = (ObjectContext)table.CreateContext();
    };

Listing 3 – Fix for Dynamic Data 1

Added to the List page it looks like this:

protected void Page_Init(object sender, EventArgs e)
{
    DynamicDataManager1.RegisterControl(GridView1, true /*setSelectionFromUrl*/);
    GridDataSource.ContextCreating += delegate(object ceSender,
        EntityDataSourceContextCreatingEventArgs ceArgs)
        {
            ceArgs.Context = (ObjectContext)table.CreateContext();
        };
}

Listing 4 – Added to List page template

You have to add this to each page template and add it twice on the ListDetails page template.

Tuesday, 20 July 2010

Using Entity Framework Code First (CTP4) in Dynamic Data

Following the sample EF CTP4 Walkthrough: Productivity Improvements you end up with a sample DbContext and of course I immediately thought of using this in Dynamic Data using the “ASP.NET Dynamic Data Entities Web Application” template (Figure 1),

Select ASP.NET Dynamic Data Entities Web Application

Figure 1 - Select ASP.NET Dynamic Data Entities Web Application

but I got the following error:

Error when using DbContext with DD4 EF Template.

Figure 2 – Error when using DbContext with DD4 EF Template.

This my my default code Listing 1

DefaultModel.RegisterContext(typeof(Models.ProductCatalog),
    new ContextConfiguration()
    {
        ScaffoldAllTables = true
    });

Listing 1 – my default DbContext registration

So I fired off an e-mail to Microsoft and was told “the guideline will be to use DD with DomainService, which will have full support of DbContext in the future” although that wont work out of the box at the moment so, I gave upBlushing

Then Diego Vega posted on twitter a link to Rowan Millers blog EF CTP4 Tips & Tricks: WCF Data Service on DbContext this turned out to be very useful, he talks about getting the underlying ObjectContext see Listing 2.

public class ProductCatalog : DbContext
{
    public ProductCatalog(String connectionString)
    {
        // set default connection string
        this.Database.Connection.ConnectionString = connectionString;
    }

    public ObjectContext UnderlyingContext
    {
        get { return this.ObjectContext; }
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
}

public class Category
{
    public string CategoryId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public string CategoryId { get; set; }

    public virtual Category Category { get; set; }
}

Listing 2 – my ProductCatalog

In Listing 2 I use this feature to get the underlying ObjectContext (Also note my setting the connection string in the constructor). So now we have a cool way of getting Entity Framework Code First CTP4 working with Dynamic Data 4. This makes me very happyHappy Wizzard

So you will see that all we need to do to get this working with Dynamic Data now is to replace the

typeof(Models.ProductCatalog)

with

() => new Models.ProductCatalog(connectionString).UnderlyingContext

in the RegisterContext method to get at the underlying ObjectContext, which now leads us to Listing 3 .

public static void RegisterRoutes(RouteCollection routes)
{
    var connectionString = "Data Source=aragorn;Initial Catalog=ProductCatalog;Integrated Security=True";
    DefaultModel.RegisterContext(() => new Models.ProductCatalog(connectionString).UnderlyingContext,
        new ContextConfiguration()
        {
            ScaffoldAllTables = true
        });

    routes.Add(new DynamicDataRoute("{table}/{action}.aspx")
    {
        Constraints = new RouteValueDictionary(new { action = "List|Details|Edit|Insert" }),
        Model = DefaultModel
    });
}

Listing 3 – Global.asax RegisterRoutes method.

Now we can Edit, Update and Insert using Entity Framework Code First feature in CTP4.

Download

Nice: One of my favourite things about this is deployment of the DB is that if the DB does not exist on the server specified in the connection string the Entity Framework CTP4 will create it for you (or if you did not specify a connection string it will try to create it in the local SQL Express instance.
!Important: CTP5 make some minor API changes see Dabid Ebbo’s article here Using Dynamic Data with EF Code First and NuGet

Thursday, 15 July 2010

EF CTP4 Released!

Entity Framework Code First CTP4 is now available see following links:

And a quick note from my testing:

public class ProductCatalog : DbContext
{
    public ProductCatalog()
    {
        this.Database.Connection.ConnectionString =
           "Data Source=.;Initial Catalog=ProductCatalog;Integrated Security=True";
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
}

Above is from the first sample you create in VS2010 with EF Code First, and a quick way to create your DB on a different server than on localhost\SQLEXPRESS thought this is super cool.

Happy Wizard

Sunday, 13 June 2010

Securing Dynamic Data 4 (Replay)

This is an updated version of the series Securing Dynamic Data Preview 4 from July 2009 here I playnto streamline the class libraries for the RTM version of Dynamic Data 4  and Visual Studio 2010.

This version is mostly the same as in Part 1 except I’ve done a great deal of refactoring and so I will list everything again here. The main difference is that there are now no user controls to replace the Delete buttons. Also I have changed the permissions system to be restrictive by default at Table level i.e. you must have a permission set on every table for the table to be seen, but a Column level you deny columns you don’t want to be seen.

Permissions Enums

The TableActions (renamed from TableDeny) enum Listing 1 has had a CombinedActions class Listing 2 added that combine sets of TableActions into logical security groups (i.e. ReadOnly equates to combining TablesActions Details and List to give an more descriptive was of assigning rights to a security Role).

/// <summary>
/// Table permissions enum, allows different
/// levels of permission to be set for each 
/// table on a per role bassis.
/// </summary>
[Flags]
public enum TableActions
{
    /// <summary>
    /// Default no permissions
    /// </summary>
    None = 0x00,
    /// <summary>
    /// Details page
    /// </summary>
    Details = 0x01,
    /// <summary>
    /// List page
    /// </summary>
    List = 0x02,
    /// <summary>
    /// Edit page
    /// </summary>
    Edit = 0x04,
    /// <summary>
    /// Insert page
    /// </summary>
    Insert = 0x08,
    /// <summary>
    /// Delete operations
    /// </summary>
    Delete = 0x10,
}

Listing 1 – TableActions

/// <summary>
/// Combines Table permissions enums
/// into logical security groups
/// i.e. ReadOnly combines TableActions
/// Details and List
/// </summary>
public static class CombinedActions
{
    /// <summary>
    /// Read Only access 
    /// TableActions.Details or 
    /// TableActions.List
    /// </summary>
    public const TableActions ReadOnly = 
        TableActions.Details | 
        TableActions.List;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or
    /// TableActions.Edit
    /// </summary>
    public const TableActions ReadWrite = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit;
    /// <summary>
    /// Read Insert access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert
    /// 
    /// </summary>
    public const TableActions ReadInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert;
    /// <summary>
    /// Read Insert and Delete access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Insert or 
    /// TableActions.Delete)
    /// </summary>
    public const TableActions ReadInsertDelete = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Insert | 
        TableActions.Delete;
    /// <summary>
    /// Read and Write access 
    /// TableActions.Details or 
    /// TableActions.List or 
    /// TableActions.Edit or 
    /// TableActions.Insert)
    /// </summary>
    public const TableActions ReadWriteInsert = 
        TableActions.Details | 
        TableActions.List | 
        TableActions.Edit | 
        TableActions.Insert;
    /// <summary>
    /// Full access 
    /// TableActions.Delete or
    /// TableActions.Details or 
    /// TableActions.Edit or 
    /// TableActions.Insert or 
    /// TableActions.List)
    /// </summary>
    public const TableActions Full = 
        TableActions.Delete | 
        TableActions.Details | 
        TableActions.Edit | 
        TableActions.Insert | 
        TableActions.List;
}

Listing 2 – CombinedActions

ColumnActions Listing 3  are used to deny either Write or Read access.

/// <summary>
/// Actions a Column can 
/// have assigned to itself.
/// </summary>
[Flags]
public enum ColumnActions
{
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyRead = 1,
    /// <summary>
    /// Action on a column/property
    /// </summary>
    DenyWrite = 2,
}

Listing 3 – ColumnActions

Secure Dynamic Data Route Handler

The SecureDynamicDataRouteHandler has changed very little since the original article all I have added is the catch all tp.Permission == CombinedActions.Full in the if statement to streamline the code.

/// <summary>
/// The SecureDynamicDataRouteHandler enables the 
/// user to access a table based on the following:
/// the Roles and TableDeny values assigned to 
/// the SecureTableAttribute.
/// </summary>
public class SecureDynamicDataRouteHandler : DynamicDataRouteHandler
{
    /// <summary>
    /// Creates the handler.
    /// </summary>
    /// <param name="route">The route.</param>
    /// <param name="table">The table.</param>
    /// <param name="action">The action.</param>
    /// <returns>An IHttpHandler</returns>
    public override IHttpHandler CreateHandler(
        DynamicDataRoute route,
        MetaTable table,
        string action)
    {
        var httpContext = HttpContext.Current;
        if (httpContext != null && httpContext.User != null)
        {
            var usersRoles = Roles.GetRolesForUser(httpContext.User.Identity.Name);
            var tablePermissions = table.Attributes.OfType<SecureTableAttribute>();

            // if no permission exist then full access is granted
            if (tablePermissions.Count() == 0)
                return null;

            foreach (var tp in tablePermissions)
            {
                if (tp.HasAnyRole(usersRoles))
                {
                    // if no action is allowed return no route
                    var tpAction = tp.Permission.ToString().Split(new char[] { ',', ' ' }, 
                        StringSplitOptions.RemoveEmptyEntries);

                    if (tp.Permission == CombinedActions.Full || tpAction.Contains(action))
                        return base.CreateHandler(route, table, action);
                }
            }
        }
        return null;
    }
}

Listing 4 – Secure Dynamic Data Route Handler

This then covers all Edit, Insert and Details actions but not Delete.

Delete Actions

In the previous article we had a User Control that handled securing the Delete action, here we have a SecureLinkButton. All we do is override the Render method and test to see if the button is disabled via the users security roles.

/// <summary>
/// Secures the link button when used for delete actions
/// </summary>
public class SecureLinkButton : LinkButton
{
    private const String DISABLED_NAMES = "SecureLinkButtonDeleteCommandNames";
    private String[] delete = new String[] { "delete" };

    /// <summary>
    /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
    /// </summary>
    /// <param name="e">
    /// An <see cref="T:System.EventArgs"/> 
    /// object that contains the event data.
    /// </param>
    protected override void OnInit(EventArgs e)
    {
        if (ConfigurationManager.AppSettings.AllKeys.Contains(DISABLED_NAMES))
            delete = ConfigurationManager.AppSettings[DISABLED_NAMES]
                .ToLower()
                .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        base.OnInit(e);
    }

    /// <summary>
    /// Renders the control to the specified HTML writer.
    /// </summary>
    /// <param name="writer">
    /// The <see cref="T:System.Web.UI.HtmlTextWriter"/> 
    /// object that receives the control content.
    /// </param>
    protected override void Render(HtmlTextWriter writer)
    {
        if (!IsDisabled())
            base.Render(writer);
        else
            writer.Write(String.Format("<a>{0}</a>", Text));
    }

    /// <summary>
    /// Determines whether this instance is disabled.
    /// </summary>
    /// <returns>
    /// 	<c>true</c> if this instance is 
    /// 	disabled; otherwise, <c>false</c>.
    /// </returns>
    private Boolean IsDisabled()
    {
        if (!delete.Contains(CommandName.ToLower()))
            return false;

        // get restrictions for the current
        // users access to this table
        var table = DynamicDataRouteHandler.GetRequestMetaTable(Context);
        var usersRoles = Roles.GetRolesForUser();
        var tableRestrictions = table.Attributes.OfType<SecureTableAttribute>();

        // restrictive permissions
        if (tableRestrictions.Count() == 0)
            return true;

        foreach (var tp in tableRestrictions)
        {
            // the LinkButton is considered disabled if delete is denied.
            var action = CommandName.ToEnum<TableActions>();
            if (tp.HasAnyRole(usersRoles) && (tp.Actions & action) == action)
                return false;
        }
        return true;
    }
}

Listing 5 – Secure Link Button

In more detail the IsDisabled method check to see if the LinkButtons CommandName is the same as the the “SecureLinkButtonDeleteCommandNames” application setting set in the web.config, note the default is “delete”. And then if the user does not have Delete permission then the button is disabled.

So how do we use this SecureLinkButton we add a tagMapping in the web.config file see Listing 6.

<configuration>
    <system.web>
        <pages>
            <controls>
                <!-- custom tag assignments -->
                <add tagPrefix="asp" namespace="NotAClue.Web.DynamicData" 
                    assembly="NotAClue.Web.DynamicData" />
            </controls>
            <!-- custom tag mappings -->
            <tagMapping>
                <add tagType="System.Web.UI.WebControls.LinkButton"
                    mappedTagType="NotAClue.Web.DynamicData.SecureLinkButton" />
            </tagMapping>
        </pages>
    </system.web>
</configuration>

Listing 6 – Tag Mapping in web.config

This means that our SecureLinkButton will replace the LinkButton throughout the site, however if you do not like this you can just rename each delete <asp:LinkButton to <asp:SecureLinkButton and then you will get the same effect and not add the tagMapping section to the web.config.

The Secure Meta Model

Here the two main parts are the SecureMetaTable and the three MetaColumn types (SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn)

SecureMetaTable

In the SecureMetaTable we override the GetScaffoldColumns method and filter the column list to where columns do not have a DenyRead action applied for any of the current users security roles.

SecureMetaColumn, SecureMetaForeignKeyColumn and SecureMetaChildrenColumn

With these types we do have to repeat ourselves a little as we override the IsReadOnly property to check to see if the column has a DenyWrite action applied for one of the users roles.
Note: Thanks to the ASP.NET team for listening and making this property virtual.

There is one issue I found and that is the default FieldTemplateFactory caches the DynamicControl model (ReadOnly, Edit and Insert) I did toy with adding the relevant code the default EntityTemplates see Listing 7, but decided again it.

protected void DynamicControl_Init(object sender, EventArgs e)
{
    DynamicControl dynamicControl = (DynamicControl)sender;
    dynamicControl.DataField = currentColumn.Name;

    // test for read-only column
    if (currentColumn.IsReadOnly)
        dynamicControl.Mode = DataBoundControlMode.ReadOnly;
}


Listing 7 – adding control mode code to the default EntityTemplates

Instead I decided to use a custom FieldTemplateFactory see Listing 8

public class SecureFieldTemplateFactory : FieldTemplateFactory
{
    public override IFieldTemplate CreateFieldTemplate(MetaColumn column,
        DataBoundControlMode mode, 
        string uiHint)
    {
        // code to fix caching issue
        if (column.IsReadOnly)
            mode = DataBoundControlMode.ReadOnly;

        return base.CreateFieldTemplate(column, mode, uiHint);
    }
}

Listing 8 – Secure Field Template Factory

The code here is simple we just check to see if the column is read-only (remembering that the SecureMetaColumns are already checking this for us) then set the Mode to DataBoundControlMode.ReadOnly. This nicely keeps our code DRY.

Secure Table and Column Attributes

These are essentially unchanged from the previous series of articles with just a little refactoring to make the code more readable.

!Important: For code see sample at end of article.

Putting It Together

Nearly all the work to get Secure Dynamic Data working is done simply in the Global.asax file.

Note: There are some changes you need to make to add Login etc but that is standard ASP.Net and specific to Dynamic Data.

Adding Security to Dynamic Data

Figure 1 – Adding Security to Dynamic Data

Also you need the tag mapping from Listing 6, there are some more bits we need to do but they are standard ASP.Net Security, so let’s get that done next.

!Important: To use this as we currently are you will need SQL Server 200x Express installed otherwise you will need to add a specific connection string and use Creating the Application Services Database for SQL Server to make your ASPNETDB database for membership and roles.
<authentication mode="Forms">
    <forms loginUrl="~/Login.aspx" protection="All" defaultUrl="~/Default.aspx" path="/"/>
</authentication>
<authorization>
    <deny users="?"/>
</authorization>
<membership>
    <providers>
        <remove name="AspNetSqlMembershipProvider"/>
        <add name="AspNetSqlMembershipProvider"
            type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
            connectionStringName="LocalSqlServer"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            applicationName="/"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="5"
            minRequiredPasswordLength="7"
            minRequiredNonalphanumericCharacters="0"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression=""/>
    </providers>
</membership>
<roleManager enabled="true" />

Listing 9 – Adding standard ASP.Net security to web.config

<location path="Site.css">
    <system.web>
        <authorization>
            <allow users="*"/>
        </authorization>
    </system.web>
</location>

Listing 10 – Allowing access to style sheet.

With SQL Server 200x Express edition installed you will get the ASPNETDB created automatically.

Note: I generally do this to create the ASPNETDB then move it to where I want it and setup a specific connection string. Also you can use the ASP.Net Configuration utility to create users and roles.
ASP.Net Configuration Utility
Figure 1 - ASP.Net Configuration Utility

Downloads

I think that is about it, so here is the download it contains three projects the Class Library and two sample projects one Entity Framework and one Linq to SQL. Have fun.

Saturday, 4 April 2009

Cascading Filters and Fields – Dynamic Data Entity Framework Version (UPDATED)

Well I wanted to do this in EF and Preview 3 at the same time but I having an issue with that so I’m combining both the Cascading Fields and Filters together and when the bugs are ironed out of the Preview I’ll do it there also.

Firstly the issue with the Preview.

  1. No filters support in the DefaultEFProject
  2. Errors when saving using the DefaultDomainServiceProject

Well I’m going to build a new Dynamic Data Entities Web Application for this project and create a separate project to keep the CascadeExtensions in.

I’ll add the projects zipped to the end of the article.

Lets create the class file first. (I’m assuming you know how to operate VS :D )

Create a new Class Library project Class Library project and delete the Class.cs file and give the project a namespace like DyanmicData.CascadeExtensions

ScreenShot285

Figure 1 – Adding Assembly name and Default namespace

using System;

namespace
DynamicData.CascadeExtensions { /// <summary> /// Attribute to identify which column to use as a /// parent column for the child column to depend upon /// </summary> public class CascadeAttribute : Attribute { /// <summary> /// Name of the parent column /// </summary> public String ParentColumn { get; private set; } /// <summary> /// Default Constructor sets ParentColumn /// to an empty string /// </summary> public CascadeAttribute() { ParentColumn = ""; } /// <summary> /// Constructor to use when /// setting up a cascade column /// </summary> /// <param name="parentColumn">Name of column to use in cascade</param> public CascadeAttribute(string parentColumn) { ParentColumn = parentColumn; } } }
Listing 1 - CascadeAttribute

You will need one of my extension methods to extract the attribute later on:

/// <summary>
/// Get the attribute or a default instance of the attribute
/// if the Column attribute do not contain the attribute
/// </summary>
/// <typeparam name="T">Attribute type</typeparam>
/// <param name="table">Column to search for the attribute on.</param>
/// <returns>The found attribute or a default instance of the attribute of type T</returns>
public static T GetAttributeOrDefault<T>(this MetaColumn column) where T : Attribute, new()
{
    return column.Attributes.OfType<T>().DefaultIfEmpty(new T()).FirstOrDefault();
}

Listing 2 – GetAttributeOrDefault extension method.

I have some more extension methods to add later that will be used by both the CascadeFieldTemplate and CascadeFilterTemplates.

using System;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Event Arguments for Category Changed Event
    /// </summary>
    public class SelectionChangedEventArgs : EventArgs
    {
        /// <summary>
        /// Custom event arguments for SelectionChanged 
        /// event of the CascadingFieldTemplate control
        /// </summary>
        /// <param name="value">
        /// The value of the currently selected 
        /// value of the parent control
        /// </param>
        public SelectionChangedEventArgs(String value)
        {
            Value = value;
        }
        /// <summary>
        /// The values from the control of the parent control
        /// </summary>
        public String Value { get; set; }
    }
}

Listing 3 - SelectionChangedEventArgs

Next we create two new classes called CascadeFieldTemplate and CascadeFilterTemplate.

using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFieldTemplate : FieldTemplateUserControl
    {
        /// <summary>
        /// Controls selected value
        /// </summary>
        public String SelectedValue { get; private set; }

        /// <summary>
        /// This controls list control 
        /// </summary>
        public ListControl ListControl { get; private set; }

        /// <summary>
        /// Parent column of this column named in metadata
        /// </summary>
        public MetaForeignKeyColumn ParentColumn { get; private set; }

        /// <summary>
        /// This FieldTemplates column as MetaForeignKeyColumn
        /// </summary>
        public MetaForeignKeyColumn ChildColumn { get; private set; }

        /// <summary>
        /// Parent control acquired from ParentColumn 
        /// </summary>
        public CascadingFieldTemplate ParentControl { get; set; }

        protected virtual void Page_Init(object sender, EventArgs e)
        {
            // get the parent column
            var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn;
            if (!String.IsNullOrEmpty(parentColumn))
            {
                ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn;
            }

            // cast Column as MetaForeignKeyColumn
            ChildColumn = Column as MetaForeignKeyColumn;


            // get parent field (note you must specify the
            // container control type in <DetailsView> or <FormView>
            ParentControl = GetParentControl();
        }

        /// <summary>
        /// Delegate for the Interface
        /// </summary>
        /// <param name="sender">
        /// A parent control also implementing the 
        /// ISelectionChangedEvent interface
        /// </param>
        /// <param name="e">
        /// An instance of the SelectionChangedEventArgs
        /// </param>
        public delegate void SelectionChangedEventHandler(
            object sender,
            SelectionChangedEventArgs e);

        //publish event
        public event SelectionChangedEventHandler SelectionChanged;

        /// <summary>
        /// Raises the event checking first that an event if hooked up
        /// </summary>
        /// <param name="value">The value of the currently selected item</param>
        public void RaiseSelectedIndexChanged(String value)
        {
            // make sure we have a handler attached
            if (SelectionChanged != null)
            {
                //raise event
                SelectionChanged(this, new SelectionChangedEventArgs(value));
            }
        }

        // advanced populate list control
        protected void PopulateListControl(ListControl listControl, String filterValue)
        {
            //get the parent column
            if (ParentColumn == null)
            {
                // if no parent column then just call
                // the base to populate the control
                PopulateListControl(listControl);
                // make sure control is enabled
                listControl.Enabled = true;
            }
            else if (String.IsNullOrEmpty(filterValue))
            {
                // if there is a parent column but no filter value
                // then make sure control is empty and disabled
                listControl.Items.Clear();

                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    listControl.Items.Add(new ListItem("[Not Set]", ""));

                // make sure control is disabled
                listControl.Enabled = false;
            }
            else
            {
                // get the child columns parent table
                var childTable = ChildColumn.ParentTable;

                // get query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // get list of values filtered by the parent's selected entity
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();

                // only add [Not Set] if in insert mode or column is not required
                if (Mode == DataBoundControlMode.Insert || !Column.IsRequired)
                    listControl.Items.Add(new ListItem("[Not Set]", ""));

                // add returned values to list control
                foreach (var row in itemlist)
                    listControl.Items.Add(
                        new ListItem(
                            childTable.GetDisplayString(row),
                            childTable.GetPrimaryKeyString(row)));

                // make sure control is enabled
                listControl.Enabled = true;
            }
        }

        /// <summary>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <param name="column"></param>
        /// <returns></returns>
        private CascadingFieldTemplate GetParentControl()
        {
            // get value of dev ddl (Community)
            var parentDataControl = GetContainerControl();

            if (ParentColumn != null)
            {
                // Get Parent FieldTemplate
                var parentDynamicControl = parentDataControl
                    .FindDynamicControlRecursive(ParentColumn.Name)
                    as DynamicControl;

                // extract the parent control from the DynamicControl
                CascadingFieldTemplate parentControl = null;
                if (parentDynamicControl != null)
                    parentControl = parentDynamicControl.Controls[0] 
as
CascadingFieldTemplate; return parentControl; } return null; } /// <summary> /// Get the Data Control containing the FiledTemplate /// usually a DetailsView or FormView /// </summary> /// <param name="control"> /// Use the current field template as a starting point /// </param> /// <returns> /// A CompositeDataBoundControl the base class for FormView and DetailsView /// </returns> private CompositeDataBoundControl GetContainerControl() { var parentControl = this.Parent; while (parentControl != null) { // NOTE: this will not work if used in // inline editing in a list view as // ListView is a DataBoundControl. var p = parentControl as CompositeDataBoundControl; if (p != null) return p; else parentControl = parentControl.Parent; } return null; } } }

Listing 4 – CascadeFieldTemplate

using System;
using System.Web.DynamicData;
using System.Web.UI.WebControls;

namespace DynamicData.CascadeExtensions
{
    /// <summary>
    /// Modifies the standard FieldTEmplateUserControl 
    /// to support cascading of selected values.
    /// </summary>
    public class CascadingFilterTemplate : FilterUserControlBase
    {
        #region Properties
        /// <summary>
        /// This controls list control 
        /// </summary>
        public ListControl ListControl { get; private set; }

        /// <summary>
        /// Paretn column of this column named in metadata
        /// </summary>
        public MetaForeignKeyColumn ParentColumn { get; private set; }

        /// <summary>
        /// This FieldTemplates column as MetaForeignKeyColumn
        /// </summary>
        public MetaForeignKeyColumn ChildColumn { get; private set; }

        /// <summary>
        /// Parent control acquired from ParentColumn 
        /// </summary>
        public CascadingFilterTemplate ParentControl { get; set; }
        #endregion

        //public override IQueryable GetQueryable(IQueryable source)
        //{
        //    return source;
        //}

        protected virtual void Page_Init(object sender, EventArgs e)
        {
            // get the parent column
            var parentColumn = Column.GetAttributeOrDefault<CascadeAttribute>().ParentColumn;
            if (!String.IsNullOrEmpty(parentColumn))
                ParentColumn = Column.Table.GetColumn(parentColumn) as MetaForeignKeyColumn;

            // cast Column as MetaForeignKeyColumn
            ChildColumn = Column as MetaForeignKeyColumn;

            // get dependee field (note you must specify the
            // container control type in <DetailsView> or <VormView>
            ParentControl = GetParentControl();
        }

        /// <summary>
        /// Delegate for the Interface
        /// </summary>
        /// <param name="sender">
        /// A parent control also implementing the 
        /// ISelectionChangedEvent interface
        /// </param>
        /// <param name="e">
        /// An instance of the SelectionChangedEventArgs
        /// </param>
        public delegate void SelectionChangedEventHandler(
            object sender,
            SelectionChangedEventArgs e);

        //publish event
        public event SelectionChangedEventHandler SelectionChanged;

        /// <summary>
        /// Raises the event checking first that an event if hooked up
        /// </summary>
        /// <param name="value">The value of the currently selected item</param>
        public void RaiseSelectedIndexChanged(String value)
        {
            // make sure we have a handler attached
            if (SelectionChanged != null)
            {
                //raise event
                SelectionChanged(this, new SelectionChangedEventArgs(value));
            }
        }

        // advanced populate list control
        protected void PopulateListControl(ListControl listControl, String filterValue)
        {
            //get the parent column
            if (ParentColumn == null)
            {
                // if no parent column then just call
                // the base to populate the control
                PopulateListControl(listControl);
                // make sure control is enabled
                listControl.Enabled = true;
            }
            else if (String.IsNullOrEmpty(filterValue))
            {
                // if there is a parent column but no filter value
                // then make sure control is empty and disabled
                listControl.Items.Clear();

                listControl.Items.Add(new ListItem("[All]", ""));

                // make sure control is disabled
                listControl.Enabled = false;
            }
            else
            {
                // get the child columns parent table
                var childTable = ChildColumn.ParentTable;

                // get query {Table(Developer).OrderBy(d => d.Name)}
                var query = ChildColumn.ParentTable.GetQuery(Column.Table.CreateContext());

                // filter the query by the parent
                var itemlist = query.GetQueryFilteredByParent(ParentColumn, filterValue);

                // clear list controls items collection before adding new items
                listControl.Items.Clear();
                listControl.Items.Add(new ListItem("[All]", ""));

                // add returned values to list control
                foreach (var row in itemlist)
                    listControl.Items.Add(
                        new ListItem(
                            childTable.GetDisplayString(row),
                            childTable.GetPrimaryKeyString(row)));

                // make sure control is enabled
                listControl.Enabled = true;
            }
        }

        /// <summary>
        /// Gets the Parent control in a cascade of controls
        /// </summary>
        /// <returns>An the parent control or null</returns>
        private CascadingFilterTemplate GetParentControl()
        {
            if (ParentColumn != null)
            {
                // get the parent container
                var parentDataControl = GetContainerControl();

                // get the parent container
                if (parentDataControl != null)
                    return parentDataControl.FindFilterControlRecursive(ParentColumn.Name)
                        as CascadingFilterTemplate;
            }
            return null;
        }

        /// <summary>
        /// Get the Data Control containing the FiledTemplate
        /// usually a DetailsView or FormView
        /// </summary>
        /// <param name="control">
        /// Use the current field template as a starting point
        /// </param>
        /// <returns>
        /// A FilterRepeater the control that 
        /// contains the current control
        /// </returns>
        private FilterRepeater GetContainerControl()
        {
            var parentControl = this.Parent;
            while (parentControl != null)
            {
                var p = parentControl as FilterRepeater;
                if (p != null)
                    return p;
                else
                    parentControl = parentControl.Parent;
            }
            return null;
        }
    }
}

Listing 5 – CadcadingFilterTemplate

You may note that I have removed GetQueryFilteredByParent and some other local methods from both CascadeFieldTemplate and CadcadingFilterTemplate, they will be placed in the extension methods class file later.

Now we come the the differences between the Linq to SQL implementation and this the Entity Framework implementation.

The issue I had when trying to make this work with EF was that

private IQueryable GetQueryFilteredByParent
    (MetaTable childTable,
    MetaForeignKeyColumn parentColumn,
    object selectedParent)
{
    // get query {Table(Developer)}
    var query = ChildColumn.ParentTable.GetQuery(DC);

    // {Developers}
    var parameter = Expression.Parameter(childTable.EntityType, childTable.Name);

    // {Developers.Builder}
    var property = Expression.Property(parameter, parentColumn.Name);

    // {value(Builder)}
    var constant = Expression.Constant(selectedParent);

    // {(Developers.Builder = value(Builder))}
    var predicate = Expression.Equal(property, constant);

    // {Developers => (Developers.Builder = value(Builder))}
    var lambda = Expression.Lambda(predicate, parameter);

    // {Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))}
    var whereCall = Expression.Call(typeof(Queryable), 
        "Where", 
        new Type[] { childTable.EntityType }, 
        query.Expression, 
        lambda);

    // generate the query and return it
    return query.Provider.CreateQuery(whereCall);
}

Listing 6 – Old GetQueryFilteredByParent method.

In here I passed in the selectedParent which contained the entity I was filtering on anyway what I found was the EF did not like that at all it said basically I want a simple value like int, String, double etc.

This is the expression I was faced with:

Table(Developer).Where(Developers => (Developers.Builder = value(Builder)))

here I was leaving the join up to L2S but EF wanted me to be more specific

Table(Developer).Where(Developers => (Developers.BuilderId = 2))

But I knew that wouldn't work because EF does not have the FK fields in it’s entities, well not if it can help it :) so I surmised that it would want something like this:

Table(Developer).Where(Developers => (Developers.Builder.Id = 2))

Where Id is the PK of the Builder entity.

And then I was lucky enough to be trying this with Preview 3 and the DefaultDomainServiceProject which if you have a look at the ForeignKey filter you will see some nice Expression creating code:

private Expression BuildQueryBody(
    ParameterExpression parameterExpression, 
    string selectedValue)
{
    IDictionary dict = new Hashtable();
    Column.ExtractForeignKey(dict, selectedValue);

    int i = 0;
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        string fieldName = Column.ParentTable.Name + "." 
            + Column.ParentTable.PrimaryKeyColumns[i++].Name;

        Expression propertyExpression = 
            CreatePropertyExpression(parameterExpression, fieldName);

        object value = ChangeType(entry.Value, propertyExpression.Type);
        Expression equalsExpression = Expression.Equal(
            propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        andFragments.Add(equalsExpression);
    }

    Expression result = null;
    foreach (Expression e in andFragments)
    {
        if (result == null)
        {
            result = e;
        }
        else
        {
            result = Expression.AndAlso(result, e);
        }
    }
    return result;
}

Listing 7 – BuildQuery from the ForeignKey filter.

This helps build something like this (item.Categories.CategoryID = 1) which eventually produces this where expression and it would deal with composite keys.

Where(item => (item.Categories.CategoryID = 2))

So I swiped that from the ForeignKey filter and added it to my Extension methods, and built the missing bits from what I could glean with Reflector.

So we have the following in the extension methods class

#region IQueryable methods
/// <summary>
/// Gets a list of entities from the source IQueryable 
/// filtered by the MetaForeignKeyColumn's selected value
/// </summary>
/// <param name="sourceQuery">The query to filter</param>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <returns>
/// An IQueryable of the based on the source query 
/// filtered but the FK column and value passed in.
/// </returns>
public static IQueryable GetQueryFilteredByParent(this IQueryable sourceQuery, MetaForeignKeyColumn fkColumn, String fkSelectedValue)
{
    // if no filter value return the query
    if (String.IsNullOrEmpty(fkSelectedValue))
        return sourceQuery;

    // {RequiredPlots}
    var parameterExpression = Expression.Parameter(sourceQuery.ElementType, fkColumn.Table.Name);

    // {(RequiredPlots.Builders.Id = 1)}
    var body = BuildWhereClause(fkColumn, parameterExpression, fkSelectedValue);

    // {RequiredPlots => (RequiredPlots.Builders.Id = 1)}
    var lambda = Expression.Lambda(body, parameterExpression);

    // Developers.Where(RequiredPlots => (RequiredPlots.Builders.Id = 1))}
    MethodCallExpression whereCall = Expression.Call(typeof(Queryable),
        "Where", new Type[] { sourceQuery.ElementType },
        sourceQuery.Expression,
        Expression.Quote(lambda));

    // create and return query
    return sourceQuery.Provider.CreateQuery(whereCall);
}

/// <summary>
/// This builds the and where clause taking
/// into account composite keys
/// </summary>
/// <param name="fkColumn">The column to filter the query on</param>
/// <param name="fkSelectedValue">The value to filter the query by</param>
/// <param name="parameterExpression">Parameter expression</param>
/// <returns>
/// Returns the expression for the where clause 
/// i.e. ((x = 1) && (Y = 2)) or (x = 1) etc.
/// </returns>
private static Expression BuildWhereClause(
    MetaForeignKeyColumn fkColumn, 
    ParameterExpression parameterExpression, 
    string fkSelectedValue)
{
    // get the FK's and value into dictionary
    IDictionary dict = new OrderedDictionary();
    fkColumn.ExtractForeignKey(dict, fkSelectedValue);

    // setup index into dictionary
    int i = 0;

    // setup array list to hold each AND fragment
    ArrayList andFragments = new ArrayList();
    foreach (DictionaryEntry entry in dict)
    {
        // get fk name 'Builders.Id'
        string keyName = fkColumn.Name
            + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

        // Build property expression 
        // i.e. {RequiredPlots.Builders.Id}
        Expression propertyExpression 
            = BuildPropertyExpression(parameterExpression, keyName);

        // sets the type based on the propertyExpression's type
        // i.e. all the values returned from the DDL are of type string
        // so the type on the expression needs setting to the correct type
        object value = ChangeType(entry.Value, propertyExpression.Type);

        // join the property expression and value in an
        // equals expression i.e. (RequiredPlots.Builders.Id = 1)
        Expression equalsExpression 
            = Expression.Equal(propertyExpression, 
            Expression.Constant(value, propertyExpression.Type));

        // add a fragment to array list
        andFragments.Add(equalsExpression);
    }

    // initialise result
    Expression result = null;
    // join add fragments of composite keys 
    // together together
    foreach (Expression e in andFragments)
    {
        if (result == null)
            result = e;
        else
            result = Expression.AndAlso(result, e);
    }
    // joined fragments look something like:
    // (RequiredPlots.Developer.Id = 1) && (RequiredPlots.HouseType.Id = 1)
    return result;
}

/// <summary>
/// Builds a property expression from the parts it joins
/// the parameterExpression and the propertyName together.
/// i.e. {RequiredPlots}  and "Builders.Id"
/// becomes: {RequiredPlots.Developers.Id}
/// </summary>
/// <param name="parameterExpression">
/// The parameter expression.
/// </param>
/// <param name="propertyName">
/// Name of the property.
/// </param>
/// <returns>
/// A property expression
/// </returns>
public static Expression BuildPropertyExpression(
    Expression parameterExpression, 
    string propertyName)
{
    Expression expression = null;
    // split the propertyName into each part to 
    // be build into a property expression
    string[] strArray = propertyName.Split(new char[] { '.' });
    foreach (string str in strArray)
    {
        if (expression == null)
            expression 
                = Expression.PropertyOrField(parameterExpression, str);
        else
            expression 
                = Expression.PropertyOrField(expression, str);
    }
    // {RequiredPlots.Developer.Id}
    return expression;
}

/// <summary>
/// Changes the type.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to.</param>
/// <returns>The value converted to the type.</returns>
public static object ChangeType(object value, Type type)
{
    // if type is null throw exception can't
    // carry on nothing to convert to.
    if (type == null)
        throw new ArgumentNullException("type");

    if (value == null)
    {
        // test for nullable type 
        // (i.e. if Nullable.GetUnderlyingType(type)
        // is not null then it is a nullable type 
        // OR if it is a reference type
        if ((Nullable.GetUnderlyingType(type) != null) 
            || !type.IsValueType)
            return null;
        else // for 'not nullable value types' return the default value.
            return Convert.ChangeType(value, type);
    }

    // ==== Here we are guaranteed to have a type and value ====

    // get the type either the underlying type or 
    // the type if there is no underlying type.
    type = Nullable.GetUnderlyingType(type) ?? type;

    // Convert using the type
    TypeConverter converter 
        = TypeDescriptor.GetConverter(type);
    if (converter.CanConvertFrom(value.GetType()))
    {
        // return the converted value
        return converter.ConvertFrom(value);
    }

    // Convert using the values type
    TypeConverter converter2 
        = TypeDescriptor.GetConverter(value.GetType());
    if (!converter2.CanConvertTo(type))
    {
        // if the type cannot be converted throw an error
        throw new InvalidOperationException(
            String.Format("Unable to convert type '{0}' to '{1}'", 
            new object[] { value.GetType(), type }));
    }
    // return the converted value
    return converter2.ConvertTo(value, type);
}
#endregion

Listing 8 – IQueryable extension methods

Updated: 

In the BuildWhereClause method in Listing 8 I have made a minor change that resolves a major bug:

    string keyName = fkColumn.ParentTable.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

has been changed to:

         string keyName = fkColumn.Name + "." + fkColumn.ParentTable.PrimaryKeyColumns[i++].Name;

The issue here was that you would get {RequiredPlots.Developers.Id} instead of {RequiredPlots.Developer.Id} (note the plural Developers) this was fine in Entity Framework where the entity was left as it cane out of the DB but no good for Linq to SQL which uses pluralisation.

So there you have it, there’s a lot more we could do with this to streamline the code make some of the extension methods more generic etc but I think I will leave it there.

Download (UPDATED)

The download is a Web Application Project for EF bit the CascadeExtensions classes, FieldTemplate and FilterUserControl are compatible with EF and L2S.

Note: Also included are the script to create the DB and some data to import in excel format.
Updated: I’ve now added filter ordering via an extension to the FilterRepeater called SortedFilterRepeater and am mapping it in web.config, also added a general sort vi IAutoFieldGenerator on all pages