Saturday 31 May 2008

Friday 30 May 2008

DynamicData - Updating the ListDetails Page - Part 7

Articles in this Series

Updating the ListDetails Page

Similarly to the List page Listing 1 show the class variables.

protected Boolean denyEdit = false;
protected Boolean denyDelete = false;
protected Boolean denyDetails = false;
protected Boolean denyInsert = false;

Listing 1

Next is the setting of the class variables based on the permissions set returned.

// Get permissions for current table
var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser());

// get status of links for column 0 from table permissions
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyEdit))
    denyEdit = true;
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDelete))
    denyDelete = true;
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDetails))
    denyDetails = true;
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyInserts))
    denyInsert = true;

Listing 2

This is the error if user got to this by invalid URL for them (e.g. old link or a hack)

// if table is denied read throw error
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
{
    Response.Redirect("~/Default.aspx?error=No access to " + table.Name);
}

Listing 3

Here the links and DetailsPanel are set to display or hide, also if the DetailsPanel is hidden the don't bother to turn off the buttons.

// set visibility of Details
DetailsPanel.Visible = !denyDetails;
GridView1.AutoGenerateSelectButton = !denyDetails;
GridView1.AutoGenerateEditButton = !denyEdit;
GridView1.AutoGenerateDeleteButton = !denyDelete;

if (!denyDetails)
{
    DetailsView1.AutoGenerateInsertButton = !denyInsert;
    DetailsView1.AutoGenerateEditButton = !denyEdit;
    DetailsView1.AutoGenerateDeleteButton = !denyDelete;
}

Listing 4

I think that about wraps it up for this...

But may be not I think next I may override some of the controls then there will be less code to add per page.

NOTE: you will need to change the routing to see ListDetails pages instead of List, Details, Edit, Insert.

Wednesday 28 May 2008

DynamicData - Miscellaneous bits - Part 6

Articles in this Series

Disabling Insert hyperlink on List and Details etc

Listing 1 shows the code to add to pages to disable the insert button.

// get user permissions
var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser());

// Disable insert hyperlink in inserts denied.
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyInserts))
{
    InsertHyperLink.Visible = false;
}

Listing 1

Removing; Edit, Delete, Details hyperlinks from Lists

Listing 2 is added to the List page as class variables and Listing 3 is added to the Page_Load, it will remove the whole column if all three class variables are true.

protected Boolean denyEdit = false;
protected Boolean denyDelete = false;
protected Boolean denyDetails = false;

Listing 2

// get user permissions
var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser());

// get status of links for column 0 from table permissions
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyEdit))
    denyEdit = true;
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDelete))
    denyDelete = true;
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyDetails))
    denyDetails = true;
    
// if all buttons are hidden the hide the column
if (denyDelete && denyDetails && denyEdit)
{
    GridView1.Columns.RemoveAt(0);
    // remove the event handler if not needed
    GridView1.RowDataBound -= GridView1_RowDataBound
}

Listing 3

To remove individual hyperlinks an OnRowDataBound event handler needs to be added see Listing 4.

<asp:GridView ID="GridView1" runat="server" 
    DataSourceID="GridDataSource" 
    AllowPaging="True"
    AllowSorting="True" 
    CssClass="gridview" 
    OnRowDataBound="GridView1_RowDataBound">

Listing 4

The same class variables are used here to turn off individual hyperlinks (

// manage security on Edit Delete links
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    // don't bother to hide individual links if they are all hidden
    if (e.Row.RowType == DataControlRowType.DataRow)// && !(!denyDelete && !denyDetails && !denyEdit))
    {
        var controls = e.Row.Cells[0].Controls;
        if (controls.Count > 0)
        {
            foreach (var c in controls)
            {
                if (c.GetType() == typeof(HyperLink))
                {
                    var h = c as HyperLink;
                    if ((h.Text.ToLower() == "edit" && denyEdit) ||
                        (h.Text.ToLower() == "details" && denyDetails))
                    {
                        ((HyperLink)c).Visible = false;
                    }
                }
                else if (c.GetType() == typeof(LinkButton))
                {
                    var h = c as LinkButton;
                    if (h.Text.ToLower() == "delete" && denyDelete)
                    {
                        ((LinkButton)c).Visible = false;
                    }
                }
            }
        }
    }
}

