MongoDB – ViewModel

Po pierwszych testach projektu zauważyłem, że wywołania REST API zwracają za dużo danych. Było to spowodowane zwracaniem modelu domenowego, który nie jest odpowiedni do tego typu operacji. Dlatego częstym zabiegiem jest wprowadzenie tzw. view model, który odpowiada danym zwracanym do interfejsu użytkownika czy wywołań API. Zazwyczaj jest on „szczuplejszy” i prostszy od domenowego.
W tym poście chciałbym pokazać rozwiązanie oparte o bazę danych MongoDB i metody generyczne.

Dogevents – domain model

IEvent – główny model domenowy. Ten interfejs zawiera wszystkie informacje o wydarzeniu. Jest złożoną strukturą, która odzwierciedla wydarzenia pobrane z Facebook przy użyciu Graph Api.

public class Event
{
[BsonId]
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }

[JsonProperty("start_time")]
public DateTime StartTime { get; set; }

[JsonProperty("end_time")]
public DateTime EndTime { get; set; }

[BsonIgnore]
public string Url { get => $"https://www.facebook.com/events/{Id}/"; }

public string CoverUrl { get => Cover.source; }

[JsonProperty("is_canceled")]
public bool IsCanceled { get; set; }

public Cover Cover { get; set; }
public Owner Owner { get; set; }
public Place Place { get; set; }
public IEnumerable<string> Categories { get; set; }
}

IViewEventModel – Podstawowy interfejs na potrzeby widoków. Stworzony do oznaczenia metod generycznych – tak aby móc wykorzystywać w nich pewne właściwości niezbędne do zapytań bazodanowych.

public interface IViewEventModel
{
long Id { get; set; }
string Name { get; set; }
DateTime StartTime { get; set; }
}

View service

Aby móc wykorzystywać serwis pobierający wydarzenia z bazy MongoDB z różnymi modelami zadeklarowałem metody generyczne z ograniczeniem, że zwracany model musi implementować IViewEventModel. Jest to podyktowane tym, iż implementacja niektórych metod musi posiadać dostęp do specyficznych właściwości modelu, np. metoda GetIncoming<T>() wykorzystuje pole StartTime do zdefiniowania filtru na tym polu. Pominięcie takiego „wskazania” (z ang. constraint) uniemożliwiło by to.

public interface IViewEventsService
{
Task<List<T>> GetPopular<T>() where T : IViewEventModel;

Task<List<T>> GetIncoming<T>() where T : IViewEventModel;

Task<List<T>> GetJustAdded<T>() where T : IViewEventModel;
}

View models

Następnie zdefiniowałem kilka klas, które będą zwracały tylko potrzebne w danym momencie dane. Dla przykładu jedna z klas, która zawiera tylko dane potrzebne do wyświetlania nagłówka wydarzenia, bez jego szczegółów.

public class EventCardHeaderViewModel : IViewEventModel
{
public long Id { get; set; }
public string Name { get; set; }
public DateTime StartTime { get; set; }

public string Url { get => $"https://www.facebook.com/events/{Id}/"; }
}

Przykład użycia

[HttpGet]
[Route("GetPopular")]
public async Task<IEnumerable<EventCardViewModel>> GetPopular()
{
return await _viewEventService.GetPopular<EventCardViewModel>();
}

Powyżej przykład wywołania metody serwisowej ze wskazaniem na jaki model mają zostać zmapowane dane. Jest tylko jedno ale. Nie została wykonana pewna konfiguracja czy też zastosowana konwencja jak ma się zachowywać MongoDB podczas deserializacji danych na nasz model. Domyślnie, jeśli zabraknie definicji któregoś z pól otrzymamy wyjątek typu FormatException:

MongoDB konwencje

Aby rozwiązać ten problem należy odpowiednio skonfigurować mechanizm serializacji w MongoDB używając mechanizmu konwencji. API MongoDB udostępnia szereg wbudowanych „pakietów zachowań”. Pełną listę można znaleźć tutaj: MongoDB Serialization Convetions Do rozwiązania tego problemu użyłem IgnoreExtraElementsConvention. Sama rejestracja odbywa się z użyciem klasy ConventionRegistry:

private static void RegisterConventions()
{
ConventionRegistry.Register("DogeventsConvetion", new MongoConvention(), x => true);
}

private class MongoConvention : IConventionPack
{
public IEnumerable<IConvention> Conventions => new List<IConvention>
{
new IgnoreExtraElementsConvention(true),
};
}