Allows you to serialize type-safe lambda expressions (in various languages) to OData $filter-style strings. This
enables cross-platform filtering of objects.
The possibilities are endless, but for me, this is most useful for exposing pub/sub notification subscriptions for a RESTful API. From any (supported) language, I can type a lambda like this:
await SubscribeAsync<PersonAdded>(
// Subscription filter: only receive notifications for adults whose names begin with "A"
p => p.Age > 18 && p.Name.StartsWith("A"),
// Callback
p => Console.WriteLine($"{p.Name} was added."));Inside SubscribeAsync, the first argument, an Expression that accepts a type T object and returns a bool, is
automatically serialized to a string for easy passing to a RESTful API. The above example
(p => p.Age > 18 && p.Name.StartsWith("A")) becomes the string "Age gt 18 and startswith(Name, 'A')". This can be
easily deserialized and compiled back into the original lambda server-side, and used for filtering notification objects
of type PersonAdded. Those server-filtered notifications can then be sent to the client, where the callback can be
invoked.
Here's a simplified C# client-side example:
public class MyClass(Notifications notifications)
{
private List<Guid> _subscriptions = [];
public Task StartAsync()
{
_subscriptions.Add(await notifications.SubscribeAsync<PersonAdded>(p => p.Age >= 18, AdultAdded));
_subscriptions.Add(await notifications.SubscribeAsync<PersonAdded>(p => p.Age < 18, ChildAdded));
_subscriptions.Add(await notifications.SubscribeAsync<PersonRemoved>(p => true, PersonRemoved));
}
public Task StopAsync()
{
foreach(Guid subscriptionId in _subscriptions)
{
await notifications.UnsubscribeAsync(subscriptionId);
}
}
public void ChildAdded(PersonAdded child) { /* ... */ }
public void AdultAdded(PersonAdded adult) { /* ... */ }
public void PersonRemoved(PersonRemoved person) { /* ... */ }
}An example implementation of Notifications is found in the
client-side notification class examples section below.
And a similar TypeScript example:
import { Subscription } from 'rxjs';
import { serializeExpression } from 'ts-lambda-to-odata';
export class MyClass {
private _subscriptions: Subscription[];
constructor(private notifications: Notifications) {
_subscriptions.add(notifications.subscribe<PersonAdded>(
// Note - we call `serializeExpression` here rather than passing the expression into this function.
// In future, `ts-lambda-to-odata` may handle receiving a func it can then parse, but currently it can't.
'PersonAdded', serializeExpression<PersonAdded>(p => p.Age >= 18)
).subscribe({
next: (adult) => this.adultAdded(adult)
}));
_subscriptions.add(notifications.subscribe<PersonAdded>(
'PersonAdded', serializeExpression<PersonAdded>(p => p.Age < 18)
).subscribe({
next: (child) => this.childAdded(child)
}));
_subscriptions.add(notifications.subscribe<PersonRemoved>(
'PersonRemoved', serializeExpression<PersonRemoved>(p => true)
).subscribe({
next: (person) => this.personRemoved(person)
}));
}
// Callbacks
private adultAdded(adult: PersonAdded): void { console.log('Adult added:', adult); }
private childAdded(child: PersonAdded): void { console.log('Child added:', child); }
private personRemoved(person: PersonRemoved): void { console.log('Person removed:', person); }
public dispose(): void {
this._subscriptions.forEach(s => s.unsubscribe());
}
}An example implementation of Notifications is found in the
client-side notification class examples section below.
See /src/typescript/sample-app for an example of how serializeExpression can be used practically, and
the ts-lambda-to-odata README for information on how to integrate the
library.
dotnet buildnpm install
npm run buildNone.
npm run startdotnet testnpm testThe below examples are very basic and don't include error-handling logic.
Assume that MyServerApi exposes a SubscribeAsync method that returns a Guid ID, and successful subscriptions will
cause those notifications to begin appearing in the MyServerWebsocket's OnNotification callback.
public class Notifications
{
private Dictionary<Guid, Action<object>> _callbacks = [];
private MyServerApi _server;
public Notifications(MyServerApi server, MyServerWebsocket websocket)
{
_server = server;
websocket.OnNotification += (notification) => {
OnNotificationReceived(notification.SubscriptionId, notification.Data);
};
}
public async Task<Guid> SubscribeAsync<T>(Func<T, bool> expression, Action<T> callback)
{
// Get type name
string typeName = typeof(T).Name;
// Convert lambda to OData string
string serializedExpression = ExpressionSerializer.Serialize<T>(expression);
// Subscribe to notifications on the server
Guid subscriptionId = await _server.SubscribeAsync(typeName, serializedExpression);
// Store the callback
_callbacks.Add(subscriptionId, (notification) => callback((T)notification));
// Return the subscription ID so that we can use it to unsubscribe
return subscriptionId;
}
public void OnNotificationReceived(Guid subscriptionId, object notification)
{
// Get the callback
if (_callbacks.TryGetValue(subscriptionId, out Action<object> callback))
{
// Invoke the callback with the notification object
callback(notification);
}
}
}Note that because serializeExpression only works at the site of the original call, this class doesn't handle
serializing the lambda - the caller does.
import { Observer, Observable } from 'rxjs';
export class Notifications {
private _callbacks: { [subscriptionId: string]: Observer } = {};
constructor(
private server: MyServerApi,
private websocket: MyWebsocket) {
websocket.on('notification', (notification) =>
this.onNotification(notification.subscriptionId, notification.data)
);
}
public subscribe<T>(typeName: string, serializedExpression: string): Observable<T> {
return new Observable<T>(observer => {
const serverSubscription = server
.subscribe(typeName, serializedExpression)
.subscribe({
next: (subscriptionId: string) => {
this._callbacks[subscriptionId] = observer;
// Setup the teardown logic when unsubscribe is called
observer.add(() => {
this.server.unsubscribe(subscriptionId);
delete this._callbacks[subscriptionId];
});
},
error: (err) => observer.error(err)
}
);
// Add serverSubscription to the observer's teardown logic
observer.add(() => serverSubscription.unsubscribe());
});
}
private onNotification(subscriptionId: string, data: any): void {
_callbacks[subscriptionId]?.next(data);
}
}