Listing 5

Note: if all three class variable are true you could add the && !(!denyDelete && !denyDetails && !denyEdit) to the if statement and remover the GridView1.RowDataBound -= GridView1_RowDataBound line from Page_Load if statement, if you you still need to process the rows for some other reason.

Next the ListDetailsPage.

Monday 26 May 2008

DynamicData - Generate Columns/Rows (using IAutoFieldGenerator) - Part 5

Articles in this Series

IAutoFieldGenerator

This is the heart of controlling which fields are shown in the List, Details, Edit views etc. Listing 1 is a skeleton of the Column/Row Generator, as you can see it is a class that is passed a table and has a loop which returns a ICollection of DynamicFields. The foreach loop is where the columns are processed similarly to the if (!column.Scaffold) which tests to see if the column to scaffolded.

using System.Collections.Generic;
using System.Web.DynamicData;

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;

    public FilteredFieldsManager(MetaTable table)
    {
        _table = table;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

        // where do I put this test I think in the page?
        foreach (MetaColumn column in _table.Columns)
        {
            // carry on the loop at the next column 
            // if scaffold column is set to false
            if (!column.Scaffold)
                continue;

            // create new DynamicField
            DynamicField newField = new DynamicField();

            // assign column name and add it to the collection
            newField.DataField = column.Name;
            oFields.Add(newField);
        }
        return oFields;
    }
}

Listing 1

The class is used like:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table);

Listing 2

The code in Listing 2 is added to the Page_Init event, this is all that is needed in the pages code behind to add column level security.

Customising the FieldGenerator class

The first change is to add a new member variable _roles and change the Constructor to take an array of Strings.

protected MetaTable _table;
protected String[] _usersRoles;

public FilteredFieldsManager(MetaTable table, params String[] roles)
{
    _table = table;
    _usersRoles = roles;
}

Listing 3

Then in the GenerateFields method the permission for the current column will be assigned a local variable and then add a test to the if (!column.Scaffold) so that if the column is DenyRead then the column will also be skipped see Listing 4.

// get permissions for current column for current user roles.
var fieldPermissions = column.GetFieldPermissions(Roles.GetRolesForUser());

// carry on the loop at the next column 
// if scaffold table is set to false or DenyRead
if (!column.Scaffold  fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyRead))
    continue;

Listing 4

The next step is to add a DynamicReadonlyField class so that id a field is marked DenyEdit then a read only field can be returned instead of the regular DynamicField see Listing 5.

public class DynamicReadonlyField : DynamicField
{
    public override void InitializeCell(
        DataControlFieldCell cell,
        DataControlCellType cellType,
        DataControlRowState rowState,
        int rowIndex)
    {
        if (cellType == DataControlCellType.DataCell)
        {
            var control = new DynamicControl() { DataField = DataField };

            // Copy various properties into the control
            control.UIHint = UIHint;
            control.HtmlEncode = HtmlEncode;
            control.NullDisplayText = NullDisplayText;

            // this the default for DynamicControl and has to be
            // manually changed you do not need this line of code
            // its there just to remind us what we are doing.
            control.Mode = DataBoundControlMode.ReadOnly;

            cell.Controls.Add(control);
        }
        else
        {
            base.InitializeCell(cell, cellType, rowState, rowIndex);
        }
    }
}

Listing 5 - Thanks to David Ebbo for this class and the explanation here on the DynamicData forum.

Now if we add a test for the column has a DenyEdit security attribute assigned then the field can now be set to an DynanicReadOnlyField. In Listing 6 the fieldPermissions variable can be tested to see if it contains a DenyEdit permission.

