Your project needs to connect to an OData service? Just point our CLI at the endpoint, and everything gets generated for you—models, client, type-safe queries. No manual work. No boilerplate. Just instant OData integration.
dotnet tool install --global Forge.OData.CLINavigate to your project and run:
dotnet odata add --endpoint https://services.odata.org/V4/TripPinServiceRWdotnet buildThat's it! You now have a fully functional, type-safe OData client ready to use:
usingvarhttpClient=newHttpClient();varclient=newTripPinServiceRWClient(httpClient);// Query with LINQ - it just works!varpeople=awaitclient.People.Where(p =>p.FirstName.StartsWith("R")).OrderBy(p =>p.LastName).Take(10).ToListAsync();foreach(varpersoninpeople){Console.WriteLine($"{person.FirstName}{person.LastName}");}Stop writing boilerplate. Stop manually creating DTOs. Stop fighting with HTTP requests and JSON parsing.
You have an OData API. You want to use it. The OData CLI does the heavy lifting:
- ✅ Downloads the metadata from your OData service
- ✅ Generates all model classes with proper types and attributes
- ✅ Creates a type-safe client with IntelliSense support
- ✅ Translates LINQ to OData queries automatically
- ✅ Handles JSON serialization with optimized converters
All this happens during your normal dotnet build. No runtime reflection. No performance overhead.
Install the Forge OData CLI as a global tool:
dotnet tool install --global Forge.OData.CLIOr install it locally in your project for team consistency:
dotnet new tool-manifest # if you don't have one already
dotnet tool install --local Forge.OData.CLI# Simple: Just provide the endpoint
dotnet odata add --endpoint https://services.odata.org/V4/Northwind/Northwind.svcThe CLI will:
- Download the
$metadatafrom the endpoint - Generate a client class named
NorthwindClient(derived from URL) - Create all model classes for entities (Products, Orders, Customers, etc.)
- Configure your project automatically
# Give your client a meaningful name
dotnet odata add \
--endpoint https://api.example.com/odata \
--client-name CompanyDataServiceNow you'll have a CompanyDataService class instead of a generic name.
# Keep your OData clients organized
dotnet odata add \
--endpoint https://api.example.com/odata \
--client-name InventoryService \
--output-path Services/ODataThis creates:
- File:
Services/OData/InventoryService.cs - Namespace:
YourProject.Services.OData
# Control the namespace for better organization
dotnet odata add \
--endpoint https://api.example.com/odata \
--client-name ProductCatalog \
--namespace MyCompany.External.Services# Add multiple services - they all work together
dotnet odata add --endpoint https://api.products.com/odata --client-name ProductService
dotnet odata add --endpoint https://api.orders.com/odata --client-name OrderService
dotnet odata add --endpoint https://api.customers.com/odata --client-name CustomerServiceEach client is independent, and you can use them side by side in your application.
When the OData service changes (new entities, modified properties), just update:
dotnet odata updateThis command:
- Finds all OData clients in your project
- Re-downloads metadata from their endpoints
- Updates the metadata files
- Rebuild to regenerate clients with the latest schema
Workflow example:
# Initial setup
dotnet odata add --endpoint https://api.example.com/odata --client-name ApiClient
dotnet build
# ... time passes, API changes ...# Update to latest schema
dotnet odata update
dotnet build # Regenerates with new metadataLet's say you're building an app that needs to fetch data from the TripPin OData service:
# Step 1: Add the clientcd MyTravelApp
dotnet odata add \
--endpoint https://services.odata.org/V4/TripPinServiceRW \
--client-name TripPinService \
--output-path Services
# Step 2: Build
dotnet buildNow use it in your code:
usingMyTravelApp.Services;publicclassTravelService{privatereadonlyHttpClient_httpClient;publicTravelService(IHttpClientFactoryhttpClientFactory){_httpClient=httpClientFactory.CreateClient();}publicasyncTask<List<Person>>GetTravelersAsync(stringfirstNamePrefix){varclient=newTripPinService(_httpClient);// Type-safe LINQ queriesreturnawaitclient.People.Where(p =>p.FirstName.StartsWith(firstNamePrefix)).OrderBy(p =>p.LastName).ToListAsync();}publicasyncTask<Person>GetPersonWithTripsAsync(stringusername){varclient=newTripPinService(_httpClient);// Expand navigation propertiesvarpeople=awaitclient.People.Where(p =>p.UserName==username).Expand(p =>p.Trips).ToListAsync();returnpeople.FirstOrDefault();}}That's it. No manual DTOs. No string-based queries. Just clean, type-safe code with full IntelliSense.
When you run dotnet odata add, the tool generates:
publicclassProduct{[Key]publicintId{get;set;}publicstringName{get;set;}publicdecimalPrice{get;set;}publicboolInStock{get;set;}// ... all properties from metadata}Custom converters for each model that:
- Deserialize JSON without reflection (faster!)
- Handle nullable types correctly
- Support all OData types
publicpartialclassYourServiceClient{publicODataQueryable<Product>Products{get;}publicODataQueryable<Order>Orders{get;}publicODataQueryable<Customer>Customers{get;}// ... all entity sets from metadata}Write normal C# LINQ queries:
// This LINQ expression...varquery=client.Products.Where(p =>p.Price>10&&p.InStock).OrderBy(p =>p.Name).Skip(20).Take(10);// ...becomes this OData query automatically:// /Products?$filter=Price gt 10 and InStock eq true&$orderby=Name asc&$skip=20&$top=10| LINQ Expression | OData Query |
|---|---|
.Where(p => p.Price > 10) | $filter=Price gt 10 |
.Where(p => p.Name == "Test") | $filter=Name eq 'Test' |
.Where(p => p.InStock && p.Price < 100) | $filter=InStock eq true and Price lt 100 |
.OrderBy(p => p.Name) | $orderby=Name asc |
.OrderByDescending(p => p.Price) | $orderby=Price desc |
.Skip(10) | $skip=10 |
.Take(20) | $top=20 |
.Select(p => new { p.Name, p.Price }) | $select=Name,Price |
.Expand(o => o.Product) | $expand=Product |
String methods work too:
.Where(p => p.Name.StartsWith("A"))→startswith(Name, 'A').Where(p => p.Name.EndsWith("Z"))→endswith(Name, 'Z').Where(p => p.Name.Contains("mid"))→contains(Name, 'mid')
Need more control? The tool supports advanced scenarios:
After generating the initial client, you can customize it:
usingForge.OData.Attributes;namespaceMyApp.Services{[ODataClient(MetadataFile="ApiMetadata.xml",Endpoint="https://api.example.com/odata")]publicpartialclassApiClient{// Add your custom methodspublicasyncTask<Product?>GetFeaturedProductAsync(){varresults=awaitProducts.Where(p =>p.Featured).OrderByDescending(p =>p.Rating).Take(1).ToListAsync();returnresults.FirstOrDefault();}// Add custom propertiespublicstringServiceVersion=>"v2.0";}}// DevelopmentvardevClient=newApiClient(httpClient,"https://dev-api.example.com/odata");// ProductionvarprodClient=newApiClient(httpClient,"https://api.example.com/odata");- Technical documentation: See CONTRIBUTE.md for detailed architecture, project structure, and contribution guidelines
- Examples: Check the
sample/directory for working examples - Changelog: See CHANGELOG.md for version history
We welcome contributions! Please see CONTRIBUTE.md for detailed information on:
- Project architecture and structure
- Development setup
- Building and testing
- Code generation workflow
- Contribution guidelines
MIT