Paul
Paul

Reputation: 9541

Understanding NHibernate IUserType and why it stays in memory

In a bid to try and work out where all of the memory is being taken, I've been profiling a web app for days which is starting to show NHibernate as a bit of a memory hungry beast - but I'm down to one last thing really:

Custom Types seem to be held in memory, and it's causing a bit of a memory leak

** After running the code below, I profiled the memory. In it was left an instance of UserConfig (the User instance has been GC'd) - as the user had gone, I would have expected an instance of UserConfig to have gone as well. The "instance retention graph" showed:

UserConfig | NHibernate.Type.CustomType | NHibernate.Type.IType[] | NHbernate.Persister.Entity.SingleTableEntityPersister | System.Collections..... | System.Collections..... | NHibernate.Impl.SessionFactoryImpl | SessionSource

I have included some code here to demonstrate it (please please don't comment on the code itself - I wrote this purely to test this problem, it's not supposed to be production ready code).

I've added a GC.Collect() to the end of the module so that anything in the memory snapshot is part of the problem.

I've only included relevant parts of the code (if you'd like the whole project, please email me)

Not using the CustomType isn't an option.

Session factory...

            SessionFactory = Fluently.Configure()
                .Database(MsSqlConfiguration.MsSql2008.ConnectionString(ConfigurationManager.ConnectionStrings["ConnectionString"].ToString()))
                .Mappings(x => GetFluantMappings(x))
                .ExposeConfiguration(c =>
                                         {
                                             c.SetProperty("generate_statistics", "true");
                                             c.SetProperty("current_session_context_class", contextClass);
                                             c.SetProperty("cache.use_second_level_cache", "false");
                                             c.SetProperty("cache.use_query_cache", "false");

                                         })
                .BuildSessionFactory();
        }
        catch(FluentConfigurationException ex)
        {
            throw new SessionSourceException("Error initializing session factory", ex);
        }

Session Per Request httpmodule

    private static void BeginRequest(object sender, EventArgs e)
    {
        ISession session = SessionSource.Instance.SessionFactory.OpenSession();
        ManagedWebSessionContext.Bind(HttpContext.Current, session);
    }

    private static void EndRequest(object sender, EventArgs e)
    {
        ISession session = ManagedWebSessionContext.Unbind(HttpContext.Current, SessionSource.Instance.SessionFactory);

        if (session == null) return;

        session.Close();
        session.Dispose();
        GC.Collect();
    }

The aspx Page

    protected void Page_Load(object sender, EventArgs e)
    {
        int userId = CreateUser(SessionSource.Instance.SessionFactory);
        GetUser(SessionSource.Instance.SessionFactory, userId);
    }

    private static void GetUser(ISessionFactory sessionFactory, int userId)
    {
        ISession session = sessionFactory.GetCurrentSession();
        User user = session.Get<User>(userId);
        //User user = session.Query<User>().Single(u => u.Id == userId);
    }

    private static int CreateUser(ISessionFactory sessionFactory)
    {
        User user = new User();
        user.Created = DateTime.Now;
        user.Email = "[email protected]";
        user.Enabled = true;
        user.FirstName = "first name";
        user.LastName = "last name";
        user.Password = "password";
        user.ScreenName = "firstname lastname";
        ISession session = sessionFactory.GetCurrentSession();
        session.SaveOrUpdate(user);
        return user.Id;
    }

The User class and User Map

public class User
{
    public virtual int Id { get; set; }
    public virtual string Email { get; set; }
    public virtual string ScreenName { get; set; }
    public virtual string Password { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual bool Enabled { get; set; }
    public virtual DateTime Created { get; set; }
    public virtual DateTime? LastLogin { get; set; }
    public virtual DateTime? LastActivity { get; set; }
    public virtual UserConfig Config { get; set; }
}

[Serializable]
public class UserConfig : List<string>, IUserType
{
    public UserConfig() { }

    public UserConfig(IEnumerable<string> items) : base(items)
    {

    }

    #region IUserType Members

    public object Assemble(object cached, object owner)
    {
        return DeepCopy(cached);
    }

