Wednesday, October 1, 2014

Storing Complex/Serialized objects in Session

Storing objects in Session state-bag is commonplace and relatively easy.
What about storing an object that has a property which is a serialized string of another object ? Trying to use that straight out of the box is either tedious and repetitive, or useless.

Consider a case where you have a database user record which contains various user properties and a property Settings that stores serialized data.
It follows reason that to begin with we make a C# class to represent the thing we want to store as serialized string. Our class will just have one property - Bar

public class UserSettings : IChanged {
    [XmlIgnore, ScriptIgnore, JsonIgnore]
    public bool Dirty { get; set; }

    private string _Bar { get; set; }
    public string Bar {
        get {
            return this._Bar;
        }
        set {
            if (this._Bar != value) {
                this._Bar = value;
                this.Dirty = true;
            }
        }
    }
}

Note that I am mandating the IChanged interface that requires you to implement a Dirty bit.This bit management is manual for now, so you would have to code your own Dirty logic for whatever bits you have.
Next, we create a wrapper class that will actually be part of the session state.

public class UserHelper : DatabaseFieldHelper<UserSettings> {
        
    public override string KeyName {
        get { return "UserSessionKey"; }
    }
        
    public override string DefaultSessionValue {
        get {
            return SessionControl.User.Settings;
        }
    }

    public override void Save() {
        using (var db = new DbContext()) {
            var userService = new UserService(db);
            var user = userService.Get(SessionControl.User.UserId);
            user.Settings = JsonConvert.SerializeObject(base.Value);
            db.SaveChanges();
        }
    }
}

Here I inherit from the DatabaseFieldHelper with type of our UserSettings, the abstract database field helper manages storing, de-serializing, and calling the Save method. KeyName, DefaultSessionValue, and Save() are all required for you to implement. All that's left is adding a property in your session control class to get or set your new DatabaseFieldHelper static object, and use away! Something like, SessionControl.UserSettings.Bar for get or set, and you would have to manually call .Save() when you've changed the properties and would like to store the data back to the database, as string, of course.
    string bar = SessionControl.UserSettings.Bar;
    bar = "New Value";
    SessionControl.UserSettings.Save();

Under the hood


/// <summary>
/// Primary purpose of this class is to recover string value from entity's property already in session to help with 
/// deserializing it into T object, using this object, and saving to database when T's properties are changed.
/// </summary>
/// <typeparam name="T">Type of setting that is serialized</typeparam>
public abstract class DatabaseFieldHelper<T> where T : class, IChanged, new() {
    /// <summary>
    /// Gets a keyname, should be session-unique
    /// </summary>
    public abstract string KeyName { get; }

    /// <summary>
    /// Default value to use when the Helper class values are not yet in session
    /// </summary>
    public abstract string DefaultSessionValue { get; }

    /// <summary>
    /// Method for saving the setting to the database.
    /// This method should set appropriate entity's column with the value from base.Serialize() string.
    /// </summary>
    public abstract void Save();

    /// <summary>
    /// Gets the serialization settings.
    /// When saving to database we should ignore property values that equal default values of those properties' types to save space.
    /// </summary>
    private JsonSerializerSettings SerializerSettings {
        get {
            return new JsonSerializerSettings() { DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore };
        }
    }

    /// <summary>
    /// Deseralizaed object.
    /// </summary>
    public T Value {
        get {
            // first look if our helper class is already in session
            T t = SessionControl.Session[KeyName] == null ? null : (T)SessionControl.Session[KeyName];
            if (t == null) {
                // when not in session look to the default value.
                string settings = this.DefaultSessionValue;
                if (!string.IsNullOrEmpty(settings)) {
                    // using the default value from somewhere else we deserialize object 
                    t = JsonConvert.DeserializeObject<T>(settings);
                } else {
                    // object is null, initialze with default T, set Dirty so we can save.
                    t = new T() {
                        Dirty = true
                    };
                }
                // save the deserealized object into the helper class's session key
                SessionControl.Session[KeyName] = t;
                // trigger the database save, should be saving new default instance of serealized object being handled
                this.Save();
            }
            return t;
        }

        private set {
            SessionControl.Session[KeyName] = value;
        }
    }

    /// <summary>
    /// Method for serializing current object with recommended settings.
    /// </summary>
    /// <returns>Serialized string safe to be stored in the database</returns>
    protected string Serialize() {
        return JsonConvert.SerializeObject(this.Value, this.SerializerSettings);
    }
}


