Actions And Functions In OData V4 Using ASP.NET Web API



Introduction

Sometimes we need to perform an operation which is not directly related to entity. The actions and functions are the way to perform server side operations which are not easily defined as CRUD operations on entities.

We can define actions and functions to OData V4 endpoint with Web API. Both actions and functions are able to return data. The following are some use of actions.

  • Sending the data which are not related to entity
  • Sending several entities at once
  • Complex transactions
  • Update few property of entity

The functions are very useful for returning data which are not directly related to entity or collection. An action and function is able to target a single entity or collection of entities. This is called binding in term of OData terminology. We can also have unbound action or function which can be referred to as static operation on service.

Action

Actions will provide a way to inject behaviors into the current Web API action without affecting current data model.

Suppose I have two tables, Employee and EmployeeRating. The relation between the entities is shown below.



The following is the class definition and Model definition,

  1. namespace WebAPITest {  
  2.     using System.ComponentModel.DataAnnotations;  
  3.     using System.ComponentModel.DataAnnotations.Schema;  
  4.   
  5.     [Table("Employee")]  
  6.     publicpartialclassEmployee {  
  7.         publicint Id {  
  8.             get;  
  9.             set;  
  10.         }  
  11.   
  12.         [Required]  
  13.         [StringLength(50)]  
  14.         publicstring Name {  
  15.             get;  
  16.             set;  
  17.         }  
  18.   
  19.         publicint DepartmentId {  
  20.             get;  
  21.             set;  
  22.         }  
  23.   
  24.         [Column(TypeName = "money")]  
  25.         publicdecimal ? Salary {  
  26.             get;  
  27.             set;  
  28.         }  
  29.   
  30.         [StringLength(255)]  
  31.         publicstring EmailAddress {  
  32.             get;  
  33.             set;  
  34.         }  
  35.   
  36.         [StringLength(50)]  
  37.         publicstring PhoneNumber {  
  38.             get;  
  39.             set;  
  40.         }  
  41.   
  42.         publicstring Address {  
  43.             get;  
  44.             set;  
  45.         }  
  46.   
  47.         publicvirtualEmployeeRating EmployeeRating {  
  48.             get;  
  49.             set;  
  50.         }  
  51.     }  
  52.   
  53.     [Table("EmployeeRating")]  
  54.     publicpartialclassEmployeeRating {  
  55.         [Key]  
  56.         [DatabaseGenerated(DatabaseGeneratedOption.None)]  
  57.         publicint EmployeeId {  
  58.             get;  
  59.             set;  
  60.         }  
  61.   
  62.         publicdouble Rating {  
  63.             get;  
  64.             set;  
  65.         }  
  66.   
  67.         publicvirtualEmployee Employee {  
  68.             get;  
  69.             set;  
  70.         }  
  71.     }  
  72. }  
  73.   
  74. namespace WebAPITest {  
  75.     using System.Data.Entity;  
  76.     publicpartialclassEntityModel: DbContext {  
  77.         public EntityModel(): base("name=EntityModel") {}  
  78.   
  79.         publicvirtualDbSet < Employee > Employees {  
  80.             get;  
  81.             set;  
  82.         }  
  83.         publicvirtualDbSet < EmployeeRating > EmployeeRatings {  
  84.             get;  
  85.             set;  
  86.         }  
  87.   
  88.         protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) {  
  89.             modelBuilder.Entity < Employee > ()  
  90.                 .Property(e => e.Name)  
  91.                 .IsUnicode(false);  
  92.   
  93.             modelBuilder.Entity < Employee > ()  
  94.                 .Property(e => e.Salary)  
  95.                 .HasPrecision(19, 4);  
  96.   
  97.             modelBuilder.Entity < Employee > ()  
  98.                 .HasOptional(e => e.EmployeeRating)  
  99.                 .WithRequired(e => e.Employee);  
  100.         }  
  101.     }  
  102. }  
Action has a side effect on server, so that they are invoked by using HTTP POST requests. Action may have parameters and can have return type. The parameters and return type are described in the service metadata. The client sends the parameter values in in request body and server send the return value in response body.

In order to declare the action, we need to modify Web API configuration to add the action parameter to EDM (entity data model).

