Skip to content

A Dotnet Core compatible and typed OData client using LINQ expressions

Notifications You must be signed in to change notification settings

WimJongeneel/cs-odata-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

C# OData Client

var result = await oDataContext.Products
    .Select(x => new { x.ProductID, x.ProductName })
    .Filter(x => x.ProductID < 10)
    .Expand(r => r.Order_Details.Select(o => o.OrderID )
            .Expand(r => r.Order.Select(o => new { o.ShipCity, o.ShipAddress, o.ShipName, o.ShipRegion })
                .Expand(r => r.Employee.Select(e => $"{e.FirstName} {e.LastName}"),
                    (r, e) => new { Order = r, Employee = e })
                .Expand(r => r.Shipper.Select(s => s.CompanyName),
                    (r, s) => new { r.Employee, r.Order, Shipper = s }),
                (d, o) => o
            ),
        (a,b) => new { Order_Details = a, Orders = b }
    )
    .Expand(r => r.Category.Select(s => s.CategoryName), 
        (r, c) => new { r.Order_Details, r.Orders, Category = c })
    .Orderby(p => p.Supplier.City)
    .ExecuteAsync();

An example query

Table of contents

Architectural overview

This client aims to provide a convenient and type-safe way to integrate an OData source into your project. For this the library comes with a code generator that generates all classes and boilerplate for you. The query builder itselfs is strongly inspired by Entity Framework and uses Linq expressions to make the OData queries as close to normal C# as possible.

This means that the client heavily relies on the Expression type and uses various Linq-to-OData compilers for the different parts of the queries. The client provides a complete end-to-end integration of OData, meaning that it doesn’t only allow you to build queries but takes care of execution and deserialisation. To make the HTTP requests needed to run a query the ODataContext depends on a IHttpClientFactory. This can be added to your services with services.AddHttpClient();. The deserialisation happens by reflection and composition of the lambda’s provided when building the query.

Outside of this complete package the library also exposed an API to generated parts of an OData query. Those are the FilterExpression, SelectExpression and OrderbyExpression classes who all expose a static Compile method that can be used to compile individual lambda’s to their OData counterparts.

Building queries

The building of queries starts of with an instance of the ODataContext, as generated by the code generator.

Select

With Select you can select data from the odata source by providing a lambda. From this function the referenced attributes will be extracted and they will be included in the $select part of the OData query. The function itself will be run as the first part of the deserialization process. Inside the lambda you can use any language construct that you want.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName });

Filter

With Filter you can add a filter to the OData query. Filter expressions do get compiled into an OData expression. Therefore, not all C# language constructs are allowed. Using an unsupported construct will result in an exception. Supported constructs are:

  • member access on the argument
  • referencing or invoking anything that isn’t on the argument, this will be converted to the resulting value
  • binary operators: most of the algebraic, logical and comparison operators
  • unary operators: negation, not, incrementation and decrementation
  • constants
  • DateTime operations: the magic get accessors of C# do get compiled to functions in OData. Supported are: Day, Hour, Minute, Month, Second and Year
  • String methods: ToLower, ToUpper, Trim, Contains, Length, Substring, StartsWith, EndsWith, IndexOf and Replace
  • Math methods: Round, Floor and Ceiling
var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .Filter(x => x.ProductID < 10 && x.Category.CategoryName.Contains("Foo"));

Expand

With Expand you can add an $expand to your query. Expands come in two flavours: expands to a collection or to a single object (e.g. to-1 and to-n relations). There are two things needed for an expansion: the inner query and the merger function.

The inner query is constructed in a lambda from the relations builder to a query. In this query you can use any OData operator that is supported by the type of relation.

The merger function takes the result of the query up until now and the result of the inner query as arguments and returns the new combined result. Think of this as the last argument of Join in Entity Framework. This merger function is only used in the deserialization process and does not get compiled. And C# code is allowed in here. You should account for null-values in the second argument in case of optional relations.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .Expand(r => r.Order_Details.Select(o => o.OrderID),
        (a,b) => new {Order_Details = a, Orders = b}
    );

Orderby

With Orderby you can order the result. This method accepts a lambda from the type of the source to the attribute that should be sorted on.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .Orderby(x => x.Supplier.City);

OrderbyThen

With OrderbyThen you can add an additional order to the result. This method accepts a lambda from the type of the source to the attribute that should be sorted on.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .OrderbyThen(x => x.Supplier.City);

OrderbyDescending

With OrderbyDescending you can order the result descending. This method accepts a lambda from the type of the source to the attribute that should be sorted on.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .OrderbyDescending(x => x.Supplier.City);

OrderbyDescendingThen

With OrderbyDescendingThen you can add an additional descending order to the result. This method accepts a lambda from the type of the source to the attribute that should be sorted on.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .OrderbyDescendingThen(x => x.Supplier.City);

Top

With Top you can set the $top of the query.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .Top(10);

Skip

With Skip you can set the $skip of the query.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .Skip(10);

ExecuteAsync

ExecuteAsync runs the query and returns the value.

var q = await oDataContext.Products
    .Select(x => new{ x.ProductID, x.ProductName })
    .ExecuteAsync(10);

Code generation

In the DTF there is a command present that allows you generate a context via the CLI. See here

dotnet run cs-odata -u "https://services.odata.org/V4/Northwind/Northwind.svc" -o "ODataContext.cs"

With the SchemaCodeGenerator you can generate an ODataContext from a XML schema. The Generate method expects an url to the $metadata and a SchemaCodeGeneratorOptions object. The options options are:

SelectOnRelations

Values: None, ToOne or All.

Sets if (and which) relations of an entity can be referenced in the Select lambda.

not yet implemented

FilterOnRelations

Values: None, ToOne or All.

Sets if (and which) relations of an entity can be referenced in the Filter predicate. OrderbyOnRelations Sets if (and which) relations of an entity can be referenced in the Orderby lambda.

Compilation API

Next to the end-to-end querybuilder this library also provides an API to build parts of an OData query. With the FilterExpression, SelectExpression and OrderbyExpression classes you can compile C# lambda’s into various OData expressions for usages in custom clients.

var select = SelectExpression.Compile<MyType>(x => new { x.Foo, x.Bar });
var filter = FilterExpression.Compile<MyType>(x => x.Foo.StartsWith("baz") && x.Bar > 2);
var orderby = OrderbyExpression.Compile<MyType>(x => x.Bar);

About

A Dotnet Core compatible and typed OData client using LINQ expressions

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages