QueryStringValue w Web Client Software Factory
Web Client Software Factory udostępnia bardzo ciekawy i przydatny mechanizm komunikacji ze stanem przechowywanym w sesji. W poniższym przykładzie podczas tworzenia obiektu do pola zostanie wstrzyknięta odpowiednia wartość pobrana z sesji:
1: public class MyClass
2: {
3: [SessionStateKey("MyNumber")]
4: public StateValue<int> MyNumber;
Do wartości tej dostać się można następująco:
1: int number = MyNumber.Value;
Wszystko za sprawą Object Buildera. Jakie korzyści płyną z zastosowania takiego rozwiązania? Oprócz ustandaryzowanego i prostego sposobu wykorzystania sesji najważniejsza jest możliwość przeprowadzenia testów jednostkowych na obiektach polegających na wartościach pobieranych z sesji.
Ale ja nie do końca o tym chciałem... Kilka tygodni temu Kuba Binkowski w swoim wystąpieniu na wg.net pokazał podobne rozwiązanie w Unity, tyle że pobierające wartość z URL. No i właśnie coś takiego dodamy za chwilę do WCSF.
Cały proces rozpoczyna się od odpalenia Reflectora i analizy szczegółów implementacyjnych StateValue, ponieważ funkcjonalność będzie praktycznie ta sama. A potem - sam miód, czyli implementacja. Zatem po kolei, do dzieła!
1. Pierwszy krok to utworzenie interfejsu analogicznego do IStateValue. Interfejs ten zdefiniuje nam kontrakt komunikacyjny pomiędzy naszym systemem a URLem i będzie prawie taki sam jak wspomniane IStateValue. Jedyna różnica to typ zwracany przez właściwość Value. W naszym przypadku będzie to string, ponieważ to właśnie możemy z URLa wyciągnąć. Dodatkowo należy zwrócić uwagę na właściwość Request - nie wołamy bezpośrednio HttpContext.Current.Request. Zamiast tego uzupełnimy tą wartość później, korzystając z innych mechanizmów dostępnych w WCSF.
1: public interface IQueryStringValue
2: {
3: string KeyName { get; set; }
4: string Value { get; }
5: HttpRequest Request { get; set; }
6: }
2. Następnie wypada interfejs ów zaimplementować. Tworzona klasa będzie wykorzystywać typ ogólny, dzięki czemu uzyskamy możliwość konwersji z ciągu znaków do liczby czy daty bez ingerencji końcowego programisty.
1: public class QueryStringValue<T> : IQueryStringValue
2: {
3: public string KeyName { get; set; }
4:
5: public HttpRequest Request { get; set; }
6:
7: string IQueryStringValue.Value
8: {
9: get { return Request.QueryString[KeyName]; }
10: }
11:
12: public T Value
13: {
14: get
15: {
16: var v = ((IQueryStringValue)this).Value;
17:
18: if (string.IsNullOrEmpty(v))
19: return default(T);
20:
21: return (T)Convert.ChangeType(v, T);
22: }
23: }
24: }
3. Mamy już strukturę potrzebną do przechowywania danych pobranych z QueryStringa. Zauważmy jednak, że brakuje jeszcze odpowiednika atrybutu SessionStateKey będącego znakiem dla ObjectBuildera że w tym miejscu należy się zatrzymać i "coś zrobić". Implementacja takiego oznaczenia jest banalnie prosta, ponieważ tak naprawdę jedyne czego potrzebujemy to klucz pod którym należy szukać żądanej wartości:
1: [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
2: public sealed class QueryStringKeyAttribute : Attribute
3: {
4: public string QueryStringKey { get; private set; }
5:
6: public QueryStringKeyAttribute(string queryStringKey)
7: {
8: QueryStringKey = queryStringKey;
9: }
10: }
4. Zatrzymajmy się na chwilę i spójrzmy co już napisaliśmy. Cała instrastruktura konieczna do wykorzystania mechanizmu jest gotowa. W akcji będzie to wyglądać tak:
1: [QueryStringkey("MyNumber")]
2: private QueryStringValue<int> SomeNumber;
...przy czym obsługiwany URL to http://www.xxx.com/yyy.aspx?MyNumber=666.
Ale to oczywiście nie koniec. Nigdzie jeszcze fizycznie nie dobieramy się do adresu, nigdzie nie wciskamy się w proces tworzenia obiektu przez ObjectBuilder! Kroczmy zatem dalej ścieżką prawych i sprawiedliwych. Jesteśmy blisko!
5. Teraz czeka nas najtrudniejsze zadanie - musimy napisać własną strategię ObjectBuildera, która powypełnia wszystkie pola oznaczone tym ślicznym atrybutem. Bez kilkukrotnego zerknięcia do Reflectora na implementację SessionStateBindingStrategy się nie obejdzie, ale w końcu po coś to narzędzie mamy, prawda? Tak więc wykonujemy wszystkie konieczne czynności, które ot tak wymienię jedna po drugiej:
- za pomocą WCSFowego pojemnika IoC uzyskujemy instancję usługi, która dostarczy nam aktualny kontekst HTTP (a więc i obiekt HttpRequest)
- przejeżdżamy się po wszystkich polach tworzonego obiektu pomijając te mające inny typ niż implementację naszego interfejsu IQueryStringValue
- z deklaracji w/w pól pobieramy instancję atrybutu QueryStringKey zawierającą klucz wskazujący na żądaną wartość w URLu (poniższa implementacja wyrzuci wyjątek, gdy pole takiego typu nie zostanie oznaczone takim atrybutem)
- wstawiamy w owo pole nowa instancję pożądanego typu, wypełniając jego właściwości przechowujące Key oraz Request
W tym momencie mamy już pole, którego właściwość Value zwróci nam to czego oczekujemy!
1: public class QueryStringBindingStrategy : BuilderStrategy
2: {
3: public override object BuildUp(IBuilderContext context, System.Type typeToBuild, object existing, string idToBuild)
4: {
5: IHttpContextLocatorService service = context.Locator.Get<IHttpContextLocatorService>(new DependencyResolutionLocatorKey(typeof(IHttpContextLocatorService), null));
6:
7: if (service != null)
8: {
9: var httpContext = service.GetCurrentContext();
10: foreach (var field in typeToBuild.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
11: {
12: if (typeof(IQueryStringValue).IsAssignableFrom(field.FieldType) == false)
13: continue;
14:
15: IQueryStringValue queryStringValue = (IQueryStringValue)Activator.CreateInstance(field.FieldType);
16: QueryStringKeyAttribute attribute = (QueryStringKeyAttribute)field.GetCustomAttributes(typeof(QueryStringKeyAttribute), false)[0];
17:
18: queryStringValue.KeyName = attribute.QueryStringKey;
19: queryStringValue.Request = httpContext.Request;
20: field.SetValue(existing, queryStringValue);
21: }
22: }
23:
24: return base.BuildUp(context, typeToBuild, existing, idToBuild);
25: }
26: }
Jeszcze jedna mała uwaga: powyższa implementacja pozwala na potraktowanie w ten sposób WSZYSTKICH, także prywatnych, pól. Dostępny w WCSF mechanizm można zastosować jedynie do pól publicznych. Powód? Linijka numer 10. Tutaj jawnie żądamy dostępu do pól publicznych i niepublicznych, podczas gdy implementacja strategii StateValue wykorzystuje bezparametryczną wersję metody GetFields() zwracającą jedynie publiczne pola.
6. Został ostatni kroczek. Musimy powiedzieć ObjectBuilderowi że mamy o taki cudny mechanizm, który chcielibyśmy w proces tworzenia obiektów wprząc. Reflector w kilka chwil pokazuje nam w którą stronę gęby nasze należy zwrócić i co uczynić, aby było to możliwe. Rozwiązaniem jest własna klasa dziedzicząca z WebClientApplication nadpisująca jedną metodę:
1: public class CustomWebApplication : WebClientApplication
2: {
3: protected override void AddBuilderStrategies(Microsoft.Practices.ObjectBuilder.IBuilder<WCSFBuilderStage> builder)
4: {
5: base.AddBuilderStrategies(builder);
6:
7: builder.Strategies.AddNew<QueryStringBindingStrategy>(WCSFBuilderStage.Initialization);
8: }
9: }
A co dalej z tą klasą zrobić - było ostatnio.
That's all folks!
Żeby nie było tak słodko dodam, że całą tą pracę wykonałem właściwie na marne. Po napisaniu owego rozwiązania wpisałem z ciekawości w Google "WCSF QueryStringValue" i... co się okazało? Istnieje sobie projekcik WCSF Contrib i tam dokładnie takie samo rozwiązanie siedzi już od jakiegoś czasu. No ale, co się człowiek nauczy to jego.
Jest też jeszcze jedna sprawa. W WCSF możemy wykorzystywać sesję (jak to brzmi...) również w inny sposób. Istnieje sobie atrybut StateDependency, dzięki któremu możemy podobne sztuczki robić z parametrami metod! To jednak dużo wyższa szkoła jazdy - bez zagłębienia się po uszy w kod źródłowy WCSF i jego modyfikacji się nie obejdzie. Zatem odpowiednik, czyli QueryStringDependency (którego już w WCSF Contrib nie uświadczymy, co jest absolutnie zrozumiałe jeśli się poprzednie zdanie jeszcze raz przeczyta), stworzymy sobie może innym razem.