The EntityTypeConfiguration class has method called "Action" is used to add an action to the EDM (entity data model). The Parameter method is used to specify a typed parameter for defined action.
  1. ODataConventionModelBuilder builder = newODataConventionModelBuilder();  
  2. builder.Namespace = "WebAPITest";  
  3. builder.ContainerName = "DefaultContainer";  
  4. builder.EntitySet < EmployeeRating > ("EmployeeRating");  
  5. builder.EntitySet < Employee > ("Employee");  
  6.   
  7. builder.EntityType < Employee > ()  
  8.  .Action("Rate")  
  9.  .Parameter < double > ("Rating");  
  10.   
  11. var edmModel = builder.GetEdmModel();  
The above code is also specifying the namespace for the EDM. The namespace is required by URI and that includes with action name to create fully-qualified action name.



Definition of Action in OData controller

To enable "Rate" action, we need to add the following method to the EmployeeControler.
  1. [HttpPost]  
  2. publicasyncTask<IHttpActionResult> Rate([FromODataUri] int key, ODataActionParameters parameters)  
  3. {  
  4. double rating = (double)parameters["Rating"];  
  5. //Do Code…  
  6. return StatusCode(HttpStatusCode.NoContent);  
  7. }  
Here, I am using IIS expression to host service on development environment and I am using Telerik's Fiddler as a client application. Now, I am posting data using following URI in Fiddler.

URI - http://localhost:24367/Employee(1)/WebAPITest.Rate



The request body contains the parameter's value in form of JSON. Web API is automatically convert it in to ODataActionParameters object. This object is just dictionary type. This object can be used to retrieve value in controller.

When I am posting data using said URI, I am getting 404 –Not Found error. Here DOT (.) is present in URI; it will create issue with IIS and returns 404 error. We can resolve this issue by adding the following setting in web.config file.
  1. <system.webServer>  
  2.     <handlers>  
  3.         <clear/>  
  4.         <addname="ExtensionlessUrlHandler-Integrated-4.0" path="/*" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />  
  5.     </handlers>  
  6. </system.webServer>  



Functions

There are two types of function supported by OData
  1. Bound function

    The first step to add OData function to the controller is to modify the web API configuration. By using "function" method of EntityTypeConfiguration class, we can define the function and using "Returns" method we can define the return type of the function. The function can invoked by the client using GET request.
    1. ODataConventionModelBuilder builder = newODataConventionModelBuilder();  
    2. builder.Namespace = "WebAPITest";  
    3. builder.ContainerName = "DefaultContainer";  
    4. builder.EntitySet<EmployeeRating>("EmployeeRating");  
    5. builder.EntitySet<Employee>("Employee");  
    6.   
    7. builder.EntityType<Employee>().Collection  
    8. .Function("GetHighestRating")  
    9. .Returns<double>();  
    Controller function definition as following-
    1. [HttpGet]  
    2. publicIHttpActionResult GetHighestRating()  
    3. {  
    4. var rating = context.EmployeeRatings.Max(x => x.Rating);  
    5. return Ok(rating);  
    6. }  
    Here URI pattern for the function is same Action I.e. namespace.functionname

    URL - http://localhost:24367/Employee/WebAPITest.GetHighestRating

    Output




    In the above example, function was bound to the collection. It is normally refer as Bound function.

  2. Unbound function

    Unbound functions are normally not bound to any specific service and they are called as static operation on service.

    Following code change needs to be done in web API configuration-
    1. ODataConventionModelBuilder builder = newODataConventionModelBuilder();  
    2. builder.Namespace = "WebAPITest";  
    3. builder.ContainerName = "DefaultContainer";  
    4. builder.EntitySet<EmployeeRating>("EmployeeRating");  
    5. builder.EntitySet<Employee>("Employee");  
    6.   
    7. builder.Function("GetEmployeeRating")  
    8. .Returns<double>()  
    9. .Parameter<int>("EmployeeId");  
    In the following function, I have passed employee id as parameter and function returns its rating. This function does not depends on the Employee type, so we can create this function in the separate controller. The [ODataRoute] attribute used to define the URI template for the function.
    1. [HttpGet]  
    2. [ODataRoute("GetEmployeeRating(EmployeeId={employeeId})")]  
    3. publicIHttpActionResult GetEmployeeRating(int employeeId)  
    4. {  
    5. var rating = context.Employees.Where(f => f.Id == employeeId).FirstOrDefault().EmployeeRating.Rating;  
    6. return Ok(rating);  
    7. }  
    URL - http://localhost:24367/GetEmployeeRating(EmployeeId=1)

    Output


Summary

Web API 2 supports the Action and function with OData and it is very helpful to perform the operation which is not directly related to entity.