.NET Core API - uniwersalne zapytania na podstawie właściwości obiektu

0

Witam.
Jak to jest "poprawnie" w API, że można przesyłać kilka parametrów i jest to zrobione na tyle uniwersalnie, że API automatycznie wie po jakich parametrach ma szukać danych.

Dla przykładu:

class Produkt
{
   int ID;
   string Kod;
   string Nazwa;
   decimal Cena;
   decimal Ilosc;
   string Grupa;
   string EAN;
}

Zapytania do API:
/api/produkty?Kod=jakiśKod - szuka po kodzie
/api/produkty - bierze wszystkie
/api/produktu?Grupa=grupa&Kod=kod - szuka po grupie i po kodzie jednocześnie

  1. Co jeśli chciałbym obsłużyć produkty, których ilość jest większa od zera?
  2. Czy to wszystko sprowadza się do obsłużenia jakie parametry przychodzą z FromQuery ręcznie?
  3. Jak to później przełożyć na SQL? Zrobić querybuilder czy ściągać wszystko do obiektu i wtedy jakimś Linq bądź Lambda filtrować na podstawie parametrów?
0

Może pokombinuj z OData, bo zaimplementowanie takich filtrów od zera będzie nietrywialne
https://devblogs.microsoft.com/odata/asp-net-core-odata-now-available/

2

Nie wiem co masz na myśli z "uniwersalne", ale jeśli dynamiczne, że raz szukasz po 5 parametrach, raz po 20, raz po 2, itd...

to Expression + dynamic query building.

IQueryable<Product> query = _context.Products;

