So, what's OData? following text which is copied from http://odata.org describes odata well:
OData (Open Data Protocol) is an ISO/IEC approved, OASIS standard that defines a set of best practices for building and consuming "RESTful APIs". OData helps you "focus on your business logic" while building RESTful APIs. OData RESTful APIs are easy to consume. The OData metadata, a machine-readable description of the data model of the APIs, enables the creation of powerful generic client proxies and tools.
Using bit framework, you can build OData services very easily, and we generate C# - TypeScript - JavaScript clients for you automatically. You can use those no matter you're developing xamarin forms, angular js, angular, react js & native etc. We also have out of the box support for Open-API (Swagger). Using azure auto rest tools, you can generate client side for almost any language you want.
An OData controller has full built-in support for paging/filtering/sorting/projection/grouping and aggregation.
At client side, use develop LINQ queries using C#/TypeScript/JavaScript, then we send that query to server side and server returns data based on your query. OData supports batch requests as well which results into better performance.
In bit apps, you develop odata controllers for your DTO (Data transfer objects) classes.
Instead of sending your "domain models/entities" to client, you send DTO to the client. Your "model/entities" gets complicated over time based on business requirements, and at the client side you need something less complicated and easier to use. DTO (Something similar to ViewModel in MVC) is a common best practice in modern software development world.
"Model/Entity" - DTO examples:
Example 1: Consider following "models/entities":
public class Product : IEntity
{
public int Id { get;set; }
public string Name { get; set; }
public decimal Price { get; set; }
public Category Category { get; set; }
[ForeignKey(nameof(Category))]
public int CategoryId { get; set; }
}
public class Category : IEntity
{
public int Id { get;set; }
public string Name { get; set; }
}
You can have following DTO:
public class ProductDto : IDto
{
public int Id { get;set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public string CategoryName { get; set; } // We've added "Name" of category as "CategoryName" into ProductDto class. So, at client side, we can create a list of products very easily and every product has its cateogry name included.
}
This is called flattening.
Example 2: Consider following "models/entities":
public class Customer : IEntity
{
public int Id { get;set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool IsActive { get; set; }
public List<Order> Orders { get; set; } = new List<Order>();
}
public class Order : IEntity
{
public int Id { get;set; }
public string Description { get; set; }
public Customer Customer { get; set; }
[ForeignKey(nameof(Customer))]
public int CustomerId { get; set; }
}
You can have following DTO:
public class CustomerDto : IDto
{
public int Id { get;set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int OrdersCount { get; set; } // count of orders. You need this in one of your forms for example.
}
You can create as many as DTOs from your "models/entities". You can add calculated fields, apply flattening etc.
To follow best practices, keep these rules in your mind:
1- Do not inherit from "models/entities" in your DTO classes. For example, CustomerDto does not inherit from Customer.
2- Do not declare a property with type of your "models/entities" in your DTO classes. For example, ProductDto has no property of type Category model. Do not use models/enities enums and complex types in your dto clasess too.
3- Develop a DTO for every Task you've. For example, if you want to show customers names and their orders count, create a DTO for this task. And when you want to create a Customer registration form which accepts credit card number, develop a new DTO for that task.
To send DTO to client side, you develop DtoController. Examples can be found here.
In Bit-OData, you develop DtoController instead of ApiController. Note that you can continue developing API controllers side by side.
So, lets take a look at new codes. First you've to configure web api & odata toghether by following code:
dependencyManager.RegisterDefaultWebApiAndODataConfiguration(); // instead of dependencyManager.RegisterDefaultWebApiConfiguration();
You can optionally add web api to your app by following the code as like as what you did before:
dependencyManager.RegisterWebApiMiddleware(webApiDependencyManager =>
{
webApiDependencyManager.RegisterWebApiMiddlewareUsingDefaultConfiguration();
});
And you can use following code to add OData to your app:
dependencyManager.RegisterODataMiddleware(odataDependencyManager =>
{
odataDependencyManager.RegisterODataServiceBuilder<BitODataServiceBuilder>();
odataDependencyManager.RegisterODataServiceBuilder<MyAppODataServiceBuilder>();
odataDependencyManager.RegisterWebApiODataMiddlewareUsingDefaultConfiguration();
});
In MyAppODataServiceBuilder class, you've access to ODataModelBuilder, which is useful in advanced scenarios. We create OData metadata from your DTO classes automatically, so you don't have to do anything special in most cases. So just configure your odata route:
public override string GetODataRoute()
{
return "MyApp"; // http://localhost/odata/MyApp/...
}
In CustomersController you see these codes:
public virtual IDtoEntityMapper<CustomerDto, Customer> Mapper { get; set; }
[Function]
public virtual async Task<IQueryable<CustomerDto>> GetActiveCustomers(CancellationToken cancellationToken)
{
return Mapper.FromEntityQueryToDtoQuery((await CustomersRepository.GetAllAsync(cancellationToken)).Where(c => c.IsActive == true));
}
Mapper automatically maps "model/entities" classes to DTO classes. It uses AutoMapper by default and the way we use auto mapper will not slow down your app as described here.
Note that you don't have to use bit repository here. You don't have to use entity framework either. You can use mongo db, simple array etc. We need some customers only.
[Function] is used to return data, it has to return data, and it accepts simple type parameters like int, string, enum, etc. [Action] is used to do something, its return value is optional, and it accepts almost anything.
Run the app and you're good to go. That's a swagger's console you see by default. By opening http://localhost:9000/odata/MyApp/$metadata you see $metadata. $metadata describes your DTO classes, complex types, enums, actions and functions in a standard format. There are tools in several languages to generate client side proxy for you. You can see the list of libraries and tools here. Note that you can call OData controllers using jquery ajax, fetch, etc too, as they're REST APIs.
Try GetActiveCustomers, It runs something like this on a database:
select * from Customers inner join Cities on Id = CustomerId where IsActive = 1 /*1 means true. It comes from your server side linq query: Where(c => c.IsActive == true))*/
It accepts several parameters such as $filter, $order by etc. These are standard OData parameters and they work no matter where the data is come from (Bit repository, entity framework's db context, simple array etc).
Try CityId eq 1 for $filter, it returns customers located in City 1 only!
It runs something like this on a database:
select * from Customers inner join Cities on Id = CustomerId where IsActive = 1 and CityId = 1
As you see, the filter we've developed at server side (c => c.IsActive == true) is combined with query we passed from client side (CityId eq 1). Filters are combined with "AND", so there is no security risk at all.
Try Id,FirstName,LastName for $select, it returns those properties of customers only!
It runs something like this on a database:
select Id,FirstName,LastName from Customers where IsActive = 1 /*true*/
There is no join between customers and cities as we've not requested CityName property. It's smart!
OData supports filtering, ordering, projection, paging etc in a standard way. Almost all UI vendors have support for OData in their rad components such as data grid etc. You can create amazing excel sheets and dashboard using its odata support. In C#/TypeScript/JavaScript you're able to write LINQ queries to consume odata resources. For example:
context.customers.GetActiveCustomers().Where(c => c.CityId == 1).ToArray(); // C#
// This will be converted to $fitler > CityId eq 1
context.customers.getActiveCustomers().filter(c => c.CityId == 1).toArray(); // JavaScript/TypeScript
OData action:
GetActiveCustomers is an OData function. Let's write an OData action to send an email to a customer by customerId and message. (Imaging every customer has an email in database).
public class SendEmailToCustomerArgs
{
public int customerId { get; set; }
public string message { get; set; }
}
public async Task SendEmailToCustomer(SendEmailToCustomerArgs args)
{
// ...
}
As you can see this action has no return value (Task is an async equivalent of void). There is a class called SendEmailToCustomerArgs. Each property of that class describes one of your parameters. It can have a properties such as public Customer[] customers { get; set; } etc.
To accept a complex type, you've to define your complex type as following:
[ComplexType]
public class Location
{
public int Lat { get;set; }
public int Lon { get;set; }
}
SendEmailToCustomerArgs can accepts following parameters too:
public class SendEmailToCustomerParams
{
public Location location { get; set; } // complex type parameter
public Customer[] customers { get; set; } // array of customer dto
}
OData Single result:
Imaging you want to create a function which returns customer by its Id. You might develop something like this:
[Function]
public virtual async Task<CustomerDto> GetCustomerById(int customerId, CancellationToken cancellationToken)
{
return await Mapper.FromEntityQueryToDtoQuery((await CustomersRepository.GetAllAsync(cancellationToken)))
.FirstAsync(c => c.Id == customerId, cancellationToken);
}
First, it converts model query to dto query, then it returns customer by id. We recommend you to use SingleResult here. It has following advantages:
1- In case no data is found, it returns 404 - NotFound status code.
2- It supports odata queries such as $select, so if you want, you can return some properties of the customer when you call GetCustomerById
Let's rewrite that as following:
[Function]
public virtual async Task<SingleResult<CustomerDto>> GetCustomerById2(int customerId, CancellationToken cancellationToken)
{
return SingleResult.Create(Mapper.FromEntityQueryToDtoQuery((await CustomersRepository.GetAllAsync(cancellationToken)))
.Where(c => c.Id == customerId)); // We use .Where instead of .First
}
Associations:
Imaging you've CategoryDto & ProductDto classes where one category has many products. You can write followings:
public class CategoryDto : IDto
{
public int Id { get; set; }
public string Name { get; set; }
[InverseProperty(nameof(ProductDto.Category))]
public List<ProductDto> Products { get; set; }
}
public class ProductDto : IDto
{
public int Id { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
[ForeignKey(nameof(CategoryId))]
[InverseProperty(nameof(CategoryDto.Products))]
public CategoryDto Category { get; set; }
}
You provide such a details for following reasons:
1- OData is a query language, for example, you can execute complicated queries such as "categories without products" ($filter) directly from the client side. You can load categories which are located in the specific city with or without their products ($exapnd).
2- We offer a local (sqlLite-webSql-indexedDb) database creation from your DTO(s) in C#/JavaScript/TypeScript, so you can use linq queries to work with the offline database as like as your odata server. We also offer a sync service to push/pull changes to/from online server and local database. To create that database, we need those information.
DtoSetController:
DtoSetController needs 3 things: Model-Dto-Repository. It gives you Create-Read-Update-Delete over that repository. It's really simple and extendable. You can write actions/functions there and you can override following Create|Read|Update|Delete methods to customize them.
Custom Dto-Model mapping:
Imaging CategoryDto has a property called ProductsCount. It's a calculated property by something like this: category => category.Products.Count()
To handle this, you've to develop custom mapping using AutoMapper facilities. For example:
public class MyAppDtoEntityMapperConfiguration : IDtoEntityMapperConfiguration
{
public virtual void Configure(IMapperConfigurationExpression mapperConfigExpression)
{
mapperConfigExpression.CreateMap<Category, CategoryDto>()
.ForMember(category => category.ProductsCount, config => config.MapFrom(category => category.Products.Count()));
}
}
// AppStartup class
dependencyManager.RegisterDtoEntityMapperConfiguration<MyAppDtoEntityMapperConfiguration>();
Run the app, you can insert/update/delete/read categories and products using swagger ui. You can invoke OData queries such as $filter there.
C# / JavaScript / TypeScript client
Background Job Worker
Singalr
Identity Server
Automated Tests
Project creation