Method Event
Summary
Methods are custom operations defined on entities using the [Method] attribute and implemented through the event system. They are automatically exposed as OData endpoints, enabling clients to invoke business operations through the API.
When does it fire?
A method fires when explicitly called — either from code or via an HTTP request to the OData API endpoint.
Defining Methods
Methods are declared on the entity class with the [Method] attribute:
[ApiEntity]
public partial class SalesOrder : EntityBase
{
[Method("Reorder from another order", MethodType.Modify)]
public partial void ReorderFrom(Guid salesOrderGuid);
[Method("Calculate line total", MethodType.Read)]
public partial decimal CalculateLineTotal(decimal taxRate, int quantity, decimal discount = 0m);
[Method("Get all open orders", MethodType.Read)]
public static partial IQueryable<SalesOrder> GetOpenOrders(string customerId);
}
MethodType
| Type | HTTP Verb | Description |
|---|---|---|
MethodType.Read |
GET | Returns data without modifying state |
MethodType.Modify |
POST | Changes data (can save, delete, etc.) |
Parameter rules
- Required: Non-nullable parameters without a default value
- Optional: Nullable parameters or parameters with a default value
- Missing required parameters return
400 BadRequest
Implementing Methods
Instance method
entity.Method(e => e.MethodName).Do((entity, args) =>
{
// args.<parameterName> for each method parameter
// args.GetService<T>() for DI services
// args.GetEntities<T>() for data access
});
Static method
entity.Method(_ => EntityType.StaticMethodName).Do(args =>
{
// No entity instance — static context
return args.GetEntities<EntityType>().Where(...);
});
Scenarios
1. Reordering from a previous sales order
Copy details from a previous order into a new order. This is a modify method because it changes data.
[ApiEntity]
public partial class SalesOrder : EntityBase
{
[Method("Reorder from another order", MethodType.Modify)]
public partial void ReorderFrom(Guid salesOrderGuid);
}
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void ReorderFromMethod()
{
salesOrder.Method(e => e.ReorderFrom).Do((order, args) =>
{
var context = args.Context;
var sourceOrder = args.GetEntity<SalesOrder>(args.salesOrderGuid);
if (sourceOrder == null)
throw new InvalidOperationException("Source order not found");
if (order.Details.Count > 0)
throw new InvalidOperationException("Target order already has details");
order.SellToCustomer = sourceOrder.SellToCustomer;
order.Description = sourceOrder.Description;
foreach (var sourceLine in sourceOrder.Details)
{
_ = new SalesOrderDetail(context)
{
SalesOrder = order,
Product = sourceLine.Product,
Quantity = sourceLine.Quantity,
UnitPrice = sourceLine.UnitPrice,
Description = sourceLine.Description
};
}
context.SaveChanges();
});
}
}
API call:
POST /api/SalesOrder({orderId})/ReorderFrom?salesOrderGuid={sourceOrderId}
2. Calculating a line total with tax
A read method that computes a value without modifying data.
[ApiEntity]
public partial class SalesOrder : EntityBase
{
[Method("Calculate line total", MethodType.Read)]
public partial decimal CalculateLineTotal(decimal taxRate, int quantity, decimal discount = 0m);
}
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void CalculateLineTotalMethod()
{
salesOrder.Method(e => e.CalculateLineTotal).Do((order, args) =>
{
var subtotal = order.UnitPrice * args.quantity;
var discounted = subtotal - (subtotal * args.discount);
return discounted + (discounted * args.taxRate);
});
}
}
API call:
GET /api/SalesOrder({orderId})/CalculateLineTotal?taxRate=0.08&quantity=5&discount=0.1
3. Static method to query open orders
A static method does not operate on a specific entity instance — it works at the entity type level.
[ApiEntity]
public partial class SalesOrder : EntityBase
{
[Method("Get all open orders", MethodType.Read)]
public static partial IQueryable<SalesOrder> GetOpenOrders(string customerId);
}
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void GetOpenOrdersMethod()
{
salesOrder.Method(e => SalesOrder.GetOpenOrders).Do(args =>
{
return args.GetEntities<SalesOrder>()
.Where(o => o.SellToCustomer!.Id == args.customerId
&& o.Status == SalesOrderStatus.Open);
});
}
}
API call:
Normal OData parameters such as $select, $expand, and $filter can be used when a method returns a collection of entities
GET /api/SalesOrder/GetOpenOrders?customerId=CUST001?$select=Guid,DocNumber,Description,TotalPrice
4. Updating a sales order's status
A modify method that changes the order status and records timestamps.
[ApiEntity]
public partial class SalesOrder : EntityBase
{
[Method("Update status", MethodType.Modify)]
public partial void UpdateStatus(SalesOrderStatus newStatus);
}
//TODO: The following is no longer a good scenario because we don't set the status directly.
[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
[RegisterLogic]
public void UpdateStatusMethod()
{
salesOrder.Method(e => e.UpdateStatus).Do((order, args) =>
{
if (order.Status == args.newStatus) return;
order.Status = args.newStatus;
if (args.newStatus == SalesOrderStatus.Closed)
order.ClosedDate = DateTime.UtcNow;
args.GetService<IDataContext>().SaveChanges();
});
}
}
API call:
POST /api/SalesOrder({orderId})/UpdateStatus?newStatus=Closed
5. Parameterless method
Methods with no parameters are also supported.
[ApiEntity]
public partial class Product : EntityBase
{
[Method("Get default markup", MethodType.Read)]
public partial decimal GetDefaultMarkup();
}
[Logic]
public class ProductBL(Product.Logic product)
{
[RegisterLogic]
public void GetDefaultMarkupMethod()
{
product.Method(e => e.GetDefaultMarkup).Do((p, args) => 1.5m);
}
}
API call (note the parentheses for GET):
GET /api/Product({productId})/GetDefaultMarkup()
Note: The
()is required for GET requests (Read methods) to indicate a function call. POST requests (Modify methods) do not need parentheses.
OData API Routes
| Method Type | HTTP | Route |
|---|---|---|
| Instance Read | GET | /api/{Entity}({id})/MethodName() |
| Instance Modify | POST | /api/{Entity}({id})/MethodName |
| Static Read | GET | /api/{Entity}/MethodName() |
| Static Modify | POST | /api/{Entity}/MethodName |
GET: Parameters are passed as query string values
POST: Parameters are passed via a json body
Notes
- Method implementations have full access to
args.GetService<T>()andargs.GetEntities<T>(). - Read methods should not modify data — they return computed values.
- Methods are automatically exposed via the OData API with no additional configuration.
- The
argsobject provides strongly-typed access to each method parameter by name.