doc/WebSite/OData-AspNetCore-Integration.md
OData is defined as "An open protocol to allow the creation and consumption of queryable and interoperable RESTful APIs in a simple and standard way" (odata.org). You can use OData with ASP.NET Boilerplate. The Abp.AspNetCore.OData NuGet package simplifies its usage.
We must first install the Abp.AspNetCore.OData NuGet package to our Web.Core
project:
Install-Package Abp.AspNetCore.OData
We need to set a dependency on AbpAspNetCoreODataModule for our module.
Example:
[DependsOn(typeof(AbpAspNetCoreODataModule))]
public class MyProjectWebCoreModule : AbpModule
{
...
}
See the module system to understand module dependencies.
OData requires us to declare entities which can be used as OData resources. We must do this in the Startup class:
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.AddOData();
// Workaround: https://github.com/OData/WebApi/issues/1177
services.AddMvcCore(options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
});
return services.AddAbp<MyProjectWebHostModule>(...);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAbp();
...
app.UseOData(builder =>
{
builder.EntitySet<Person>("Persons").EntityType.Expand().Filter().OrderBy().Page();
});
// Return IQueryable from controllers
app.UseUnitOfWork(options =>
{
options.Filter = httpContext =>
{
return httpContext.Request.Path.Value.StartsWith("/odata");
};
});
app.UseMvc(routes =>
{
routes.MapODataServiceRoute(app);
...
});
}
}
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.AddOData();
// Workaround: https://github.com/OData/WebApi/issues/1177
services.AddMvcCore(options =>
{
foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
}
});
return services.AddAbp<MyProjectWebHostModule>(...);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAbp();
// Return IQueryable from controllers
app.UseUnitOfWork(options =>
{
options.Filter = httpContext => httpContext.Request.Path.Value.StartsWith("/odata");
});
...
app.UseODataBatching();
app.UseEndpoints(endpoints =>
{
...
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Person>("Persons").EntityType.Expand().Filter().OrderBy().Page();
endpoints.MapODataRoute("odataPrefix", "odata", builder.GetEdmModel());
...
});
...
}
}
public class Startup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(/*...*/)
.AddOData(opts =>
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Person>("Persons").EntityType.Expand().Filter().OrderBy().Page().Select();
opts.AddRouteComponents("odata", builder.GetEdmModel());
}
);
return services.AddAbp<MyProjectWebHostModule>(...);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAbp();
// Return IQueryable from controllers
app.UseUnitOfWork(options =>
{
options.Filter = httpContext => httpContext.Request.Path.Value.StartsWith("/odata");
});
...
}
}
Here, we got the ODataModelBuilder reference and set the Person entity. You can use EntitySet to add other entities in a similar way. See the OData documentation for more information on the builder.
The Abp.AspNetCore.OData NuGet package includes the AbpODataEntityController
base class (which extends standard ODataController) to create your
controllers easier. An example to create an OData endpoint for the Person
entity:
public class PersonsController : AbpODataEntityController<Person>, ITransientDependency
{
public PersonsController(IRepository<Person> repository)
: base(repository)
{
}
}
It's that easy! All the methods of AbpODataEntityController are virtual.
This means that you can override the Get, Post, Put, Patch,
Delete and other actions and add your own logic.
The Abp.AspNetCore.OData NuGet package also inclıudes AbpODataDtoController. This controller can be used for using DTOs in your OData controller. An example to create an OData endpoint for the Person
entity based on AbpODataDtoController can be defined like this:
public class PersonsController : AbpODataDtoController<Person, PersonDto, PersonCreateInput>, ITransientDependency
{
public PersonsController(IRepository<Person> repository, IObjectMapper objectMapper)
: base(repository, objectMapper)
{
}
}
Abp.AspNetCore.OData calls the
IRouteBuilder.MapODataServiceRoute method with the conventional
configuration. If you need to, you can set
Configuration.Modules.AbpAspNetCoreOData().MapAction to map OData routes
yourself.
Abp.AspNetCore.OData implements AbpODataDontWrapResultFilter to disable result wrapping for paths that start with "/odata".
Add it to WrapResultFilters in the PreInitialize method of your module:
Configuration.Modules.AbpWebCommon().WrapResultFilters.Add(new AbpODataDontWrapResultFilter());
Here are some requests made to the controller defined above. Assume that the application works on http://localhost:21021. We will show some basic examples. Since OData is a standard protocol, you can easily find more advanced examples on the web.
Getting all people.
GET http://localhost:21021/odata/Persons
{
"@odata.context": "http://localhost:21021/odata/$metadata#Persons",
"value": [
{
"Name": "Douglas Adams",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 1
},
{
"Name": "John Nash",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 2
}
]
}
Getting the person with Id = 2.
GET http://localhost:21021/odata/Persons(2)
{
"@odata.context": "http://localhost:21021/odata/$metadata#Persons/$entity",
"Name": "John Nash",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 2
}
Getting the person with Id = 1 including his/her phone numbers.
GET http://localhost:21021/odata/Persons(1)?$expand=Phones
{
"@odata.context": "http://localhost:21021/odata/$metadata#Persons/$entity",
"Name": "Douglas Adams",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 1,
"Phones": [
{
"PersonId": 1,
"Type": "Mobile",
"Number": "4242424242",
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 1
},
{
"PersonId": 1,
"Type": "Mobile",
"Number": "2424242424",
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 2
}
]
}
Here's a more advanced query that includes filtering, sorting and getting the top 2 results.
GET http://localhost:21021/odata/Persons?$filter=Name eq 'Douglas Adams'&$orderby=CreationTime&$top=2
{
"@odata.context": "http://localhost:21021/odata/$metadata#Persons",
"value": [
{
"Name": "Douglas Adams",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2015-11-07T20:12:39.363+03:00",
"CreatorUserId": null,
"Id": 1
}
]
}
OData supports paging, sorting, filtering, projections and much more. See its own documentation for more information.
In this example, we're creating a new person.
POST http://localhost:21021/odata/Persons
{
Name: "Galileo Galilei"
}
Here, the "Content-Type" header is "application/json".
{
"@odata.context": "http://localhost:21021/odata/$metadata#Persons/$entity",
"Name": "Galileo Galilei",
"IsDeleted": false,
"DeleterUserId": null,
"DeletionTime": null,
"LastModificationTime": null,
"LastModifierUserId": null,
"CreationTime": "2016-01-12T20:36:04.1628263+02:00",
"CreatorUserId": null,
"Id": 4
}
If we get the list again, we can see the new person. We can also update or delete an existing entity since OData supports it.
We can get the metadata of entities, as shown in this example.
GET http://localhost:21021/odata/$metadata
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="AbpODataDemo.People" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Person">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="IsDeleted" Type="Edm.Boolean" Nullable="false" />
<Property Name="DeleterUserId" Type="Edm.Int64" />
<Property Name="DeletionTime" Type="Edm.DateTimeOffset" />
<Property Name="LastModificationTime" Type="Edm.DateTimeOffset" />
<Property Name="LastModifierUserId" Type="Edm.Int64" />
<Property Name="CreationTime" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="CreatorUserId" Type="Edm.Int64" />
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<NavigationProperty Name="Phones" Type="Collection(AbpODataDemo.People.Phone)" />
</EntityType>
<EntityType Name="Phone">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="PersonId" Type="Edm.Int32" />
<Property Name="Type" Type="AbpODataDemo.People.PhoneType" Nullable="false" />
<Property Name="Number" Type="Edm.String" Nullable="false" />
<Property Name="CreationTime" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="CreatorUserId" Type="Edm.Int64" />
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<NavigationProperty Name="Person" Type="AbpODataDemo.People.Person">
<ReferentialConstraint Property="PersonId" ReferencedProperty="Id" />
</NavigationProperty>
</EntityType>
<EnumType Name="PhoneType">
<Member Name="Unknown" Value="0" />
<Member Name="Mobile" Value="1" />
<Member Name="Home" Value="2" />
<Member Name="Office" Value="3" />
</EnumType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Persons" EntityType="AbpODataDemo.People.Person" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Metadata is used to investigate the service.
Note: If you want to use ODataQueryOptions in the controller methods, you need to ignore validation for ODataQueryOptions and ODataQueryOptions<> as shown below;
Configuration.Validation.IgnoredTypes.AddIfNotContains(typeof(ODataQueryOptions));
Configuration.Validation.IgnoredTypes.AddIfNotContains(typeof(ODataQueryOptions<>));
You can get the source code of the sample project here: https://github.com/aspnetboilerplate/sample-odata