NET 5 WebAPI + OData - własny model zwracanego wyniku oraz obsługa wyjątków

0

Witam.
Lecimy dalej z problemami. Ktoś na forum polecił mi OData do mojego przypadku i jest to bardzo fajne narzędzie. Problem zaczyna się gdy chce zrobić własny response model. Dane są bardzo podstawowe i w pewnych przypadkach to co jest domyślnie wystarczy, ale nie u mnie. Drugą sprawą jest obsługa wyjątków.

try
{
    return Ok(await _dataReader.GetCustomers("database_name"));
}
catch (Exception ex)
{
    return StatusCode(500, ex.Message);
}

Nie mogę sobie tego tak zrobić, ponieważ OData oczekuje, że zostanie zwrócona lista obiektów, którą parametrami pofitruje, a endpoint w tym miejscu zwraca wyjątek i OData już sobie z tym nie radzi:

{
    "error": {
        "code": "",
        "message": "The query specified in the URI is not valid. The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.",
        "details": [],
        "innererror": {
            "message": "The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.",
            "type": "Microsoft.OData.ODataException",
            "stacktrace": "   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ValidateSelectExpandOnly(ODataQueryOptions queryOptions) in Microsoft.AspNetCore.OData.dll:token 0x60002ff+0x3c\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.ExecuteQuery(Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request) in Microsoft.AspNetCore.OData.dll:token 0x60002fa+0x2f\r\n   at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuted(ActionExecutedContext actionExecutedContext, Object responseValue, IQueryable singleResultCollection, ControllerActionDescriptor actionDescriptor, HttpRequest request) in Microsoft.AspNetCore.OData.dll:token 0x60002f5+0x97"
        }
    }
}

Dwa pytania:

  1. Jak ogarnąć custom response używając OData?
  2. Jak ogarnąć wyjątki?
0

Musiałbym wiedziec całą akcję kontrolera jak masz napisaną ale wydaje mi się że moze to Ci pomóc:

try
{
    return Ok(await _dataReader.GetCustomers("database_name"));
}
catch (Exception ex)
{
    return NotFound(ex.Message);
}

źródło: https://docs.microsoft.com/pl-pl/aspnet/core/web-api/action-return-types?view=aspnetcore-5.0

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return Ok(product);
}
0

A co jeśli chciałbym to mieć w postaci takiej jak napisałem na SO NET 5 WebAPI + OData - custom result model?

#EDIT
Udało mi się to ogarnąć. OData najlepiej pracuje z EF, ponieważ "zmusza" do korzystania z IQueryable zamiast IEnumerable. W moim przypadku nie mogę używać EF, ponieważ to nie moja baza, więc korzystam z Dappera. Wszyscy wiemy, że Dapper żyje innymi prawami i IQueryable nie daje żadnych korzyści ale dzięki temu jest możliwość zwrócić custom response korzystając z OData:

MODEL

    public class CustomersResponse
    {
        public string ReponseMessage { get; set; }
        public int ResponseCode { get; set; }
        public int ItemsLength { get; set; }
        public IQueryable Items { get; set; }
    }

KONTROLER

        [HttpGet]
        public async Task<IActionResult> Get(ODataQueryOptions<Customer> options)
        {
            var response = new CustomersResponse();

            try
            {
                var customers = await _dataReader.GetCustomers("database_name");
                var customersResult = options.ApplyTo(customers.AsQueryable()).Cast<Customer>();
                if(customersResult.Count() == 0)
                {
                    response.ItemsLength = customers.Count();
                    response.ReponseMessage = "NotFound";
                    response.ResponseCode = 404;
                    response.Items = customersResult;
                    return NotFound(response);
                }
                response.ItemsLength = customers.Count();
                response.ReponseMessage = "OK";
                response.ResponseCode = 200;
                response.Items = customersResult;
                return Ok(response);
            }
            catch (Exception ex)
            {
                response.ResponseCode = 500;
                response.Items = null;
                response.ReponseMessage = ex.Message;
                response.ItemsLength = 0;
                return StatusCode(500, response);
            }
        }

Usuwamy z endpointa atrybut [EnableQuery], dodajemy parametr ODataQueryOptions<Customer> options i korzystamy z options.ApplyTo(), który przygotuje odpowiednio dane. Przypominam, że średnio to jest wydajne, ponieważ w pierwszej linii pobieramy wszystkich kontrahentów, a później dopiero filtrujemy za pomocą OData.

DZIAŁA

.Filter()
.OrderBy()
.SetMaxTop(50)

NIE DZIAŁA

.Count() // nie potrzebne nam już jest
.Expand()
.Select()

W przypadku Select() jest najprawdopodobniej problem ze zwracanymi danymi, ponieważ OData próbuje to jakoś konwertować na swój typ.

{
    "reponseMessage": "Unable to cast object of type 'Microsoft.AspNetCore.OData.Query.Wrapper.SelectSome`1[OptimoLogic.Models.Customer]' to type 'OptimoLogic.Models.Customer'.",
    "responseCode": 500,
    "itemsLength": 0,
    "items": null
}
0

