OData Fundamentals

💡 Vad är OData?

OData (Open Data Protocol) är som SQL för webb-API:er!

Tänk dig att du är på ett bibliotek. Istället för att säga “ge mig alla böcker” (vanlig REST), kan du med OData säga “ge mig de 10 första böckerna skrivna efter 2020, sorterade efter titel, men bara titel och författare”. Bibliotekaren förstår och ger dig exakt det!

🎯 TL;DR

OData = REST + SQL-liknande queries. Filtrera, sortera, paginera och välja fält direkt i URL:en. Microsoft’s standard för enhetliga API:er med kraftfulla query-möjligheter.

📋 Efter att ha läst detta kommer du att kunna

  • Förstå OData-principerna och query-syntax
  • Implementera OData i ASP.NET Core
  • Skriva komplexa OData-queries
  • Hantera filtrering, sortering och paginering
  • Jämföra OData med REST och GraphQL

🔧 Grundprinciper

Princip 1: URL-baserade queries

Allt händer i URL:en - inga POST requests för att hämta data.

Princip 2: SQL-liknande syntax

$filter, $orderby, $select, $top - precis som SQL.

Princip 3: Standardiserat

Samma syntax fungerar för alla OData-API:er.

💭 OData Query-syntax

💬 OData queries läggs till som query parameters:


GRUNDLÄGGANDE QUERIES:
$select   - Välj specifika fält
$filter   - Filtrera resultat
$orderby  - Sortera
$top      - Begränsa antal
$skip     - Hoppa över x antal
$expand   - Inkludera relaterad data

EXEMPEL:
GET /api/products?$select=name,price
GET /api/products?$filter=price gt 100
GET /api/products?$orderby=name desc
GET /api/products?$top=10&$skip=20
GET /api/products?$expand=category

graph TB A[OData Request] --> B{Query Type} B -->|$filter| C[Filtrera data] B -->|$select| D[Välj fält] B -->|$orderby| E[Sortera] B -->|$top/$skip| F[Paginering] B -->|$expand| G[Expandera relationer] C --> H[Filtrerat resultat] D --> I[Enbart valda fält] E --> J[Sorterad data] F --> K[Paginerad data] G --> L[Data + relationer]

🚀 OData i praktiken

🟢 Grundläggande OData-syntax


# Hämta alla produkter
GET /api/products

# Bara namn och pris
GET /api/products?$select=name,price

# Produkter dyrare än 500kr
GET /api/products?$filter=price gt 500

# Sortera efter pris, högst först
GET /api/products?$orderby=price desc

# Första 5 produkterna
GET /api/products?$top=5

# Hoppa över 10, ta nästa 5
GET /api/products?$skip=10&$top=5

# Kombinerat query
GET /api/products?$filter=category eq 'Electronics'&$orderby=price&$select=name,price&$top=10

🟡 OData i ASP.NET Core


// Install: dotnet add package Microsoft.AspNetCore.OData

// 1. Definiera din data-model
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
    public DateTime CreatedDate { get; set; }
    public bool IsActive { get; set; }

    // Navigation property för relationer
    public virtual ICollection<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public string CustomerName { get; set; }
    public DateTime OrderDate { get; set; }
    public int Quantity { get; set; }

    public virtual Product Product { get; set; }
}

// 2. Konfigurera OData i Program.cs
using Microsoft.AspNetCore.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

var builder = WebApplication.CreateBuilder(args);

// Bygg OData EDM model
static IEdmModel GetEdmModel()
{
    var odataBuilder = new ODataConventionModelBuilder();
    odataBuilder.EntitySet<Product>("Products");
    odataBuilder.EntitySet<Order>("Orders");
    return odataBuilder.GetEdmModel();
}

// Lägg till OData services
builder.Services.AddControllers()
    .AddOData(options => options
        .Select()           // Tillåt $select
        .Filter()           // Tillåt $filter
        .OrderBy()          // Tillåt $orderby
        .SetMaxTop(100)     // Max 100 items
        .Count()            // Tillåt $count
        .Expand()           // Tillåt $expand
        .AddRouteComponents("api", GetEdmModel())
    );

var app = builder.Build();

app.MapControllers();
app.Run();

// 3. Skapa OData Controller
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly List<Product> _products;

    public ProductsController()
    {
        // Dummy data för demo
        _products = new List<Product>
        {
            new() { Id = 1, Name = "Laptop", Price = 15999, Category = "Electronics", CreatedDate = DateTime.Now.AddDays(-10), IsActive = true },
            new() { Id = 2, Name = "Mouse", Price = 299, Category = "Electronics", CreatedDate = DateTime.Now.AddDays(-5), IsActive = true },
            new() { Id = 3, Name = "Book", Price = 199, Category = "Books", CreatedDate = DateTime.Now.AddDays(-2), IsActive = false }
        };
    }

    // OData endpoint - automatisk query-hantering
    [HttpGet]
    [EnableQuery] // Detta är magin - aktiverar OData queries
    public IQueryable<Product> Get()
    {
        return _products.AsQueryable();
    }

    // Specifik produkt med OData-support
    [HttpGet("{id}")]
    [EnableQuery]
    public IActionResult Get(int id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product == null) return NotFound();

        return Ok(product);
    }
}

🔴 Avancerade OData-queries


# FILTER-OPERATORER:
eq  - lika med         : price eq 100
ne  - inte lika        : category ne 'Books'
gt  - större än        : price gt 500
ge  - större eller lika: price ge 100
lt  - mindre än        : createdDate lt 2024-01-01T00:00:00Z
le  - mindre eller lika: price le 1000