DynamicField f;
if (fieldPermissions.Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
{
    f = new DynamicReadonlyField();
}
else
{
    f = new DynamicField();
}

f.DataField = column.Name;
oFields.Add(f);

Listing 6

Listing 7 shows the code to test for foreign Key tables parent and child table permissions and if DenyRead do not show the field for the column.

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaChildrenColumn))
{
    // Get permissions for current columns child table
    var childTablePermissions = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (childTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

//if foreign key table is hidden then hide the column that references it in this table
if (column.GetType() == typeof(MetaForeignKeyColumn))
{
    // Get permissions for current columns parent table
    var parentTablePermissions = column.GetFkTablePermissions(Roles.GetRolesForUser());

    // carry on the loop at next column
    if (parentTablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        continue;
}

Listing 7

Put it all together in Listing 8.

public class FilteredFieldsManager : IAutoFieldGenerator
{
    protected MetaTable _table;
    protected String[] _usersRoles;

    public FilteredFieldsManager(MetaTable table, params String[] roles)
    {
        _table = table;
        _usersRoles = roles;
    }

    public ICollection GenerateFields(Control control)
    {
        List<DynamicField> oFields = new List<DynamicField>();

        // Get table permissions
        var tablePermissions = _table.GetTablePermissions(this._usersRoles);

        // if table is DenyRead then do not output any fields
        if (!tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        {
            foreach (MetaColumn column in _table.Columns)
            {
                // get permissions for current column for current user roles.
                var fieldPermissions
                    = column.GetFieldPermissions(Roles.GetRolesForUser());

                // carry on the loop at the next column 
                // if scaffold table is set to false or DenyRead
                if (!column.Scaffold  fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyRead))
                    continue;

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaChildrenColumn))
                {
                    // Get permissions for current columns child table
                    var childTablePermissions
                        = column.GetChildrenTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (childTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                // if foreign key table is hidden then hide
                // the column that references it in this table
                if (column.GetType() == typeof(MetaForeignKeyColumn))
                {
                    // Get permissions for current columns parent table
                    var parentTablePermissions
                        = column.GetFkTablePermissions(Roles.GetRolesForUser());

                    // carry on the loop at next column
                    if (parentTablePermissions
                        .Contains(TablePermissionsAttribute.Permissions.DenyRead))
                        continue;
                }

                DynamicField f;
                if (fieldPermissions
                    .Contains(FieldPermissionsAttribute.Permissions.DenyEdit))
                {
                    f = new DynamicReadonlyField();
                }
                else
                {
                    f = new DynamicField();
                }

                f.DataField = column.Name;
                oFields.Add(f);
            }
        }
        return oFields;
    }
}

Listing 8

having done all this remember all you have to add to the page is:

// code to add column level security
table = GridDataSource.GetTable();
GridView1.ColumnsGenerator = new FilteredFieldsManager(table, Roles.GetRolesForUser());

SQL Server 2005

SQL Server 2008

the sample already has Login and Roles setup (Account details below).

Website users:

  • admin
  • fred
  • sue
  • pam

all passwords are: password

DynamicData - Limit Tables shown on Default page and List, Edit & Details etc - Part 4

Articles in this Series

 

Limit Tables Shown on Start Page

See sample metadata in Listing 1

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Order_Detail {}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Employee {}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Shipper {}

Listing 1

Now on the Default.aspx.cs a generic piece of code can be added that will remove any table with a DenyRead attribute for one of the current users roles.

System.Collections.IList visibleTables = MetaModel.Default.VisibleTables;



// remove tables from the list if DenyRead
String[] roles = Roles.GetRolesForUser();
foreach (var table in MetaModel.Default.Tables)
{
    var permissions = ((MetaTable)table).GetTablePermissions(roles);
    if (permissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
        visibleTables.Remove(table);
}

Listing 2

The code in Listing 2 simply checks the permissions on each table for the current users Roles and if a DenyRead is encountered then the table is removed form the visibleTables collection.

Some Error Handling for Pages Reached with Tables that are DenyRead

In Listing 3 a list of permission for the current users roles are acquired from the GetTablePermissions helper extension method and if it contains a DenyRead then the page is redirected to the Default.aspx page with an error message in the URL.

// get user permissions
var tablePermissions = table.GetTablePermissions(Roles.GetRolesForUser());

// if table is denied read throw error
if (tablePermissions.Contains(TablePermissionsAttribute.Permissions.DenyRead))
{
    Response.Redirect("~/Default.aspx?error=No access to " + table.Name);
}

Listing 3

DynamicData Sorting - FilterUserControl and ForeignKey_Edit.ascx user controls

Sorting - FilterUserControl and ForeignKey_Edit.ascx user controls

To sort either the Filter on a list page of the FK drop down lists, you can use DisplayColumnAttribute which can takes upto 3 parameters ColumnName, SortColumn and SortDecending (bool)

Excerpt from the qucik starts page

"When a data field is a foreign key of another table, Dynamic Data displays this column as a link to the other table. Dynamic Data uses the first column of type string as the link text when the relationship between the tables is a one-to-one relationship.

To change the column that is displayed as the value for the foreign key field, you must apply the DisplayColumnAttribute attribute to the partial entity class of the relationship table. You must create a partial class with the name of the table to be able to apply the attribute."

[DisplayColumnAttribute("PostalCode", "PostalCode", false)]
public partial class Customer
{
}
DisplayColumnAttribute.DisplayColumnAttribute(string displayColumn, string sortColumn, bool sortDescending) 

The above image show the fully overloaded constructor for this attribute:

  • string displayColumn - the name of the column to show when using this table in a DropDownList.
  • string sortColumn - the name of the column to sort the DropDownList by
  • bool sortDescending - set to true changes the sort to descending

So by adding this to the Customer table you can change how it is displayed in filter list and FK DropDownLists.

Customer Filter on Orders table Customer FK list on Orders table

Above shows the Customers filter and Customer FK drop down list on the Orders table List and Edit pages.

Sunday 25 May 2008

Permissions Attribute class Helper Extension Methods - Part 3

Articles in this Series

Helper Extension Methods

The attribute data is held in:

  • MetaTable.Attributes
  • MetaColumn.Attributes
  • MetaForeignKeyColumn.ParentTable.Attribute
  • MetaChildrenColumn.ChildTable.Attributes

Form the above list four extension methods will be needed on to deal with each type.

  • What data type should the extension method operate on? The obvious type would be AttributeCollection but would move unnecessary casting in some of the other extension methods. The types will be MetaTable for the table permissions extension method and MetaColumn for the others and any casting will then be hidden in the extension methods themselves.
  • What type should the extension method return? The obvious and correct type would be the TablePermissionsAttribute.Permissions or FieldPermissionsAttribute.Permissions types and since there could be multiple entries per class/property then a generic List<T> of the type should be returned.
  • What other parameter will be need? A role of type String, but because a user can have several roles it should be roles of type String[] because this is what is returned by Roles.GetRolesForUser().

If a breakpoint is set at an instance of column of type MetaColumn (see Figure 1) it can be seen that the column.Attributes is of type System.ComponentModel.AttributeCollection and 1 & 2 are FieldPermissionsAttributes this means that the collection will need to be iterated through to pick out the FieldPermissionsAttributes.

Figure 1

Figure 1

Let us look at the extension method definition for MetaColumn.Attributes.

public static List<FieldPermissionsAttribute.Permissions>
GetFieldPermissions(this MetaColumn column, String[] roles)
{
    List<FieldPermissionsAttribute.Permissions> permissions = 
        new List<FieldPermissionsAttribute.Permissions>();
    //TODO: get attributes permissions
    return permissions;
}

Listing 1

Calling the GetFieldPermissions extension method:

var fieldPermissions = column.GetFieldPermissions(Roles.GetRolesForUser());
In Listing 1 each attribute is checked to see if it has one of the users roles passed in from the GetRolesForUser() method of the Roles class. If a match is found then the permission is added to “permissions” local variable.
public static List<FieldPermissionsAttribute.Permissions>
    GetFieldPermissions(this MetaColumn column, String[] roles)
{
    var permissions = new List<FieldPermissionsAttribute.Permissions>();

    // you could put: 
    // var attributes = column.Attributes;
    // but to make it clear what type we are using:
    System.ComponentModel.AttributeCollection attributes = column.Attributes;

    // check to see if any roles passed
    if (roles.Count() > 0)
    {
        // using Linq to Object to get 
        // the permissions foreach role
        permissions =
            (from a in attributes.OfType<FieldPermissionsAttribute>()
             where a.HasAnyRole(roles)
             select a.Permission).ToList<FieldPermissionsAttribute.Permissions>();
    }
    return permissions;
}

Listing 2

Above in Listing 2 is the full helper extension method GetFieldPermissions three more extension methods are required to cater for:

  • Table
  • ChileTable
  • ParentTable

MetaColumn Types. Below in Listing 3 are all the extension methods for the Permissions Attributes classes.

/// <summary>
/// Permissions Attributes Helper Extension Methods
/// </summary>
public static class PermissionsAttributesHelper
{
    /// <summary>
    /// Get a list of permissions for the specified role
    /// </summary>
    /// <param name="attributes">
    /// Is a AttributeCollection taken 
    /// from the column of a MetaTable
    /// </param>
    /// <param name="role">
    /// name of the role to be matched with
    /// </param>
    /// <returns>A List of permissions</returns>
    public static List<FieldPermissionsAttribute.Permissions>
    GetFieldPermissions(this MetaColumn column, String[] roles)
    {
        var permissions = new List<FieldPermissionsAttribute.Permissions>();

        // you could put: 
        // var attributes = column.Attributes;
        // but to make it clear what type we are using:
        System.ComponentModel.AttributeCollection attributes = column.Attributes;

        // check to see if any roles passed
        if (roles.Count() > 0)
        {
            // using Linq to Object to get 
            // the permissions foreach role
            permissions =
                (from a in attributes.OfType<FieldPermissionsAttribute>()
                 where a.HasAnyRole(roles)
                 select a.Permission).ToList<FieldPermissionsAttribute.Permissions>();
        }
        return permissions;
    }

    /// <summary>
    /// Get a list of permissions for the specified role
    /// </summary>
    /// <param name="attributes">
    /// Is a AttributeCollection taken from the column of a MetaTable
    /// </param>
    /// <param name="role">name of the role to be matched with</param>
    /// <returns>A List of permissions</returns>
    public static List<TablePermissionsAttribute.Permissions>
        GetTablePermissions(this MetaTable table, String[] roles)
    {
        var permissions = new List<TablePermissionsAttribute.Permissions>();
        var attributes = table.Attributes;

        // check to see if any roles passed
        if (roles.Count() > 0)
        {
            // using Linq to Object to get 
            // the permissions foreach role
            permissions =
                (from a in attributes.OfType<TablePermissionsAttribute>()
                 where a.HasAnyRole(roles)
                 select a.Permission).ToList<TablePermissionsAttribute.Permissions>();
        }
        return permissions;
    }

    /// <summary>
    /// Get a list of permissions for the specified role
    /// </summary>
    /// <param name="attributes">
    /// Is a AttributeCollection taken from the column of a MetaTable
    /// </param>
    /// <param name="role">name of the role to be matched with</param>
    /// <returns>A List of permissions</returns>
    public static List<TablePermissionsAttribute.Permissions>
        GetFkTablePermissions(this MetaColumn column, String[] roles)
    {
        var permissions = new List<TablePermissionsAttribute.Permissions>();
        var foreignKeyColumn = column as MetaForeignKeyColumn;
        var attributes = foreignKeyColumn.ParentTable.Attributes;

        // check to see if any roles passed
        if (roles.Count() > 0)
        {
            // using Linq to Object to get 
            // the permissions foreach role
            permissions =
                (from a in attributes.OfType<TablePermissionsAttribute>()
                 where a.HasAnyRole(roles)
                 select a.Permission).ToList<TablePermissionsAttribute.Permissions>();
        }
        return permissions;
    }

    /// <summary>
    /// Get a list of permissions for the specified role
    /// </summary>
    /// <param name="attributes">
    /// Is a AttributeCollection taken from the column of a MetaTable
    /// </param>
    /// <param name="role">name of the role to be matched with</param>
    /// <returns>A List of permissions</returns>
    public static List<TablePermissionsAttribute.Permissions>
        GetChildrenTablePermissions(this MetaColumn column, String[] roles)
    {
        var permissions = new List<TablePermissionsAttribute.Permissions>();
        var childrenColumn = column as MetaChildrenColumn;
        var attributes = childrenColumn.Attributes;

        // check to see if any roles passed
        if (roles.Count() > 0)
        {
            // using Linq to Object to get 
            // the permissions foreach role
            permissions =
                (from a in attributes.OfType<TablePermissionsAttribute>()
                 where a.HasAnyRole(roles)
                 select a.Permission).ToList<TablePermissionsAttribute.Permissions>();
        }
        return permissions;
    }

    /// <summary>
    /// Returns a copy of the array of string 
    /// all in lowercase
    /// </summary>
    /// <param name="strings">Array of strings</param>
    /// <returns>array of string all in lowercase</returns>
    public static String[] AllToLower(this String[] strings)
    {
        String[] temp = new String[strings.Count()];
        for (int i = 0; i < strings.Count(); i++)
        {
            temp[i] = strings[i].ToLower();
        }
        return temp;
    }
}

Listing 3

Next putting it all together in the Generate Columns/Rows (using IAutoFieldGenerator)

Metadata for project - Part 2

Articles in this Series

Sample Metadata

For this project the Northwind database will be used, you can download it from here (it says it is for SQL 2000 but works fine on SQL 2005 and SQL 2008 although if using SQL 2008 the database will be upgraded and then will not run on SQL 2000 or SQL 2005).

Figure 1 has some tables out of the Northwind database that have been added in to the Linq to SQL model.

Northwind DBML

Figure 1

Listing 1 is the metadata on some Northwind model.

[MetadataType(typeof(OrderMetadata))]
[TablePermissions(TablePermissionsAttribute.Permissions.DenyInserts, "Accounts")]
public partial class Order
{
}

public class OrderMetadata
{
    [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyEdit, "Sales", "Production")]
    [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyRead, "Accounts")]
    public Object OrderDate { get; set; }

    [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyEdit, "Sales", "Production")]
    [FieldPermissions(FieldPermissionsAttribute.Permissions.DenyRead, "Accounts")]
    public Object RequiredDate { get; set; }

    // entities
    [HideInFilter]
    public Object Employee { get; set; }
}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Order_Detail
{
}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyDelete, "Sales")]
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
}

