ASP.NET MVC 2: Lokalizowanie modelu danych przy użyciu Model MetaData Provider

Jak wiadomo w ASP.NET MVC 2 można określać etykiety poszczególnych właściwości modelu poprzez atrybut DisplayName. W przypadku kiedy chcemy mieć z lokalizowany opis to jest mały problem zwłaszcza jeśli jest potrzeba uruchomić to na .NET 3.5 gdzie DisplayName nie posiada dodatkowych możliwości podana zasobów i etykiet z którego może pobrać opis. A co w przypadku kiedy atrybut ten już ma takie możliwości (.NET 4.0) ale nie ma dostępu do zasobów bo jest w zewnętrznej bibliotece a zasoby są dopiero dostępne w samej aplikacji?
Jednym z działających rozwiązań jest następujące:

...
[DisplayName("X:Code")]
public string Code {get; set;}
...

A więc używając atrybutu DisplayName ustaliłem specjalny zapis nazwy - klucza.
Wykorzystują dostępny model providerów w ASP.NET MVC 2 utworzyłem sobie własny provider który przetwarza atrybuty właściwości w modelu danych. Wyprowadziłem sobie własną klasę z DataAnnotationsModelMetadataProvider w której sprawdzam atrybut DisplayName dla danej właściwości modelu danych i jeśli jest według mojego klucza - wzoru to staram się znaleźć tłumaczenie w lokalnych zasobach aplikacji.


public class LocalizedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable attributes, Type containerType, Func < object > modelAccessor, Type modelType, string propertyName)
{
var meta = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (string.IsNullOrEmpty(propertyName))
{
return meta;
}
if (meta != null && !string.IsNullOrEmpty(meta.DisplayName) && meta.DisplayName.StartsWith("X:", StringComparison.Ordinal))
{
string key = meta.DisplayName.Replace("X:", "");
meta.DisplayName = Resources.Strings.ResourceManager.GetString(key, Resources.Strings.Culture);
if (string.IsNullOrEmpty(meta.DisplayName))
{
meta.DisplayName = key;
}
}
if (string.IsNullOrEmpty(meta.DisplayName))
{
meta.DisplayName = string.Format(CultureInfo.InvariantCulture, "{0}", propertyName);
}
return meta;
}
}

Potem zostaje mi to zarejestrować w aplikacji poprzez plik Global.asax np w ten sposób:

protected void Application_Start()
{
...
ModelMetadataProviders.Current = new LocalizedDataAnnotationsModelMetadataProvider();
...
}

Dzięki takiemu mechanizmowi mogę dynamicznie tłumaczyć opisy właściwości modelu a i model zdefiniować jako nie zależny od zasobów aplikacji.

NHibernate i audyt encji

Podczas pisania aplikacji bazodanowych często jest potrzeba zapisywania informacji na temat audytu danej encji. Tylko jak zrobić żeby się nie narobić :). W NHibernate które ostatnio zacząłem używać jest bardzo fajny i wygodny sposób na to. Jak wiemy ten system ORM posiada funkcjonalność jak zdarzanie które są odpalane w różnych momentach np przed zapisem, utworzeniem, kasowaniem itd.
Tworzymy więc sobie interfejs który będzie miał zadeklarowane właściwości opisujące audyt np.

public interface IAuditObject
{
DateTime CreatedDateTime { get; set; }

int CreatedUserId { get; set; }

DateTime ModifyDateTime { get; set; }

int ModifyUserId { get; set; }
}

Teraz wyposażamy klasę naszej encji w ten interfejs. Oczywiście trzeba uzupełnić pliki mapowania o te informacje.
Do przeprowadzenia audytu wykorzystamy dwa zdarzenia które są dostępne: IPreInsertEventListener oraz IPreUpdateEventListener. Te zdarzenia są wywoływane przed dodaniem/zapisem encji. Omówimy teraz sposób implementacji IPreInsertEventListener bo te drugie zdarzenie będzie praktycznie wyglądało bardzo podobnie a różnica będzie w postaci mniejszej ilości pól do zmienienia. A więc jej implementacja mogła by wyglądać następująco:

public bool OnPreInsert(PreInsertEvent @event)
{
if (UpdateAuditInformation(@event.Entity))
{
var entity = @event.Entity as IAuditObject;

Set(@event.Persister, @event.State, "CreatedDateTime", entity.CreatedDateTime);
Set(@event.Persister, @event.State, "ModifyDateTime", entity.ModifyDateTime);
Set(@event.Persister, @event.State, "CreatedUserId", entity.CreatedUserId);
Set(@event.Persister, @event.State, "ModifyUserId", entity.ModifyUserId);
}
return false;
}

W klasie PreInsertEvent są m.in następujące rzeczy:
- instancja naszej encji na której dokonujemy wpisu audytu @event.Entity
- instancja klasy mapującej i zapisującej naszą encje @event.Persister
- tablica wartości które zostaną dodane/zapisane do bazy @event.State
.

Teraz musimy ustawić informacje na temat audytu w naszej encji oraz tablicy stanów. Aby rozpoznać co gdzie leży w tej tablicy możemy skorzystać z obiektu Persister i jego właściwości PropertyNames.
Przykładowa implementacja metody pomocniczej to:

private static void Set(IEntityPersister persister, object[] state, string propertyName, object value)
{
var index = Array.IndexOf(persister.PropertyNames, propertyName);
if (index != -1)
{
state[index] = value;
}
}


Metoda UpdateAuditInformation ustawia nam na encji odpowiednie wartości czasu utworzenia/modyfikacji oraz np ID użytkownika który
wykonał operacje.
Zostaje nam tylko teraz powiadomienie NHibernate o naszej obsłudze tych zdarzeń a możemy to zrobić np:

...
var configuration = new Configuration();
configuration.Configure();

var auditor = new DatabaseAuditor();
configuration.SetListener(ListenerType.PreInsert, auditor);
configuration.SetListener(ListenerType.PreUpdate, auditor);

_sessionFactory = configuration.BuildSessionFactory();
...