Możesz używać EF w już istniejącej bazie danych. Definiujesz wtedy Entities ręcznie i potem robisz mapping, określasz kontekst itp.

https://www.entityframeworktutorial.net/efcore/create-model-for-existing-database-in-ef-core.aspx

1

Nie ma szans. To jest baza Comarch Optima, tych tabel jest 472 :D jestem w czarnej d... Albo się zadowolę tym co OData domyślnie zwraca albo muszę wrócić do własnej implementacji filtrów. Bo korzystać z EF tylko dlatego, że ogarnia IQueryable chyba mija się z celem 🤔

0

Nikt Ci nie każde przecież mapować wszystkich tabel od razu tylko pod konkretny kontekst. A co do tabel to wiem jakie są bolączki bo pracuję na XL i Optimie, aczkolwiek jak widzisz nie jesteś skazany na dappera jedynie.

0

No w sumie tak. Tylko teraz pytanie - czy to jest wartę zachodu? 🤔 Potrzebuje EF tylko do pobierania danych. Usuwanie, edycja i zapis będzie ogarniana przez obiekty COM.

0

Zrobiłem parę przykładów i w tym przypadku EF wygrywa z Dapperem. Pomyślałem, że może się do czegoś przyda, a że różnica jest kolosalna to przy okazji może kogoś zaciekawi.

4 REQUESTY

  1. Dapper z [EnableQuery]
  2. Dapper z ApplyTo() i custom response
  3. EF z [EnableQuery]
  4. EF z ApplyTo() i custom response

Ten sam request url, te same filtry

https://localhost:5001/api/documents?$count=true&$orderBy=date desc&$filter=type eq 302 and bufor eq 0&$top=10

NR 1

        [HttpGet]
        [EnableQuery]
        public async Task<IActionResult> Get()
        {
            using var db = _sqlConnection.CreateCompanyConnection("database_name");
            {
                return Ok((await db.QueryAsync<Document>(_documentsQueries.GetDocumentsQuery)).AsQueryable());
            }
        }

dapper_enable_query.png

NR 2

        [HttpGet]
        public async Task<IActionResult> Get(ODataQueryOptions<Document> options)
        {
            using var db = _sqlConnection.CreateCompanyConnection("CDN_TRON2018");
            {
                DocumentsResponse response = new DocumentsResponse();

                var documents = (await db.QueryAsync<Document>(_documentsQueries.GetDocumentsQuery)).AsQueryable();
                var results = options.ApplyTo(documents);
                response.Items = results;
                response.ItemsLength = documents.Count();
                response.ReponseMessage = "OK";
                response.ResponseCode = 200;
                return Ok(response);
            }
        }

dapper_apply_to_custom_response.png

NR 3

        [HttpGet]
        [EnableQuery]
        public IActionResult Get()
        {
            return Ok(_dbContext.TraNags.AsNoTracking());
        }

ef_enable_query.png

NR 4

        [HttpGet]
        public IActionResult Get(ODataQueryOptions<TraNag> options)
        {
            DocumentsResponse response = new DocumentsResponse();

            var documents = _dbContext.TraNags.AsNoTracking();
            var results = options.ApplyTo(documents);
            response.Items = results;
            response.ItemsLength = documents.Count();
            response.ReponseMessage = "OK";
            response.ResponseCode = 200;
            return Ok(response);
        }

ef_apply_to_custom_response.png

PS.
Oczywiście takie testowanie na localhost daje pogląd na to czego można się spodziewać na produkcyjnym.
Punkt 4 w linijce _dbContext.TraNags.AsNoTracking(); pobiera wszystkie 13834 dokumenty, zamiast około 5800, ponieważ filtr na typ dokumentu nie jest nałożony (type eq 302), a i tak jest sporo szybciej niż z Dapperem.

Wniosek?
Czy to jest kwestia IQueryable, którego Dapper nie obsługuje?
Skąd tak duża różnica?

1

W przypadku testów z EF połączenie z bazą masz już otwarte, a tych z Dapperem dopiero otwierasz, prawda?

0

UseSqlServer() wywołuje się w momencie kiedy odpytuje API, nie na start aplikacji, więc mogę założyć, że jest otwierane z każdym requestem?

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer("connection_string");
            }
        }
0

No ja widzę, że w kodzie z Dapperem masz: using var db = _sqlConnection.CreateCompanyConnection("database_name");, czego nie masz w kodzie z EF.
Mierzysz czas całego requestu, prawdopodobnie z łączeniem do bazy, a nie samego wykonywania zapytania, i prawdopodobnie stąd te różnice w czasie.

0

Wydaje mi się, że połączenie nie wydłuża tego czasu. Czy to jest miarodajne?

Pierwszy request zaraz po uruchomieniu API

request_timer_first_request.png

Drugi request

request_timer_second_request.png

Każdy kolejny request jest w granicy tego drugiego, czasem szybciej, czasem wolniej, ale nigdy nie wraca do tego co przy pierwszym.

1 użytkowników online, w tym zalogowanych: 0, gości: 1