if (!string.IsNullOrEmpty(search.KodProduktu)
{
    query = query.Where(x => x.KodProduktu.Contains(search.KodProduktu));
}

if (search.CenaUpperBound.HasValue)
{
    query = query.Where(x => x.Cena <= search.CenaUpperBound.Value);
}

query.ToList();

I będzie to poprawnie przetłumaczone na SQL.

Jeżeli chcesz robić bardziej zaawansowane predykaty, to chyba musisz wejść głębiej w Expression.

Ewentualnie, ale ostrożnie z tym

Easy way to create a C# lambda expression from a string (with Roslyn)

0

Te całe OData zrobi chyba jeszcze więcej kłopotów. Dziwne rozwiązanie.

@WeiXiao
Twoje już prędzej. I właśnie spodziewałem się, że to będzie miliony ifów, aby sprawdzać czy dany parametr został użyty, tego się obawiałem. Liczyłem, że Core gdzieś tam to ułatwi.

Co najważniejsze, bo wasze przykłady są z EF, a co jeśli chciałbym użyć Dappera?

1

I właśnie spodziewałem się, że to będzie miliony ifów, aby sprawdzać czy dany parametr został użyty, tego się obawiałem

Tyle, że tam wcale nie musi być miliona ifów bo możesz podejść do tego w wersji "leniwej" (w kontekście późniejszej obsługi) czyli opracować sobie generyczny mechanizm który obsłuży ci filtry.
Możesz podejść do tego np tak, żeby stworzyć sobie model filtra - np taki który będzie obsługiwać stringi, liczby itd i generyczny mechanizm który mając informację na temat tego jaki to filtr, jaka operacja ma zostać wykonana i jaka jest zawartość tego filtra wygeneruje odpowiednie wyrażenia. Mając w jednym obiekcie kilka filtrów łączysz wszystkie wyrażenia które powstały w jedno i przekazujesz je do EF.
Może się jednak okazać że ten mechanizm nie będzie się sprawdzać w każdym przypadku - np dlatego że reprezentacja modelu używanego na widoku zupełnie inna niż bazodanowa i w tym przypadku musisz przemyśleć sobie kwestię umożliwienia przeciążenia metody filtrującej.

Jest to "leniwe" ponieważ późniejsza obsługa jest zazwyczaj banalna. Wymaga jednak więcej pracy na samym początku.

2

Ja zrobiłem sobie do tego kiedyś taki mechanizm:

    public enum OrderDirection
    {
        NoOrder,
        Ascending,
        Descending
    }

    public class OrderInfo
    {
        /// <summary>
        /// Direction of order. NoOrder means that no ordering is done.
        /// </summary>
        public OrderDirection Direction { get; set; }

        /// <summary>
        /// Ordering field
        /// </summary>
        public string FieldName { get; set; }
    }


    /// <summary>
    /// Use this class to filter through WebAPI.
    /// </summary>
    /// <typeparam name="T">Model that is going to be filtered</typeparam>
    public class QueryFilter<T> where T: class
    {
        /// <summary>
        /// "Where" condition. This is lambda expression.
        /// </summary>
        public Expression<Func<T, bool>> Where { get; set; }

        /// <summary>
        /// Order by
        /// </summary>
        public OrderInfo OrderBy { get; set; }

        /// <summary>
        /// How many records should be skipped. Mainly for pagination
        /// </summary>
        public int Skip { get; set; }

        /// <summary>
        /// How many records should be retrieved. Mainly for pagination
        /// </summary>
        public int Max { get; set; }

        public QueryFilter()
        {
            OrderBy = new OrderInfo();
            OrderBy.Direction = OrderDirection.NoOrder;
        }

        public static QueryFilter<T> FromDto(QueryFilterDto dto, Expression<Func<T, bool>> where)
        {
            QueryFilter<T> result = new QueryFilter<T>();
            result.Max = dto.Max;
            result.OrderBy = dto.OrderBy;
            result.Skip = dto.Skip;
            result.Where = where;

            return result;
        }

        public void CreateWhere(Expression<Func<T, bool>> where)
        {
            Where = where;
        }

        public void CreateOrder(OrderDirection direction, string fieldName)
        {
            OrderBy.Direction = direction;
            OrderBy.FieldName = fieldName;
        }


    }

    /// <summary>
    /// This is helper class that is send between client and WebApi. Use generic QueryFilter instead
    /// </summary>
    public class QueryFilterDto
    {
        public string ModelName { get; set; }
        public string Where { get; set; }
        public OrderInfo OrderBy { get; set; }
        public int Skip { get; set; }
        public int Max { get; set; }
    }

Potem mogę sobie pobrać listę wartości np. tak - po stronie klienta:

        public virtual async Task<ListResponse<T>> GetList(QueryFilter<T> filter)
        {
            QueryFilterDto filterDto = new QueryFilterDto();
            filterDto.Max = filter.Max;
            filterDto.OrderBy = filter.OrderBy;
            filterDto.Skip = filter.Skip;
            filterDto.Where = filter.Where.ToString();
            filterDto.ModelName = typeof(T).Name;

            var response = await client.PostJsonAsync(filter, GetListUri);
            if (!response.IsSuccessStatusCode)
                return await CreateListResponse(response, null);

            string content = await response.Content.ReadAsStringAsync();
            var items = JsonConvert.DeserializeObject<IList<T>>(content);
            return await CreateListResponse(response, items);
        }

A po stronie serwera jest coś takiego:
kontroler:

        [HttpPost]
        public async Task<IActionResult> Get([FromBody]QueryFilterDto filterDto)
        {
            Expression<Func<Order, bool>> where = await Utils.ExpressionFromStr<Order>(filterDto.Where);
            QueryFilter<Order> filter = QueryFilter<Order>.FromDto(filterDto, where);

            var result = await orderService.Get(filter);
            if (result.ErrorResult != ApiError.OK)
                return BadRequest(result.ErrorResult);
            else
                return Ok(result.Items);            
        }

Teraz cała tajemnica tkwi w Utils.ExpressionFromStr, które ze stringa robi z powrotem całe linq expression, które już w bardzo łatwy sposób możesz wykorzystać w zapytaniach SQL.
To wygląda tak:

public static class Utils
{
    public async static Task<Expression<Func<T, bool>>> ExpressionFromStr<T>(string expressionStr)
    {
        var options = ScriptOptions.Default.AddReferences(typeof(T).Assembly);
        return await CSharpScript.EvaluateAsync<Expression<Func<T, bool>>>(expressionStr, options);
    }
}

I kolejna tajemnica - wymaga Roslyn.Potrzebujesz takich dwóch nugetów:
Microsoft.CodeAnalysis.Scripting
Microsoft.VisualStudio.Web.CodeGeneration.Design

0

delegaty?

1

Nie do końca rozumiem problem. Jeśli masz różne zapytania to wystawiaj różne funkcje API odpowiadające odpowiednio Twoim wymaganiom. Bardziej "uniwersalne" zapytania mógłbyś ewentualnie wysyłać do tego samego endpointu z dodatkową flagą, np. QueryType. Po stronie serwera możesz np. zastosować polimorfizm i fabrykę. Odpowiednio: oddzielna klasa dla konkretnego rodzaju zapytania i funkcja która bierze QueryType i na jej podstawie tworzy instancję klasy przetwarzającej zapytanie. Wszystkie klasy mogą implementować interfejs IProductsQuery:

interface IProductsQuery
{
    IEnumerable<Product> FindProducts();
}

Wtedy w kontrolerze:

    var query = ProductsQueryFactory.Create(queryType);
    var products = query.FindProducts();
0

Widzę, że każdy ma jakiś swój sposób, czyli wnoszę, że jakiegoś jednego, stworzonego przez dev team microsoftu nie ma. Czy jest możliwość wyciągnięcia całego query? Np wpadłem na taki pomysł:

class Produkt
{
   int ID;
   string Kod;
   string EAN;
   string Nazwa;
   string Grupa;
   string Kategoria;
   decimal Ilosc;
   bool Promocja;
   bool Usluga;
}

[HttpGet]
public async Task<IActionResult> GetProdukty([FromQuery] string query)
{
   //query ma postać
   //?grupa=procesory&kategoria=elektronika&promocja=true&usluga=false
   string where = string.Empty;
   foreach(var property in typeof(Product).GetProperties())
   {
      if(query.Key.Contains(property.Name)
      {
          //generowanie Where
          where += property.Name + "=" + query.Value; 
          //To query.Value i query.Key jest tutaj abstrakcyjne. Ręcznie musiałbym wartość i klucze ze stringa wyjąć, ale nie muszę tego tutaj pisać :D
      }
   }
}

Zamiast foreach można by walnąć for, zawsze to o parę ms szybciej :D Ale nie podejrzewam, żebym miał jakoś super dużo takich parametrów, więc w miarę wydajne chyba będzie? Czy nie?

0

czyli wnoszę, że jakiegoś jednego, stworzonego przez dev team microsoftu nie ma

A to na wszystko musi być gotowe rozwiązanie od MS? Twój scenariusz brzmi jak coś konkretnego dla Twoich wymogów. Rozwiązanie które proponujesz wydaje się ok, ale moim zdaniem i tak niepotrzebnie to komplikujesz. Czy musisz mieć taką dynamiczność że nie będziesz wiedział jakie zapytania będzie trzeba obsłużyć? A jeśli odpowiedź brzmi "tak" to zadaj sobie kolejne pytanie- dla czego? Ogólnie API powinno być "strażnikiem" danych i ograniczać co na tych danych można wykonać- również jakie zapytania. Jeśli pozostaniesz przy swoim rozwiązaniu to pamiętaj żeby zabezpieczyć się przed SQL Injection.

0

No teoretycznie nie potrzebuje aż tak skomplikowanych zapytań ale jeśli jakiś warunek będzie mi potrzebny w przyszłości to już będę miał obsłużone i nie będzie trzeba dopisywać nowego zapytania, albo poprawiać istniejącego.

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