public class CustomerMetadata
{
    [ScaffoldColumn(false)]
    public Object UpdatedBy { get; set; }
    [ScaffoldColumn(false)]
    public Object UpdatedOn { get; set; }
    [ScaffoldColumn(false)]
    public Object CreatedBy { get; set; }
    [ScaffoldColumn(false)]
    public Object CreatedOn { get; set; }

}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Employee
{
}

[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Sales")]
public partial class Shipper
{
}

Listing 1

Note: the table level metadata is on the partial class that is implemented from the DBML file this was to workaround a bug in DynamicData 5-12 release but it seems to be the correct place.

Next is the metadata helper classes...

DynamicData - Automatic Column Update

Articles in this Series

Automatic Column Update

The problem I'm trying to address is lets say you have the same columns on each table for simple auditing to check:

  1. Who created the record and when
  2. Who updated the record last and when

We will use the Northwind database for this test with a little tweaking.

We will use the Customers table in Northwind and add four columns:

  1. CreatedBy String
  2. CreatedOn DateTime
  3. UpdatedBy String
  4. UpdatedOn DateTime

These columns don’t need to be shown on any pages so scaffolding needs to be turned off see Listing 1

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
}

public class CustomerMetadata
{
    [ScaffoldColumn(false)]
    public Object UpdatedBy { get; set; }
    [ScaffoldColumn(false)]
    public Object UpdatedOn { get; set; }
    [ScaffoldColumn(false)]
    public Object CreatedBy { get; set; }
    [ScaffoldColumn(false)]
    public Object CreatedOn { get; set; }

}
Listing 1

