WebAPI¶
WebAPI to standard komunikacji pomiędzy klientami a serwerem. Transport odbywa się za pośrednictwem protokołu HTTP a dane mogą być przesyłane w różnych formatach (najpopularniejsze to JSON i XML). Zbiór funkcjonalności udostępnianych za pośrednictwem WebAPI nazywamy serwisem albo usługą.
Niniejszy rozdział mówi o tym, w jaki sposób możemy przy pomocy Neosa stworzyć serwis WebAPI (stronę serwerową), aby udostępniać komuś z zewnątrz dane lub usługi.
Od zera do serwera w kilku prostych krokach¶
Utworzenie metody zwracającej dane¶
Przyjmijmy, że chcesz stworzyć WebAPI udostępniające dane o stanach magazynowych. W tym celu, w Neos Expercie znajdujesz obiekt STANYIL i przechodzisz do okna metod C#. U góry w rozwijanym menu znajdziesz funkcję Dodaj metodę WebAPI... -> do zwrócenia listy elementów. Po jej wywołaniu zostanie dodana następująca metoda:
[HttpGet]
public static List<STANYILEntity> GetAll()
{
// Możesz zamienić STANYILEntity na strukturę danych, którą chcesz zwrócić.
// Jeśli nie masz jeszcze zadeklarowanej struktury utwórz ją poprzez funkcję 'Dodaj strukturę danych do WebAPI'
// Sposób użycia: GET /api/stanyil
var list = new List<STANYILEntity>();
var obj = new STANYIL();
if(obj.FirstRecord()) do {
var item = new STANYILEntity();
obj.AssignTo(item);
list.Add(item);
} while(obj.NextRecord());
return list;
}
Jeśli przyjrzysz się treści tej metody, to zobaczysz, iż otwiera ona źródło danych STANYIL i iteruje po wszystkich rekordach z danymi. Dla każdego rekordu tworzy strukturę STANYILEntity i przypisuje do niej dane bieżącego rekordu. Lista takich struktur jest zwracana jako wynik metody.
Zwalnianie zasobów w metodach WebAPI
Obiekty źródeł danych oraz logiki biznesowej (np. new STANYIL(), new PROJEKT.LOGIC.OBIEKT()) powinny być tworzone w bloku using, aby były zamykane zaraz po użyciu i nie powodowały narastania zużycia pamięci przez aplikację. Jest to szczególnie istotne właśnie w metodach WebAPI. Szczegóły i przykłady poprawnego użycia znajdują się w rozdziale Jak korzystać z dostępu do bazy danych w warstwie logiki biznesowej.
Skonfigurowanie webserwisu¶
Aby uruchomić webserwis trzeba w Neos Expercie w oknie Developer -> Pozostałe -> Zarządzaj uprawnieniami (dawniej: Konfiguracja -> Neos -> Zarządzaj uprawnieniami) utworzyć nowy webserwis. W okienku edycyjnym trzeba ustawić podstawowe parametry konfiguracyjne:
- nazwę roli/webserwisu
- adres i port na którym webserwis będzie działał; do testów najczęściej będzie to wpis typu
localhost:9xxx - procedura logująca jest procedurą w bazie danych, używaną do weryfikacji danych uwierzytelniających podanych przez klienta
- czy usługa ma być dostępna na adresie https
- czy usługa jest aktywna
Od momentu kliknięcia zapisz usługa zostanie włączona, o czym zostaniemy powiadomieniu w oknie monitora serwera i debuglogu. Nie ma ona jednak przypisanych żadnych metod. Aby wybrać metody dostępne w ramach danego webserwisu, musimy w prawej częsci okna nadawania uprawnień nadać granty do wybranych obiektów zawierających metody webserwisu (np. do obiektu STANYIL) i kliknąć przycisk Odśwież webserwisy.
Jeżeli zmienimy parametry konfiguracyjne usługi, zaczynają one działać od razu w momencie zapisania zmian, gdyż usługa jest zatrzymywana i uruchamiana na nowo z nowymi parametrami. Z kolei po dododaniu/usunięciu metod lub obiektów które mają wchodzić w skład webserwisu konieczne jest kliknięcie przycisku Odśwież webserwisy, bądź skompilowanie projektu domyślnego.
Uwaga!
Pamiętaj, że serwer uruchomi tylko te webserwisy, które są zdefiniowane w projekcie domyślnym wskazanym w pliku smd oraz w tych projektach, które projekt domyślny ma na swojej liście referencji. Brak spełnienia tego warunku jest najczęstszą przyczyną nie działania webapi.
Użycie¶
WebAPI jest już gotowe. Otwórz teraz przeglądarkę i wpisz:
http://localhost:9xxx/api/stanyil
Zobaczysz strukturę danych wygenerowanych przez utworzoną przed chwilą metodę STANYIL.GetAll(). Gratuluję, utworzyłeś właśnie swoje pierwsze WebAPI.
Anatomia WebAPI¶
Użycie własnych struktur danych¶
W powyższym przykładzie użyto strukturę STANYILEntity. Jest to domyślna struktura danych, generowana przez Neosa dla każdego obiektu. A więc istnieje także, KLIENCIEntity, DOSTAWCYEntity, NAGFAKEntity, idt. Struktura ta zawiera wszystkie pola modelu danych obiektu.
Jeśli potrzebujesz zwrócić tylko wybrane pola obiektu, możesz założyć własną strukutrę danych. W oknie edycji metod C#, ustaw się np na obiekcie STANYIL i z rozwijanego menu u góry wybierz opcję Dodaj strukturę danych dla WebAPI. Pokaże się okno, w którym możesz wpisać nazwę struktury (np. STANYILInfo) oraz wybrać pola, które chcesz umieścić w tej strukturze. Wygenerowana struktura może wyglądać tak:
public class STANYILInfo
{
public String MAGAZYN { get; set; }
public Decimal? ILOSC { get; set; }
public String KTM { get; set; }
public String NAZWAT { get; set; }
public Decimal? ZABLOKOW { get; set; }
public Decimal? ZAMOWIONO { get; set; }
public Decimal? ZAREZERW { get; set; }
}
Zmień teraz kod metody GetAll() tak aby strukturę STANYILEntity zastąpić strukturą STANYILInfo. Skompiluj projekt SENTE i uruchom ponownie zapytanie pod przeglądarką. Zobaczysz teraz dane wynikowe w odchudzonej strukturze danych.
Zauważ, że niezależnie, czy używałeś struktury STANYILEntity czy STANYILInfo, jej wypełnieniem zajmuje się metoda AssignTo(object target). Jest to metoda uniwersalna, która przy pomocy refleksji analizuje wszystkie właściwości (properties) dowolnej struktury danych i przypisuje im wartości na podstawie mapowania nazw pól modelu danych, na nazwy właściwości struktury. Wszystkie automatycznie generowane struktury danych są w pełni mapowane, natomiast gdybyś do struktury STANYILInfo dodał własne pole, np takie:
public string MojePole { get; set; }
to metoda AssignTo(...) pozostawi je puste.
Aby temu zaradzić należy przeciążyć metodę AssignTo(...) np w taki sposób:
public object AssignTo(object target)
{
base.AssignTo(target);
if(target is STANYILInfo) {
var item = target as STANYILInfo;
item.MojePole = this.JAKIESINNEPOLE + "abcd"; // <- tutaj wyliczamy MojePole
}
return target;
}
Wywołanie metody bazowej powoduje pozostawienie mapowania automatycznego bez zmian, a dodatkowy kod mapuje te właściwości, które nie mogą być mapowane automatycznie.
W ten sam sposób można tworzyć i wypełniać bardzo złożone struktury danych. Zajrzyj do obiektu SENTE.KLIENCI a zobaczysz, że struktura DaneKlienta zawiera oprócz zwykłych pól także adresy w postaci listy struktur AdresKlienta, a metoda AssignTo(...) buduje tą listę.
Tip
Zaleca się aby wszystkie pola zwracanych struktur danych były "nullable", czyli dopuszczały wartość null. Stąd znaki zapytania przy niektórych typach. Będzie to miało szczególne znaczenie przy opracjach CRUD przez WebAPI, gdyż pozwoli przekazywać jedynie wybrane fragmenty struktur.
Szybkie generowanie dużej ilości danych¶
Jeśli zamierzasz zwracać duże ilości danych przez WebAPI wiedz, że jest to możliwe, ale zapoznaj się z rozdziałem dotyczącym wydajności. Dwie rzeczy, które napewno warto zrobić, to:
- rezygnacja z uniwersalnego kodu metody AssignTo(...) i ręczne mapowanie wszystkich potrzebnych pól.
- bezpośrednie zadanie zapytania SQL w celu pobrania danych.
Jeśli kod metody AssignTo(...) zmienisz na poniższy, to staje się ona mniej elastyczna, ale za to działa około 5 razy szybciej. Musisz pamiętać o modyfikacji kodu tej metody po każdej zmianie struktury danych.
public object AssignTo(object target)
{
if(target is STANYILInfo)
{
STANYILInfo item = target as STANYILInfo;
item.KTM = this.KTM;
item.MAGAZYN = this.MAGAZYN;
item.NAZWAT = this.NAZWAT;
item.ILOSC = this.ILOSC.AsNumeric;
item.ZABLOKOW = this.ZABLOKOW.AsNumeric;
item.ZAMOWIONO = this.ZAMOWIONO.AsNumeric;
item.ZAREZERW = this.ZAREZERW.AsNumeric;
} else {
base.AssignTo(target);
}
return target;
}
Jeśli metodę GetAll() napiszesz w sposób wykorzystujący DB.OpenTable(...) zamiast new STANYIL, to uzyskasz jeszcze około dwukrotne przyspieszenie. Ponieważ cała dziedzina wyniku zapytania będzie analizowana raz, zaleca się użycie trybu NFillMode.Full.
[HttpGet]
public static List<STANYILInfo> GetAll()
{
// Sposób użycia: GET /api/stanyil
var list = new List<STANYILInfo>();
NDataView dv = DB.OpenTable("","","select * from STANYIL", NFillMode.Full, "", "*");
if(dv==null) return null;
if(dv.FirstRecord()) do {
var item = new STANYILInfo();
item.KTM = dv["KTM"];
item.MAGAZYN = dv["MAGAZYN"];
item.NAZWAT = dv["NAZWAT"];
item.ILOSC = dv["ILOSC"].AsNumeric;
item.ZABLOKOW = dv["ZABLOKOW"].AsNumeric;
item.ZAMOWIONO = dv["ZAMOWIONO"].AsNumeric;
item.ZAREZERW = dv["ZAREZERW"].AsNumeric;
list.Add(item);
} while(dv.NextRecord());
dv.Close();
return list;
}
Filtrowanie danych¶
Czasami bardziej użyteczne niż zwrócenie wszystkich danych jest zwrócenie tylko wybranych danych, przefiltrowanych parametrem. Przyjmijmy, że interesują Cię stany magazynowe konkretnego towaru we wszystkich magazynach. W tym celu wygeneruj drugą metodę STANYIL.GetAll() ale zaraz po jej wygenerowaniu, zmień jej kod w następujący sposób:
[HttpGet]
public static List<STANYILEntity> GetForKTM(string ktm)
{
// Sposób użycia: GET /api/stanyil?ktm=...
var list = new List<STANYILEntity>();
var obj = new STANYIL();
obj.FilterAndSort("KTM='"+ktm+"'");
if(obj.FirstRecord()) do {
var item = new STANYILEntity();
obj.AssignTo(item);
list.Add(item);
} while(obj.NextRecord());
return list;
}
Zauważ, że zmeniono nazwę metody i dodano jej parametr oraz dodano kod filtrujący dane tabeli STANYIL. Po skompilowaniu projektu możesz wywołać takie WebAPI z przeglądarki w następujący sposób:
http://localhost:9xxx/api/stanyil?ktm=AKBGB7000001
Tip
Zauważ, że w przeglądarce nigdy jawnie nie podajesz nazwy metody do wywołania. Wybierana jest zawsze najlepiej pasująca metoda pod kątem przekazywanych parametrów, wśród metod z atrybutem [HttpGet]
W analogiczny sposób towrzy się metodą zwracającą konkretny rekord. W obiekcie SENTE.KLIENCI wygenerowano i lekko zmodyfiowano metodę Get(int id) tak, aby zwracała dane konkrentego klienta. Parametr int id jest specyficzny, gdyż w przeglądarce można użyć go na dwa sposoby:
http://localhost:9xxx/api/klienci/123
http://localhost:9xxx/api/klienci?id=123
Operacje CRUD¶
W oknie metod C# Neos Experta możesz także wygenerować kod metod do dodawania, modyfikacji i usuwania rekordu. Przykładowe meody wygenerowano w obiekcie SENTE.KLIENCI.
Przyjrzyjmy się procesowi zakładania nowego klienta. W tym celu wygenerowano metodę Put(...), przy czym wymieniono strukturę danych KLIENCIEntity na przygotowaną wcześniej strukturę DaneKlienta.
[HttpPut]
public static DaneKlienta Put(DaneKlienta item)
{
// Sposób użycia: PUT /api/klienci
var obj = new KLIENCI();
obj.NewRecord();
obj.AssignFrom(item);
obj.PostRecord(true,true);
obj.AssignTo(item);
return item;
}
Metoda ta, tworzy źródło danych KLIENCI i inicjuje w nim tworzenie nowego rekord przez wywołanie NewRecord(). Nstępnie wywoływana jest metoda AssignFrom(...), która działa analogicznie jak AssignTo(...) tylko mapuje dane w przeciwnym kierunku, czyli z przekazanej struktury do obiektu biznesowego. Ponieważ użyto własnej struktury danych DaneKlienta, to metoda AssignFrom(...) została podziedziczona i zawiera własny kod przepisujący dane. Jeśli użylibyśmy standardowej struktury KLIENCIEntity wystarczyłby standardowy kod AssignFrom(...). Następnie wykonujemy zapis danych poprzez wywołanie PostRecord(true,true). Pierwszy parametr ustawiony na true oznacza, że źródło danych ma ustawić się na tym rekordzie po jego dodaniu (bez tego parametru nie jest to robione). Drugi parametr oznacza, że w przypadku błędu, metoda ma zwrócić wyjątek. Bez tego parametru Post nie generuje wyjątku, ale zwraca false. W WebAPI wyjątki są wygodnym sposobem na zwrócenie szczegółowej informacji o błędzie do klienta WebAPI, co ułatwia określenie przyczyny niepowodzenia tej operacji. Na końcu jest jeszcze wywołane AssignTo(...). Chodzi o to aby pełen rekord po dodaniu został zwrócony przez API. Pozwala to zwrócić wartości pól, które nadały się same, np REF klienta.
Wywołanie metody Put() nie jest proste pod zwykłą przeglądarką, gdyż wszystkie adresy url, pobierane są poleceniem GET. Do testów zalecamy zainstalowanie rozszerzenia Google Chrome o nazwie Postman.
W Postmanie możemy wywołać następującą komendę
localhost:9011/api/klienci (PUT)
przekazując jej następującą strukturę JSON w postaci surowej (RAW).
{
"FSKROT": "nowy klient",
"NAZWA": "nowy klient"
}
Po wykonaniu polecenia otrzymamy mniej więcej następujący wynik.
{
"REF": 104956,
"AKTYWNY": 1,
"FIRMA": 1,
"FSKROT": "nowy klient",
"GRUPA": 50,
"LIMITKR": 0,
"NAZWA": "nowy klient",
"TERMIN": 0,
"ZAGRANICZNY": 0,
"Adresy": [
{
"TYP": 0,
"KODP": "",
"KRAJ": "Polska",
"MIASTO": "",
"ULICA": ""
},
{
"TYP": 1,
"KODP": "",
"KRAJ": "Polska",
"MIASTO": "",
"ULICA": ""
},
{
"TYP": 2,
"KODP": "",
"KRAJ": "Polska",
"MIASTO": "",
"ULICA": ""
}
]
}
Jest to rekord danych nowo założonego klienta w postaci JSON. Warto zaznaczyć, że oprócz zadanego skrótu i nazwy, triggery w BD oraz metody na inicjalizację danych w Neosie nadały wartości także różnym innym polom, które zostały zwrócone w wyniku. Dane tego klienta możemy otrzymać za każdym razem, wywołując:
localhost:9011/api/klienci/104956
Inne funkcje¶
Jeśli chcielibyśmy przy pomocy WebAPI wykonywać operacje nie związane z CRUD, a na przykład akceptować faktury, najlepiej przyjąć konwencję, wg której mamy w obiekcie jedną metodę, działającą analogicznie jak metoda Get(int id) ale z dodatkowym parametrem o nazwie action. Przykładowa metoda akceptująca fakturę zdefiniowana w obiekcie NAGFAK może wyglądać następująco:
[HttpGet]
public static bool DoAction(int id, string action)
{
// Sposób użycia: GET /api/nagfak/123?action=accept
switch(action) {
case "accept" :
//wykonaj kod akceptujący fakturę o zadanym ID, najlepiej przez wywołanie procedury SQL
//jeśli akcja się nie uda, wyrzuć wyjątek
return true;
}
return false; //zwróc false jeśli nie rozpoznano akcji
}
Upload pliku¶
Jeśli chcemy przesłać plik na serwer za pomocą WebAPI, możemy to zrobić za pomocą następującego kodu:
[HttpPost]
public void UploadFile(List<FormFile> files)
{
string directoryPath = @".\Uploads";
foreach (var file in files)
{
// Zapisz plik na serwerze
string filePath = Path.Combine(directoryPath, file.FileName);
file.SaveAsAsync(filePath).Wait();
}
}
Obsługa klasy FormFile weszła w wersji Neosa 6.2. Jesli Neos nie obsługuje jeszcze klasy FormFile, to upload plików można zrealizować w następujący sposób:
[HttpPost]
public void UploadFile(HttpRequestMessage request)
{
string directoryPath = @".\Uploads";
if (!request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(System.Net.HttpStatusCode.UnsupportedMediaType);
}
var provider = request.Content.ReadAsMultipartAsync().Result;
foreach (var file in provider.Contents)
{
var fileName = file.Headers.ContentDisposition.FileName?.Trim('\"');
if (!string.IsNullOrWhiteSpace(fileName))
{
// Zapisz plik na serwerze
var filePath = Path.Combine(directoryPath, fileName);
using (var fs = new FileStream(filePath, FileMode.Create))
{
file.CopyToAsync(fs).Wait();
}
}
}
}
Wysyłanie plików na taki endpoint odbywa się z pomocą "multipart/form-data". Poniżej przykładowy kod, który wysyła plik.
using (var client = new HttpClient())
using (var content = new MultipartFormDataContent())
using (var fileStream = new FileStream(filePath, FileMode.Open))
{
var fileName = Path.GetFileName(filePath);
// Dodaj plik do treści żądania
content.Add(new StreamContent(fileStream), "files", fileName);
var response = await client.PostAsync(apiEndpoint, content);
if (response.IsSuccessStatusCode)
{
Console.WriteLine("Plik został przesłany i zapisany na serwerze.");
}
else
{
Console.WriteLine($"Błąd: {response.StatusCode}");
}
}
Użycie DateTime¶
Używając DateTime jako parametr WebAPI musimy pamiętać o tym aby był on w formacie "yyyy-MM-dd HH:mm:ss" w przeciwnym wypadku wartość DateTime będzie null. Jeżeli chcemy w parametrze WebAPI dostawać czas lepiej użyć do tego string a nie DateTime. W przypadku gdy użytkownik poda złą date lub jej nie poda DateTime będzie null, string zaś zachowa informację o złym formacie.
Przykład:
użytkownik podaje datę 20231211
DateTime = null string = "20231211"
Przykładowy kod:
public void Get(int id, string time)
{
if(string.IsNullOrEmpty(time))
{
// Przypadek gdy nie został podany time
}
if(!DateTime.TryParse(time, out DateTime dateTime))
{
// Przypadek gdy podany format daty był błędny
// nadal mamy wartośc podaną w zmiennej time jako string i można z nią coś dalej robić
}
else
{
// Przypadek gdy format daty jest poprawny i mamy do niej dostęp ze zmiennej dateTime
}
}
Uwierzytelnianie¶
Dla każdej metody webserwisu Neos sprawdza, czy uprawnienia do niej zostały nadane jakiejś roli publicznej. Jeżeli tak, to jest ona publiczna i żadne uwierzytelnianie nie jest wymagane. W przeciwnym przypadku metoda wymagać będzie wcześniejszego uwierzytelnienia klienta.
Zaimplementowany został mechanizm autentykacji basic.
Jeżeli spróbujemy wywołać jakąś metodę niepubliczną bez podania danych uwierzytelniających, otrzymamy odpowiedź o statusie HTTP 401 - Unauthorized. W ten sposób serwis prosi klienta aby się przedstawił.
[Request:]
GET /api/produkty HTTP/1.1
Host: localhost:10421
[Response:]
HTTP/1.1 401 Unauthorized
Content-Length: 0
WWW-Authenticate: Basic realm="localhost"
Aby uzyskać dane autoryzujące, klient powinien wywołać metodę login w trybie GET, podając parametry user i password.
[Request:]
GET /api/login?user=****&password=**** HTTP/1.1
Host: localhost:10421
[Response:]
HTTP/1.1 200 OK
Content-Length: 142
Content-Type: application/json; charset=utf-8
{
"User": "sente",
"Password": "7b9ce1fc82594a889dcb6b2ee70900cc",
"BasicBase64": "c2VudGU6N2I5Y2UxZmM4MjU5NGE4ODlkY2I2YjJlZTcwOTAwY2M=",
"Code": 200
}
Parametry user i password są przekazywane otwartym tekstem, nie jest używane haszowanie, dlatego aby zapewnić bezpieczną komunikację należy włączyć szyfrowanie warstwy transportowej, czyli SSL/TLS i łączyć się na adres zaczynający się od https:.
Neos do autoryzacji użytkownika użyje metody określonej w parametrach roli tego webserwisu, domyślnie wywoła procedurę SYS_GET_USERINFO na domyślnej bazie danych. Hasło klient przesyła otwartym tekstem w parametrze password, a dopiero w Neosie zostaje ono zahaszowane w standardowy sposób, kompatybilny z tym używanym w kliencie VCL. Procedura logująca powinna mieć tę samą sygnaturę co procedura logująca dla profili www.
Tip
Procedura SYS_GET_USERINFO zwraca UUID roli na której będzie działał zalogowany w naszym webserwisie klient. Również ta rola musi mieć nadane uprawnienia dostępowe do metod webserwisu. Dzięki temu rozwiązaniu wielu różnych klientów może mieć uprawnienia do różnych części tego samego webserwisu.
Jeżeli wszystko pójdzie poprawnie Neos założy klientowi sesję i odeśle pokazaną wyżej strukturę informacyjną, w której powtórzy nazwę użytkownika i poda nam tymczasowe hasło dostępu do danej sesji.
Wróćmy teraz do próby wywołania naszej metody. Aby ją wywołać musimy, zgodnie ze standardem dodać nagłówek autoryzacyjny, w którym w kodowaniu base64 podamy otrzymane wcześniej nazwę użytkownika i tymczasowe hasło. Dla wygody otrzymujemy także już zakodowany tą metodą ciąg który wystarczy wstawić do zapytania.
[Request:]
GET /api/produkty HTTP/1.1
Authorization:Basic c2VudGU6N2I5Y2UxZmM4MjU5NGE4ODlkY2I2YjJlZTcwOTAwY2M=
Host: localhost:10421
[Response:]
HTTP/1.1 200 OK
EnableAutosessions=yes - Pozwala wykonywać requesty bez potrzeby uwierzytelniania klienta, przykład:
CustomRoutes=v1/{controller}/{action}/{id} - Dodatkowa trasa zapytań webapi, przykład:
Sliding expiration¶
Nie możemy spodziewać się, że użytkownik zawsze poprawnie się wyloguje z systemu. W takim przypadku, dla oszczędzania zasobów naszego serwera, po upływie pewnego czasu bezczynności (od ostatniego poprawnego wywołania niepublicznej metody webserwisu). Czas po upływie którego klient zostanie wylogowany jest określany przez parametr InactivityPeriod w pliku smd. Mechanizm który odpowiada za to zachowanie jest współdzielony z sesjami użytkowników logujących się do klienta www do Neosa.
Po wylogowaniu, przy następnej próbie dostępu, zostanie zwrócony normalny komunikat 401 - Unauthorized, w reakcji na który klient powinien ponownie przeprowadzić proces logowania.
Kontekst wykonania metody webserwisu¶
W każdym webserwisie tworzony jest domyślny kontekst wykonania[1] przypisany do roli webserwisu. To znaczy, że w momencie sprawdzania takich parametrów jak UserName, albo sprawdzania czy dana metoda ma uprawnienia do zapisu jakiegoś obiektu biznesowego, granty będą pochodziły z roli, którą tworzyliśmy przy konfiguracji webserwisu. Jest to wspólny kontekst dla wszystkich wywołań metod publicznych, więc teoretycznie moglibyśmy współdzielić jakieś parametry przez np. SessionInfo. Nie należy jednak tego robić, ponieważ metody publiczne powinny być z założenia bezstanowe.
W przypadku metod wymagających uwierzytelnienia sytuacja jest inna. Podając użytkownika i hasło, zostaje wywołana na domyślnej bazie danych procedura logowania, która zwraca uuid roli zalogowanego użytkownia. Zostaje utworzona nowa sesja i wszystkie wykonania metod w ramach tej sesji będą wykonywane w kontekście roli danego użytkownika. Pozwala to dokładniej sterować uprawnieniami. Przykładowo, jeżeli prawo logowania do webserwisu zamówień ma Kowalski i Abacki, to jeden może mieć uprawnienia do zmiany parametrów zamówienia, a drugi nie, bo wynika to z grantów rol Kowalskiego i Abackiego.
Warstwy kontroli uprawnień w webserwisach¶
W webserwisach zaimplementowanych zostało kilka warstw kontroli uprawnień do danych.
- Na etapie kompilacji projektu sprawdzamy, które metody wejdą w skład danego webserwisu, zatem fizycznie nie będzie możliwości wywołania innych. Metody czytające dane (
GET) wymagają uprawnienia typu view (efektywny grantEV), metody modyfikujące dane (PUT/POST/DELETE) wymagane są granty do modyfikacji danych (efektywny grantEU) - Również na etapie kompilacji rozstrzygamy, czy dostęp do danej metody wymagać będzie uwierzytelniania czy będzie to metoda publiczna
- W momencie wywołania metody sprawdzamy, czy rola w bieżącym kontekście wykonania metody posiada uprawnienia do danej metody webserwisu - to znaczy, że możemy np. konkretnym użytkownikom zabraniać dostępu bądź nadawać dostęp do wybranych metod, bez konieczności restartu usługi. Tutaj również w zależności od charakteru metody sprawdzamy granty
EVlubEG. - Jeżeli korzystamy ze standardowych metod dostępu do danych obiektów biznesowych (czyli metod
NewRecord,PostRecorditp), również sprawdzane są odpowiednie uprawnienia dla roli w ramach bieżącego kontekstu. Ta warstwa uprawnień nie działa gdy korzystamy z warstwyDB(przezDB.OpenTable)
SSL/TLS¶
Standard autentykacji http basic nie jest odporny ani na sniffing, ani na man in the middle, ani na CSRF, głównie dlatego, że parametry autoryzacyjne są przesyłane otwartym tekstem. W tej sytuacji jedyną możliwą metodą zapewnienia bezpiecznej komunikacji jest włączenie szyfrowanej wersji protokołu transportowego. W tym celu, musimy zaznaczyć w parametrach roli usługi znacznik SSL/TLS, dzięki czemu po odświeżeniu usługa zostanie skonfigurowana tak, aby reagować na wywołania https. Oprócz tego należy wykonać pewną ilość czynności stricte administracyjnych, aby system operacyjny mógł realizować żądania dostępu w ten sposób. O skonfigurowanie tego poproś administratora sieci w której ma działać usługa. Jeżeli masz problemy, to jest pobieżny opis tego, co należy zrobić[2].
-
Wygenerować certyfikat ssl[3]. Podpisać go certyfikatem instytucji weryfikujących (w p.p. przeglądarki będą pokazywać go jako niebezpieczny) i zaimportować na listę certyfikatów danej maszyny[4].
-
W oknie konsoli, posiadającej uprawnienia administratora, wpisz
netsh http add urlacl url=<url\> user=<user\>
gdzie:
urlto adres na którym nasłuchiwać będzie zabezpieczona usługa, na przykładhttps://+:4443/, adres ten powinien być zgodny z tym podanym w parametrach roli webserwisu-
userto nazwa użytkownika, na którym chodzi Neos -
Następnie wpisz:
netsh http add sslcert ipport=<ipport> certhash=<thumbprint> appid={<app-guid>}
gdzie:
ipportto adres ip oraz numer portu na którym obowiązywać będzie certyfikat. Specjalny adres0.0.0.0mapuje się na dowolny port lokalny na danej maszyniethumbprintjest skrótem instalowanego certyfikatuapp-guidto dowolny guid używany do identyfikacji, która aplikacja będzie używać tego wpisu. Do testów wystarczy00000000-0000-0000-0000-000000000000
Jakie metody wejdą w skład danego webserwisu?¶
Do danego webserwisu wejdą tylko metody zdefiniowane w domyślnym projekcie (DefaultProject), lub metody z projektów które są na liście jego referencji (przechodnie domknięcie). Czyli, jeżeli mamy projekty SYSTEM po którym dziedziczy projekt SENTE, a z niego dziedziczą projekty P1 i P2, to jeżeli ustawimy domyślny projekt na P1, nigdy nie będziemy mieli w webserwisie metod z obiektów w projekcie P2. Jeżeli ustawimy domyślny projekt na SENTE, nie wejdą do webserwisów metody w obiektach z projektów P1 jak i P2.
Metody webserwisu są normalnymi, choć statycznymi, metodami obiektów biznesowych i są dziedziczone. Dziedziczenie może być między projektami. Jeżeli mamy obiekt A w projekcie SYSTEM i obiekt B w projekcie SENTE, ustawiając odpowiednio domyślny projekt będziemy mogli wywoływać metody z obiektu A (z adresu /api/a) oraz z B (z adresu /api/b). W tym celu obecnie w Neosie, można z powodzeniem zmieniać implementacje dziedziczonych metod statycznych.
Jeżeli chcemy w webserwisie mieć tylko metody z obiektu B, powinniśmy B podziedziczyć z A w trybie przykrycia. Wtedy wygenerowane będą tylko metody z obiektu B.
Czy WebAPI działa wydajnie?¶
Testy¶
WebApi jest dosyć wydajnym mechanizmem udostępniania danych. Wydajność testowano zwracając rekordy struktury STANYILInfo z projektu SENTE. Jeden rekord ma 7 pól, w tym 3 tekstowe i 4 liczbowe. Dane pobierano przy pomocy zadanego bezpośrednio zapytania SQL przy pomocy polecenia
NDataView dv = DB.OpenTable("","","select * from STANYILPROC", NFillMode.Full, "", "*");
Procedura STANYILPROC zwielokrotniała ilość rekordów tabeli STANYIL.
Wyniki otrzymywano w formacie JSON. Rezultat testów zebrano w poniższej
tabeli.
| Ilość danych wynikowych | Ilość rekordów | Czas sumaryczny [ms] | Czas generowania danych [ms] | Narzut WebApi [ms] |
|---|---|---|---|---|
| 133 KB | 1 000 | 90 - 100 | 70 - 83 | 18.5 |
| 1.3 MB | 10 000 | 750 - 920 | 560 - 730 | 190 |
| 13 MB | 100 000 | 7180 - 8330 | 5150 - 6210 | 2075 |
Przedostatnia kolumna zawiera czas w jakim kod C# wsparty zapytaniem SQL rzeczywiście generował dane. Ten czas jest zależny w 100% od programisty i możliwości wydajnego pobrania danych z bazy, a więc będzie się zmieniał w zależności od złożoności zapytania SQL a także wydajności i obciążenia serwera BD.
W ostatniej kolumnie wyliczono narzut jaki daje sam mechanizm WebApi, a więc protokół HTTP, konwersja formatów danych, itp.
Testy prowadzono na komputerze lokalnym zawierającym jednocześnie serwer jak i przeglądarkę, toteż przyjęto zerowy narzut na transmisję danych przez sieć. W przypadku udostępniania WebApi przez intranet lub internet należy skalkulować narzut na transmisję. Jest oczywiste, że w sieci lokalnej 100Mbit dane o wielkości 13 MB mogą być przesłane najszybciej w czasie około 1,3 sek.
Czynniki wpływające na wydajność¶
Jeśli format application/json zastąpimy formatem text/xml rozmiar przesyłanych danych rośnie około 1.5 raza. Nie zwiększa się czas generowania danych ani narzut na WebApi. Zwiększy się natomiast narzut na transmisję przez sieć.
Jeśli zapytanie SQL zastąpimy otwarciem obiektu biznesowego i czytaniem z niego danych, to proces generowania danych może się wydłużyć dwukrotnie.
var obj = new SENTE.STANYIL;
Jeśli oprócz powyższego zastąpimy przypisywanie ręczne pól modelu danych do pól zwracanej struktury przez gotową metodę AssignTo(...) to czas generowania danych może się wydłużyć około 5 razy. Metoda AssignTo wykorzystuje bowiem refleksje do mapowania pól wg ich nazw w przekazanej strukturze. Zaleta tej metody polega na tym, że dodanie nowego pola do obiektu biznesowego lub do struktury nie wymaga modyfikacji kodu metod WebApi.
W przypadku transmisji dużych ilości danych przez słabe łącza internetowe, klient korzystający z Webapi może w zapytaniu REST dodać nagłówek powodujący włączenie kompresji danych.
Accept-Encoding: gzip, deflate
Powoduje to około 10-ciokrotne zmniejszenie rozmiaru przesyłanych danych, a więc 10-ciokrotnie zmniejsza narzut na czas transmisji przez sieć. Narzut algorytmu kompresji jest niewielki.
Logowanie komunikatów¶
We właściwościach roli znajduje się zakładka, w której możemy włączyć logowanie komunikacji webserwisu. Domyślnie logi zapisuja się w plikach logów systemowych w folderze logs. Logi webserwisu podobnie jak logi samego Neosa są zapisywane per dzień i pozostają przez tydzień.
Aby wyodrębnić logi webserwisów do oddzielnego pliku można skorzystać z przykładowej konfiguracji logger:
{
"Neos": {
"LogMode": "WARNING|ERROR|WEBAPI"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{
"Name": "Logger",
"Args": {
"configureLogger": {
"Filter": [
{
"Name": "ByExcluding",
"Args": {
"expression": "(StartsWith(SourceContext, 'Webapi'))"
}
}
],
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/log-.log",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} <pid {ProcessId,5} tid {ThreadId,4} mem {MemoryUsage}> {SourceContext,35} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
"formatter": "Serilog.Formatting.Json.JsonFormatter",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 1073741824,
"retainedFileCountLimit": 7
}
}
]
}
}
},
{
"Name": "Logger",
"Args": {
"configureLogger": {
"Filter": [
{
"Name": "ByIncludingOnly",
"Args": {
"expression": "(StartsWith(SourceContext, 'Webapi'))"
}
}
],
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/Webapi-.log",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} <pid {ProcessId,5} tid {ThreadId,4} mem {MemoryUsage}> {SourceContext,35} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
"formatter": "Serilog.Formatting.Json.JsonFormatter",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"fileSizeLimitBytes": 1073741824,
"retainedFileCountLimit": 7
}
}
]
}
}
}
],
"Enrich": [
"FromLogContext",
"WithMachineName"
],
"Properties": {
"Application": "MultipleLogFilesSample"
}
}
}
W wersjiach starszych niż 5.4 logi zapisywane są w plikach o nazwie Webserwis_<nazwa_webserwisu><data>.log.
Domyślnie logowane są tylko nagłowki poprzedzone nazwą webserwisu:
2015-10-02 12:36:52:910 (5520) #[25] Klienci GET 1.1 http://localhost:9014/api/klienci/194304
2015-10-02 12:36:53:478 (5520) #[25] Klienci SUCCESS 200 OK OK
Jeżeli podczas przetwarzania żądania nastąpił błąd i nie udało się poprawnie wygenerować odpowiedzi zamiast SUCCESS zobaczymy FAILURE.
Możemy włączyć jeszcze logowanie nagłówków http tych żądań, oraz logowanie samej zawartości żądań/odpowiedzi. W tym ostatnim przypadku, logowana jest zawartość żądania, po ewentualnej dekompresji, jeżeli była skompresowana przez nadawcę. Odpowiedź natomiast jest logowana przed ewentualną kompresją zwrotną. W przypadku braku zawartości wyświetlany jest wiersz <[ empty content ]>. Jeżeli odpowiedzą jest strumień binarny (czyli np. Content-type: application/png) to możemy zobaczyć krzaki.
Możemy również wykluczyć logowanie pewnych żądań (np. GET) oraz możemy również z logowania wykluczyć odpowiedzi o podanych statusach (np. nie logujemy odpowiedzi o kodzie 200 OK, tylko te z błędami).
W momencie, gdy klikniemy zapisz we właściwościach roli, nowa konfiguracja logowania jest już aktywna.
Zmiana struktury dziedziczonych obiektów w metodach webapi¶
Częstym przypadkiem w trakcie wdrożeń jest zmodyfikowanie istniejącego webapi, w tym przypadku może pojawić się potrzeba modyfikacji zwracanej struktury. Istnieją dwa sposoby na dokonanie takiej modyfikacji.
Przygotowanie¶
Przed rozpoczęciem modyfikacji należy upewnić się, że mamy utworzony projekt oraz obiekt, który dziedziczy po obiekcie z metodami webapi, który będzie zawierał zmienioną strukturę danych. Należy pamiętać, aby zmienić nazwę obiektu dziedziczącego.
W poniższych przykładach zakładam, że mamy projekt PARENTPROJECT z obiektem A, który posiada metodę webapi Get() oraz projekt CHILDPROJECT z obiektem B, który dziedziczy po A
Sposób 1 Dziedziczymy metodę webapi, podmieniając w ciele metody zwracany typ np:¶
Metoda Get w obiekcie bazowym A:
public PARENTPROJECT.MODEL.A Get(int id)
{
this.FilterAndSort("REF="+id);
if(this.FirstRecord()) {
var item = new PARENTPROJECT.MODEL.A();
this.AssignTo(item);
return item;
}
return null;
}
Metoda Get w obiekcie, który dziedziczy B:
public PARENTPROJECT.MODEL.A Get(int id)
{
var baseObj = base.Get(id);
var childObj = new CHILDPROJECT.MODEL.B();
childObj.NAZWA = baseObj.NAZWA; //przypisujemy pola z obiektu bazowego
childObj.NOWEPOLE = 3; // przypisujemy nowe pola
return childObj;
}
Sposób 2 Tworzymy nową metodę webapi poprzez słowo kluczowe "new" i zmieniamy jej sygnature tak, aby zwracała nowe Entity np:¶
Metoda Get w obiekcie bazowym A:
public PARENTPROJECT.AEntity Get(int id)
{
this.FilterAndSort("REF="+id);
if(this.FirstRecord()) {
var item = new PARENTPROJECT.AEntity();
this.AssignTo(item);
return item;
}
return null;
}
public new CHILDPROJECT.BEntity Get(int id)
{
var baseObj = base.Get(id);
var childObj = new CHILDPROJECT.BEntity();
childObj.NAZWA = baseObj.NAZWA; //przypisujemy pola z obiektu bazowego
childObj.NOWEPOLE = 3; // przypisujemy nowe pola
return childObj;
}
Błędy¶
Webserwis ... zawiera już kontroler o nazwie ...¶
W webserwisie nie mogą istnieć dwa kontrolery o tej samej nazwie. Jeżeli obiekt A definiuje metody webserwisu i zrobisz obiekt B, który dziedziczy po A i nadasz roli webserwisu uprawnienia do obydwu to zobaczysz poniższy komunikat i w webserwisie znajdzie się tylko jeden z tych obiektów. Pierwszeństwo będzie miał ten, który znajduje się w domyślnym projekcie. Jeżeli chcesz aby widoczny był kontroler dla obiektu A, zabierz uprawnienia do B i nadaj je do A.
WebApi w wersji v2¶
Od wersji 5.0 serwer NEOS obsługuje dodatkową trasę zapytań do WebApi. Umożliwia to zadawanie zapytań z konkretną nazwą metody jaką serwer powinien wykonać. Poniżej przykład:
Do wersji 5.0
http://localhost:9999/api/TOWARY
Od wersji 5.0
http://localhost:9999/api/TOWARY
http://localhost:9999/api/v2/TOWARY/GetAll
Wersja 5.0 jest kompatybilna wstecz. Zatem poprawnie obsługuje aktualnie utworzone i wystawione WebService'y oraz umożliwia tworzenie zapytań z wskazaniem nazwy metody.
Konfiguracja WebAPI¶
Od wersji 6.2 wymagane jest konfigurowanie WebAPI z pliku smd, aby to zrobić należy dodać sekcję
[Webservices]
Przykład:
[Webservices]
ExampleApi.Example:Port=7777
Dokumentacja WebAPI - Swagger¶
Od wersji Neosa 6.3.1 udostępniono wystawianie dokumentacji WebAPI za pomocą swaggera. Aby dostać się do dokumentacji konkretnego API, należy przejść na adres, na którym mamy wystawiony web serwis z sufixem /swagger/ui/index np. localhost:9010/swagger/ui/index. Otworzy nam się strona, na której możemy przejrzeć wystawione przez nas endpointy dla konkretnej wersji WebAPI - domyślnie otwiera się strona z wersją *.
Aby przejść na konkretną wersję naszego WebAPI, należy w polu tekstowym na górze strony wpisać odpowiednią ścieżkę do wygenerowanego przez swaggera dokumentu. np. Jeżeli chcemy przejść na wersję v2 należy w tym polu wpisać http://localhost:9010/swagger/docs/v2 i wcisnąć enter.
!!! Important Ważne!
Sufix http://[host]:[port]/swagger/docs/ jest zawsze wymagany, gdyż wskazuje on na plik .json wygenerowany przez Swaggera.
Note
Jeżeli chcemy wyświetlić wszystkie endpointy dostępne w danym web serwisie naeży w polu wyszukiwania wpisać sufix wraz z * na końcu.
http://[host]:[port]/swagger/docs/*
Dostępne wersje WebAPI wystawiane przez Neosa:
- default - domyślna wersja web api z krótką ścieżka: api/{controller}/{id}
- v1 - wersja web api ze ścieżka: api/v2/{controller}/{id}
- v2 - wersja web api v2 ze ścieżka: api/v2/{controller}/{action}/{id}
Obsługiwane są również customowe ścieżki do WebAPI konfigurowane w sekcji [WebAPI] i parametrem CustomRoutes= w pliku .smd. Dostępne są one pod skróconą nazwą cr[index] np. http://localhost:9010/swagger/docs/cr0 - Liczone od 0 względem kolejności, w jakiej zostały skonfigurowane w .smd.
Dodawanie opisów metod i parametrów do dokumentacji Swaggera¶
Aby opisy metod WebAPI oraz ich parametrów były widoczne w dokumentacji Swaggera, należy wykorzystywać komentarze XML. Komentarze te powinny być umieszczone nad definicjami metod oraz ich atrybutami w kodzie widocznym w Visual Studio Code lub jako treść komentarza dewelopera metody w Neos Expercie (w szczegółach pod metodą). Dzięki temu Swagger może automatycznie pobierać i wyświetlać te informacje w interfejsie użytkownika.
Przykład zastosowania komentarza XML w Visual Studio Code:
/// <summary>
/// Zwraca listę użytkowników.
/// </summary>
/// <param name="active">Jeśli ustawione na true, zwraca tylko aktywnych użytkowników.</param>
/// <returns>Lista użytkowników w systemie.</returns>
[CustomData("WebApi=Y")]
[UUID("ccb6aa7fc1f7469fb8fa7e9ed6969a9d")]
[HttpGet]
public IEnumerable<User> GetUsers(bool active)
{
// implementacja
}
Przykład zastosowania komentarza XML w Neos Expercie:
Uwaga!
Dodawanie komentarza nad metodą w Neos Expercie nie jest wspierane — w rzeczywistości nad metodą mogą znajdować się atrybuty (np. CustomData, UUID), które nie są widoczne w edytorze. Z tego powodu komentarz umieszczony nad metodą może nie zostać prawidłowo wczytany. Zalecane jest więc umieszczanie opisu jako komentarz dewelopera (w dolnym panelu), albo bezpośrednio w kodzie źródłowym w Visual Studio Code.
[1]: formalnie WorkingContext z nowym ClientInstance
[2]: opis tutaj oparty jest na tym artykule
[4]: Ogólnie temat wykracza poza ramy tego poradnika i powinien być ogarniany przez ludzi zajmujących się zawodowo utrzymywaniem infrastruktury sieciowej, ale można sobie np. zajrzeć tutaj