    public object DeepCopy(object value)
    {
        return value;
    }

    public object Disassemble(object value)
    {
        return DeepCopy(value);
    }

    public new bool Equals(object x, object y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;
        return x.Equals(y);
    }

    public int GetHashCode(object x)
    {
        return x.GetHashCode();
    }

    public bool IsMutable
    {
        get { return false; }
    }

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        object obj = NHibernateUtil.String.NullSafeGet(rs, names[0]);
        if (obj == null)
        {
            return new UserConfig();
        }

        return XmlSerialiser.FromXml<UserConfig>(obj as string);
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        var parameter = (IDataParameter)cmd.Parameters[index];
        if (value == null)
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = XmlSerialiser.ToXml<UserConfig>(value);
        }
    }

    public object Replace(object original, object target, object owner)
    {
        return original;
    }

    [XmlIgnore]
    public Type ReturnedType
    {
        //the .Net type that this maps to
        get { return typeof(UserConfig); }
    }

    [XmlIgnore]
    public SqlType[] SqlTypes
    {
        //the sql type that this maps to
        get { return new[] { SqlTypeFactory.GetString(int.MaxValue), }; }
    }

    #endregion
}

public class XmlSerialiser
{
    public static XmlSerializerNamespaces GetNamespaces()
    {
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add(string.Empty, string.Empty);
        return ns;
    }

    //Creates an object from an XML string.
    public static T FromXml<T>(string xml) where T : new()
    {
        return FromXml<T>(xml, () => new T());
    }

    public static T FromXml<T>(string xml, Func<T> func)
    {
        if (xml == "<nil-classes type=\"array\" />")
        {
            return func();
        }

        XmlSerializer ser = new XmlSerializer(typeof(T));
        StringReader stringReader = new StringReader(xml);
        XmlTextReader xmlReader = new XmlTextReader(stringReader);

        try
        {
            object obj = ser.Deserialize(xmlReader);
            xmlReader.Close();
            stringReader.Close();
            return (T) obj;
        }
        finally
        {
            stringReader.Dispose();
        }
    }

    public static string ToXml(Type type, object obj)
    {
        XmlSerializer ser = new XmlSerializer(type);
        MemoryStream memStream = new MemoryStream();
        XmlTextWriter xmlWriter = new XmlTextWriter(memStream, Encoding.Unicode);

        try
        {
            xmlWriter.Namespaces = true;
            ser.Serialize(xmlWriter, obj, GetNamespaces());
            xmlWriter.Close();
            memStream.Close();
            string xml = Encoding.Unicode.GetString(memStream.GetBuffer());
            xml = xml.Substring(xml.IndexOf(Convert.ToChar(60)));
            xml = xml.Substring(0, (xml.LastIndexOf(Convert.ToChar(62)) + 1));
            return xml;
        }
        finally
        {
            memStream.Dispose();
        }
    }

    //Serializes the <i>Obj</i> to an XML string.
    public static string ToXml<T>(object obj)
    {
        return ToXml(typeof(T), obj);
    }
}

public sealed class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Id(x => x.Id).Column("UserId");
        Map(x => x.Created);
        Map(x => x.Email);
        Map(x => x.Enabled);
        Map(x => x.FirstName);
        Map(x => x.LastActivity);
        Map(x => x.LastLogin);
        Map(x => x.LastName);
        Map(x => x.Password);
        Map(x => x.ScreenName);
        Map(x => x.Config).CustomType(typeof(UserConfig));
    }
}

Upvotes: 0

Views: 1867

Answers (1)

Diego Mijelshon
Diego Mijelshon

Reputation: 52725

The problem is that you're using the same class to represent the data and the manipulation. In doing do, you're also coupling the domain model with the persistence.

If I understand correctly, User.Config is just a list of strings that you want to serialize as XML in a single field.

You should create the property as List<string> (or IList<string>), and create a IUserType that can serialize that to XML (I'd call it XMLStringListType)

After that, you can map it fluently just like you've been doing.

Upvotes: 2

Related Questions