To automatically update this table each time a row is created or update the DataContext partial class will need to be extended see Listing 2 in the DataContext each table has a partial method that can be implemented. The two needed for adding auditing data are InsertTableName and UpdateTableName where TableName is the name of the table that is to have auditing added.

public partial class NorthwindDataContext : System.Data.Linq.DataContext
{
    partial void InsertCustomer(Customer instance)
    {
        var user = HttpContext.Current.User;
        instance.CreatedBy = user.Identity.Name;
        instance.CreatedOn = DateTime.Now;
        instance.UpdatedBy = user.Identity.Name;
        instance.UpdatedOn = DateTime.Now;

        // finally send this to the DB
        this.ExecuteDynamicInsert(instance);
    }

    partial void UpdateCustomer(Customer instance)
    {
        var user = HttpContext.Current.User;
        instance.UpdatedBy = user.Identity.Name;
        instance.UpdatedOn = DateTime.Now;

        // finally send this to the DB
        this.ExecuteDynamicUpdate(instance);
    }
}
Listing 2

Now when a new record is created in the Customer table the four fields will be updated automatically and because the scaffolding metadata the columns will not be show in the DynamicData app.

Note: This will also work with Linq to SQL.

DynamicData Attribute Based Security - part 1

Articles in this Series

Permissions Attribute (Metadata) Classes