Thursday, February 20, 2014

Binding, Sorting, and Filtering

Objective

Binding a list of to a Gridview or similar is something we all do frequently and with ease, however, as soon as you want to throw in filtering and ordering things get more complicated. In these last couple of days at work I believe I have created a solution that helps deal with all the nuisance that comes along with using filtered data. Essentially what you want is to easily create your object, then bind the list of them to a grid, and then you're done. 

The UI


Skipping the paging, this is a simple grid with sortable columns and (!) a "contains" filter on various fields. The previous implementation of the page used ASP.classic and was building the SQL query as a string, making filtering and ordering somewhat simple by just appending SQL statements based on the sort and the filter. Obviously this tactic cannot work with LINQ querries (well, I suppose it can...)

Creating the class 

Theoretically, your "business" project's method will return you either the entities as an IQuerable or you are creating your "DataRow" object on the page from what the business project gives you. I will describe the latter approach. 

private class GridOrdersDataRow {
    [CORECommon.General.Display(Name = "OrderID")]
    public long OrderId { get; set; }
    [CORECommon.General.Display(Name = "Account Number")]
    public string AccountNumber { get; set; }
    [CORECommon.General.Display(Name = "EventID")]
    public long EventId { get; set; }
    public string FirstName { get; set; }
    public string SchoolName { get; set; }
    public string LastName { get; set; }
    [CORECommon.General.Display(Name = "Customer", DatabaseFields = new string[] { "FirstName", "LastName" })]
    public string FullName {
        get {
            return this.FirstName + " " + this.LastName;
        }
    }
}

For the filtering, you would add a custom attribute with the name you want to appear in the drop down list , and you have an option to specify DatabaseFields as seen with the "FullName" property. The DatabaseFields defaults to the property name or can be supplied as an array, this gives you full control over how your data is queried. As the name states the property name or supplied name must be the actual name of your IQueryable (not necessarily the actual database fieldnames, will consider the attribute property). If the attribute is not present the property is not enabled for filtering.

Binding, sorting, and filtering 

Binding the property filter drop-down done simply with :

ddlSearchProperty.DataSource = CORECommon.General.GetDisplayAttributes<GridOrdersDataRow>();
ddlSearchProperty.DataBind();


Ordering and filtering your data, OrderByWithDirection takes a string and a SortDirection which I had a Tuple at the time.

list = list.WhereContains(txtSearchText.Text, CORECommon.General.GetDatabaseFieldNames<GridOrdersDataRow>(ddlSearchProperty.SelectedValue));
list = list.OrderByWithDirection(sort.Item1, sort.Item2);

Enjoy results.

So basically, you've made a class and added properties for filtering. Other things that still have to be done manually which I would like to work out is creating the Header text for gridview from the class and the sort expression - these are in the markup of my page (pretty straightforward).


Under the hood.

All of this kind of started when M showed me this article on how to sort by using a string. From there I decided that instead of keeping random strings in random places I would use an attribute on my class, and from there I decided that the filtering need to work in a similar fashion and I came up with this :

public static IQueryable<T> WhereContains<T>(this IQueryable<T> source, object value, params string[] searchProperties) {
    var type = typeof(T);
    var lambdaBuilder = PredicateBuilder.False<T>();
    foreach (var searchProperty in searchProperties) {
        var property = type.GetProperty(searchProperty);
        var parameter = Expression.Parameter(type, property.Name);
        var propertyAccess = Expression.MakeMemberAccess(parameter, property);
        var convertedProperty = Expression.Call(Expression.Convert(propertyAccess, typeof(object)), typeof(object).GetMethod("ToString"));
        var contains = Expression.Call(convertedProperty,
                                        typeof(string).GetMethod("Contains", new[] { typeof(string) }),
                                        Expression.Constant(Convert.ToString(value)));

        var lambda = (Expression<bool>)Expression.Lambda(contains, parameter);
        lambdaBuilder = lambdaBuilder.Or(lambda);
                
    }
            
    return source.AsExpandable().Where(lambdaBuilder);
}


Really cool stuff.
Of course you can expand on this by simply adding more attributes and filtering types, whatever you require. For example, you could add a Where() not the contains version, which I actually started off with.




CodeBlocks