# STRING-FUNKTIONER:
contains   : contains(name,'Lap')           # Innehåller
startswith : startswith(name,'Mac')         # Börjar med
endswith   : endswith(name,'Pro')           # Slutar med
length     : length(name) gt 5              # Längd
tolower    : tolower(name) eq 'laptop'      # Till gemener

# DATUM-FUNKTIONER:
year   : year(createdDate) eq 2024
month  : month(createdDate) eq 12
day    : day(createdDate) eq 25

# LOGISKA OPERATORER:
and : price gt 100 and category eq 'Electronics'
or  : category eq 'Books' or category eq 'Electronics'
not : not (isActive eq false)

# KOMPLEXA QUERIES:
GET /api/products?$filter=contains(tolower(name),'laptop') and price gt 10000&$orderby=price desc&$select=name,price

# EXPANDERA RELATIONER:
GET /api/products?$expand=orders&$filter=orders/any(o: o/quantity gt 5)

🔧 Anpassad OData-logik


// Anpassad validering och begränsningar
[HttpGet]
[EnableQuery(MaxTop = 50, AllowedQueryOptions =
    AllowedQueryOptions.Select |
    AllowedQueryOptions.Filter |
    AllowedQueryOptions.OrderBy)]
public IQueryable<Product> GetProducts()
{
    return _products.AsQueryable();
}

// Custom query-hantering
[HttpGet("search")]
public IActionResult Search(ODataQueryOptions<Product> options)
{
    IQueryable<Product> query = _products.AsQueryable();

    // Anpassad logik innan OData tillämpas
    if (options.Filter != null)
    {
        query = options.Filter.ApplyTo(query, new ODataQuerySettings()) as IQueryable<Product>;
    }

    // Lägg till egen affärslogik
    query = query.Where(p => p.IsActive);

    if (options.OrderBy != null)
    {
        query = options.OrderBy.ApplyTo(query) as IQueryable<Product>;
    }

    // Räkna innan paginering för metadata
    var totalCount = query.Count();

    if (options.Skip != null)
    {
        query = options.Skip.ApplyTo(query, new ODataQuerySettings()) as IQueryable<Product>;
    }

    if (options.Top != null)
    {
        query = options.Top.ApplyTo(query, new ODataQuerySettings()) as IQueryable<Product>;
    }

    var result = new
    {
        value = query.ToList(),
        count = totalCount
    };

    return Ok(result);
}

📊 OData vs REST vs GraphQL

FeatureRESTODataGraphQL
FiltreringAnpassade endpoints$filter=price gt 100where: {price: {gt: 100}}
SorteringQuery params$orderby=name descorderBy: {name: DESC}
PagineringAnpassad$skip=10&$top=5skip: 10, first: 5
Fält-urvalAnpassad$select=name,price{name, price}
RelationerFlera requests$expand=orders{orders {id}}
StandardIngetMicrosoft standardGraphQL standard
KomplexitetLågMediumHög

⚠️ Vanliga misstag och begränsningar


// SÄKERHET: Begränsa vad som är tillåtet
[EnableQuery(
    AllowedQueryOptions = AllowedQueryOptions.Filter | AllowedQueryOptions.OrderBy,
    MaxTop = 100,
    MaxSkip = 1000)]
public IQueryable<Product> GetProducts()
{
    // VIKTIGT: Filtrera känslig data före OData
    return _products
        .Where(p => p.IsActive) // Affärslogik först
        .AsQueryable();
}

// Performance: Var försiktig med $expand
[EnableQuery(MaxExpansionDepth = 2)] // Begränsa djup
public IQueryable<Product> GetProductsWithOrders()
{
    return _products.AsQueryable();
}

// Validering: Kontrollera queries
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var request = context.HttpContext.Request;

        // Blockera farliga queries
        if (request.QueryString.Value?.Contains("password") == true)
        {
            context.Result = new BadRequestResult();
            return;
        }

        base.OnActionExecuting(context);
    }
}

🛠️ Testing OData endpoints


// Unit test för OData
[Test]
public async Task GetProducts_WithFilter_ReturnsFilteredResults()
{
    // Arrange
    var client = _factory.CreateClient();

    // Act
    var response = await client.GetAsync("/api/products?$filter=price gt 1000&$select=name,price");

    // Assert
    response.EnsureSuccessStatusCode();
    var json = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<ODataResponse<Product>>(json);

    Assert.All(result.Value, p => Assert.True(p.Price > 1000));
}

public class ODataResponse<T>
{
    public List<T> Value { get; set; }
    public int Count { get; set; }
}

📚 Sammanfattning

OData ger dig SQL-kraft i webb-API:er:

  • Standardiserad query-syntax
  • Kraftfull filtrering och sortering
  • Automatisk paginering och metadata
  • Expandera relationer i samma request
  • Microsoft-standard med bra verktyg
  • Perfekt för data-tunga applikationer

🎯 Övningar

  1. 🟢 Grundläggande: Skapa ett OData API för Books med filter och sortering
  2. 🟡 Utmanande: Lägg till relationer (Author, Publisher) med $expand
  3. 🔴 Expert: Implementera custom query validation och säkerhet

😄 Obligatorisk dad joke

Varför gillar databas-administratörer OData? För att de äntligen kan köra sina SELECT-queries direkt i URL:en! 📊


Upp

Upp


Licens: Apache 2.0 | © 2023 Marcus Medina, Campus Mölndal. Alla rättigheter förbehållna.
Du får använda och modifiera detta verk enligt villkoren i Apache License, Version 2.0. Du får inte använda detta verk för kommersiella ändamål utan tillstånd från upphovsmannen.