An attributes class inherits from System.Attribute and must have the following properties set, AttributeUsage with at least one of the 16 possible targets see below and also AllowMultiple set to true so that multiple occurrences of the attribute can decorate a class or property.

AttributeTargets

All

GenericParameter

Assembly

Interface

Class

Method

Constructor

Module

Delegate

Parameter

Enum

Property

Event

ReturnValue

Field

Struct

The AttributeTargets options that the PermissionsAttributes classes will use are class and Property.

Listing 1 shows a simplified attribute that can be applied to a class multiple times see Listing 2 where the attribute is applied to the Orders table once each for "Accounts" and "Sales" roles.

[AttributeUsage(AttributeTargets.class, AllowMultiple = true)]
public class TableDenyWriteAttribute: System.Attribute
{
    ...
}
Listing 1

Note: By convention, the name of the attribute class ends with the word Attribute. While not required, this convention is recommended for readability. When the attribute is applied, the inclusion of the word Attribute is optional.

[TableDenyWrite ("Sales")]
[TableDenyWrite ("Accounts")]
public partial class Order
{
}
Listing 2

Two types of attribute are required for the permissions attribute, Class (Entity/Table) and Property(Field/Column). Looking at Listing 2 this is all right for a simplistic approach but will make extracting the roles for each permission a bit clunky. So a better approach would be to have an attribute that is more flexible see Listing 3. The effective permissions for the class Order shown in Listing 3 are listed in Table 1.

