e.g. when I change the address on a user account I want to make sure the address object has been populated (at least).
So these days I've started to design classes that have public getters & private setters, and if you want to modify state you are required to call a method, e.g. 'ChangeAddress'. This is nothing new, there are plenty of examples out there.
So in the example below 'Token' is property that's defined when the object is constructed and 'Path' is a property which is modified by calling the method 'ChangePath'.
public class File : ObservableEntity<int>
{
private string _path;
private Guid _token;
private readonly IExtractFileProperties _propertiesExtractor = new FilePropertiesExtractor();
public File()
{
_token = Guid.NewGuid();
}
public virtual string Path
{
get { return _path; }
private set { ChangePropertyAndNotify(ref _path, value, x => Path); }
}
public virtual Guid Token
{
get { return _token; }
private set { ChangePropertyAndNotify(ref _token, value, x => Token); }
}
public File ChangePath(string path)
{
Path = path;
Size = _propertiesExtractor.Size(Path);
Format = _propertiesExtractor.Format(Path);
FileCreatedOn = _propertiesExtractor.CreatedOn(Path);
FileLastModified = _propertiesExtractor.LastModified(Path);
return this;
}
}
Now this attitude has been going well as it's made my classes more behaviour rich with better encapsulation & abstraction.
The only problem I've come across is when I'm trying to re-hydrate objects from the database where the properties I'm attempt to set have some kind of special behaviour - properties representing timestamps & versions that are historically set in the database via some function.
So in the following example I've got 3 properties - 'CreatedOn' 'LastModified' & 'Version'. I don't want to expose a method to set this values - I want them to be immutable...
But this is going to be a problem with an ORM, how am I going to update these values when saving or updating - I want them appear to be auto-updating from a users perspective.
public abstract class ObservableEntity<T> : IEntity<T>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private T _id;
private DateTime? _createdOn;
private DateTime? _lastModified;
private int? _version;
public virtual T Id
{
get { return _id; }
private set { ChangePropertyAndNotify(ref _id, value, x => Id); }
}
public virtual DateTime? CreatedOn
{
get { return _createdOn; }
private set { ChangePropertyAndNotify(ref _createdOn, value, x => CreatedOn); }
}
public virtual DateTime? LastModified
{
get { return _lastModified; }
private set { ChangePropertyAndNotify(ref _lastModified, value, x => LastModified); }
}
public virtual int? Version
{
get { return _version; }
private set { ChangePropertyAndNotify(ref _version, value, x => Version); }
}
protected void ChangePropertyAndNotify<T2>(ref T2 value, T2 newValue, Expression<Func<object, T2>> expression)
{
value = newValue;
Notify(expression);
}
protected void Notify<T2>(Expression<Func<object, T2>> expression)
{
var propertyName = ((MemberExpression)expression.Body).Member.Name;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now when using nHibernate as your ORM this becomes very easy because you can attach Listeners and do custom actions.
So I've got a custom action when NH saves or updates an entity to set these properties with the correct values.
public class EntitySaveOrUpdateEventListener : IPreInsertEventListener, IPreUpdateEventListener
{
public bool OnPreInsert(PreInsertEvent @event)
{
var hasType = (@event.Entity.GetType().GetInterface(typeof(IVersionable).Name, true) != null);
if (!hasType)
return false;
var now = DateTime.Now;
var createdOnIndex = new List<string>(@event.Persister.PropertyNames).FindIndex(x => x == "CreatedOn");
@event.State[createdOnIndex] = now;
SetPrivateVariable("CreatedOn", now, @event.Entity);
var lastModifiedIndex = new List<string>(@event.Persister.PropertyNames).FindIndex(x => x == "LastModified");
@event.State[lastModifiedIndex] = now;
SetPrivateVariable("LastModified", now, @event.Entity);
var versionIndex = new List<string>(@event.Persister.PropertyNames).FindIndex(x => x == "Version");
var version = GetPrivateVariable<int?>("Version", @event.Entity);
version = version.GetValueOrDefault() + 1;
@event.State[versionIndex] = version;
SetPrivateVariable("Version", version, @event.Entity);
return false;
}
public bool OnPreUpdate(PreUpdateEvent @event)
{
var hasType = (@event.Entity.GetType().GetInterface(typeof(IVersionable).Name, true) != null);
if (!hasType)
return false;
var now = DateTime.Now;
var lastModifiedIndex = new List<string>(@event.Persister.PropertyNames).FindIndex(x => x == "LastModified");
@event.State[lastModifiedIndex] = now;
SetPrivateVariable("LastModified", now, @event.Entity);
var versionIndex = new List<string>(@event.Persister.PropertyNames).FindIndex(x => x == "Version");
var version = GetPrivateVariable<int?>("Version", @event.Entity);
version = version.GetValueOrDefault() + 1;
@event.State[versionIndex] = version;
SetPrivateVariable("Version", version, @event.Entity);
return false;
}
public void SetPrivateVariable(string name, object value, object entity)
{
var entityType = entity.GetType();
var pi = entityType.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
pi.ReflectedType.BaseType.InvokeMember(name,
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance,
null,
entity,
new[] { value });
}
public T GetPrivateVariable<T>(string name, object entity)
{
var entityType = entity.GetType();
var pi = entityType.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
return (T)pi.GetValue(entity, null);
}
}
Now if you follow this code, when an entity is saved for the first time then the 'CreatedOn' & 'LastModified' property values are set to the current date & time and the 'Version' property is incremented (the initial value is 0) via the private backing properties. When the entity is updated only the 'LastModified' & 'Version' properties are set via the private backing properties.
This event listener is then instantiated via the fluent NH config.
private static ISessionFactory CreateSessionFactoryImpl()
{
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2005.ConnectionString(c => c.FromConnectionStringWithKey("Pronunciations")).ShowSql())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<IMap>())
.ExposeConfiguration(c =>
{
c.EventListeners.PreInsertEventListeners = new IPreInsertEventListener[] { new EntitySaveOrUpdateEventListener() };
c.EventListeners.PreUpdateEventListeners = new IPreUpdateEventListener[] { new EntitySaveOrUpdateEventListener() };
})
.BuildSessionFactory();
}
Awkward Coder
0 comments:
Post a Comment