[TablePermissions(TablePermissionsAttribute.Permissions.DenyInserts, "Sales")]
[TablePermissions(TablePermissionsAttribute.Permissions.DenyDelete, "Sales", "Production",)]
[TablePermissions(TablePermissionsAttribute.Permissions.DenyRead, "Accounts", "HR")]
public partial class Order
{
}
Listing 3
Roles Insert Delete Read
Accounts 1n n n
HR n n n
Sales n n y
Production y n y

Table 1

1. Note you will not be able to do an insert if the table cannot be viewed.

Listing 4 contain the finished attribute classes.

The changes in the finished classes include:

  1. A permission parameter
  2. The role parameter has become roles and is a string array
  3. Each attribute class has an enum which contain the table and column permissions.
  4. Helper method HasRole which return true if the role is present.
  5. Helper method HasAnyRole which return true if any of the roles passed in are present.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.DynamicData;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class TablePermissionsAttribute : System.Attribute
{
    // this property is required to work with "AllowMultiple = true" ref David Ebbo
    // As implemented, this identifier is merely the Type of the attribute. However,
    // it is intended that the unique identifier be used to identify two
    // attributes of the same type.
    public override object TypeId { get { return this; } }

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="permission"></param>
    /// <param name="roles"></param>
    public TablePermissionsAttribute(Permissions permission, params String[] roles)
    {
        this._permission = permission;
        this._roles = roles;
    }

    private String[] _roles;
    public String[] Roles
    {
        get { return this._roles; }
        set { this._roles = value; }
    }

    private Permissions _permission;
    public Permissions Permission
    {
        get { return this._permission; }
        set { this._permission = value; }
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public Boolean HasRole(String role)
    {
        // call extension method to convert array to lower case for compare
        String[] rolesLower = _roles.AllToLower();
        return rolesLower.Contains(role.ToLower());
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="roles"></param>
    /// <returns></returns>
    public Boolean HasAnyRole(String[] roles)
    {
        // call extension method to convert array to lower case for compare
        foreach (var role in roles.AllToLower())
        {
            if (_roles.AllToLower().Contains(role.ToLower()))
                return true;
        }
        return false;
    }

    /// <summary>
    /// list of Deny permissions as the default is read write on everything
    /// this model apply the most severe permission restriction
    /// </summary>
    public enum Permissions
    {
        DenyRead,
        DenyEdit,
        DenyInserts,
        DenyDelete,
        DenyDetails,
        DenySelectItem, // Don't know wether this will be any use???
    }

}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class FieldPermissionsAttribute : System.Attribute
{
    // this property is required to work with "AllowMultiple = true" ref David Ebbo
    // As implemented, this identifier is merely the Type of the attribute. However,
    // it is intended that the unique identifier be used to identify two
    // attributes of the same type.
    public override object TypeId { get { return this; } }

    // Constructor
    public FieldPermissionsAttribute(Permissions permission, params String[] roles)
    {
        this._permission = permission;
        this._roles = roles;
    }

    private String[] _roles;
    public String[] Roles
    {
        get { return this._roles; }
        set { this._roles = value; }
    }

    private Permissions _permission;
    public Permissions Permission
    {
        get { return this._permission; }
        set { this._permission = value; }
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="role"></param>
    /// <returns></returns>
    public Boolean HasRole(String role)
    {
        // call extension method to convert array to lower case for compare
        String[] rolesLower = _roles.AllToLower();
        return rolesLower.Contains(role.ToLower());
    }

    /// <summary>
    /// helper method to check for roles in this attribute
    /// the comparison is case insensitive
    /// </summary>
    /// <param name="roles"></param>
    /// <returns></returns>
    public Boolean HasAnyRole(String[] roles)
    {
        // call extension method to convert array to lower case for compare
        foreach (var role in roles.AllToLower())
        {
            if (_roles.AllToLower().Contains(role.ToLower()))
                return true;
        }
        return false;
    }

    /// <summary>
    /// list of Deny permissions as the default is read write on everything
    /// this model apply the most severe permission restriction
    /// </summary>
    public enum Permissions
    {
        DenyRead,
        DenyEdit,
    }
}
Listing 4

Next step is the metadata helper classes.