OData Migration extension usage and architecture
Applies To: # OData webapi v7 for .net core OData AspNetCore WebApi V7
The OData Migration library provides ASP.NET Core 2.2+ OData V4 services with the capability of handling and responding to OData V3 requests. An OData V4 service that uses the OData Migration Library may appear to an OData V3 client as the equivalent V3 service. This may be useful to you if you have migrated your service from OData V3 to OData V4, but wish to bridge the gap between your newly migrated V4 service and your OData V3 clients.
The OData Migration library applies to:
- OData V4 services that wish to support V3 clients
- ASP.NET Core 2.2+ OData V4 services
- JSON formatted requests and responses
OData Feature | Tested | Example |
---|---|---|
OData XML/Atom format | Not supported | See Atom and XML formats |
Instance annotations beyond basic types in request/response payloads | Not tested or supported | Example of entity reference link via annotation: "@odata.id" : "serviceRoot/People('vincentcalabrese')" |
Navigation Links | Not tested | http://localhost/v3/Customer(1)/$links/Orders |
Nested/mixed action return payloads | Not tested | { mixed_payload: [{ "name": "entity", "id": 3}, 4, 5, [{"name: "other_entity", "price": 9}]] } |
Automatic conversion between Edm.Time and Edm.Duration/Edm.TimeOfDay | Not tested | - |
JSON Verbose Format | Not supported | - |
In addition, all features that are new in OData V4 but not present in OData V3 are not supported by this extension.
There are two steps to configure your service to use OData Migration.
Adding OData Migration to your service configuration simply one properly placed line of code.
public static void ConfigureServices(IServiceCollection services)
{
// your code here
// AddOData must be called before AddODataMigration
services.AddOData();
services.AddODataMigration();
// your code here
}
AddODataMigration takes no arguments, and adds the following to your service collection:
- OData Migration filters
- OData Migration input formatter
- OData Migration output formatter
The OData Migration filters are ASP.NET Core filters that are used to:
- Add version and content ID headers to batched requests
- Catch exceptions in requests within batch requests and send them back as internal server errors with content ID headers attached
The OData Migration input and output formatters observe the incoming request to see if it contains either of the specific OData version headers.
These headers are dataserviceversion: 3.0
and maxdataserviceversion: 3.0
If either of these headers are present in the request, the input formatter
will deserialize the request as a OData v3 request, and the output formatter will return JSON that is OData v3 compliant. Both of these formatters
make use of the OData Edmx contract to validate requests and responses just as they would be validated in v4.
Configuring your application to use OData Migration in its pipeline requires two parameters:
- A string representation of the OData V3 model you would like to support
- The IEdmModel representation of your OData V4 model.
The string representation is used for model validation during translation and is also returned when a request asks for the metadata of your service. The V4 IEdmModel is used for model validation during translation.
Also, the V3 and V4 edmx contracts, while different in format, must match in meaning, or else model validation will fail when checking between models.
Supplying these two parameters to the UseODataMigration extension method is all you need to do to configure your application. The code below shows how you might obtain the string representation of your V3 model, and where to call UseODataMigration:
public static void Configure(IApplicationBuilder builder)
{
// your code here
// If using batching, you must call UseODataBatching before UseODataMigration
builder.UseODataBatching();
IEdmModel v4model = /*your model*/;
// If you are working with a Data.Edm.IEdmModel:
Data.Edm.IEdmModel v3model = /*your model*/;
builder.UseODataMigration(v3model, v4model);
// If you have your OData V3 model representation as an EDMX string (e.g., by querying your V3 service for metadata)
// string v3Edmx = /*your EDMX string*/
// builder.UseODataMigration(v3Edmx, v4model);
// your code here
}
Calling UseODataMigration inserts the middleware responsible for translating incoming request URLs. For example, an OData version 3 request URL might look like:
https://localhost/v3/Product(guid'02951787-4c1a-4dff-a917-a04b21b40ad3')
whereas the equivalent OData version 4 request URL looks like:
https://localhost/v4/Product(02951787-4c1a-4dff-a917-a04b21b40ad3)
OData Migration extension's middleware will take care of this conversion for you automatically, provided that you are using JSON and have either the
dataserviceversion: 3.0
or maxdataserviceversion: 3.0
header in your request headers.
Be aware that in its current state, using this extension will cause your v3 metadata to always be returned in response to any metadata requests.
This project uses the main branch for development.
The main point of entry for usage is MigrationExtension.cs
. This exposes public methods for services who want to use this extension.
The architecture of this extension can be thought of as three separate features:
- URL Translation
- Request Body Translation
- Response Body Translation
URL translation in the code is split into two parts: path translation and query translation. The entry point for URL translation is ODataMigrationMiddleware.cs
which
formats and checks the request headers to see if the incoming request is an OData V3 request. If so, it translates the path and query of the HttpContext.
The path of the request URL is translated by:
- Parsing the request path into OData V3 segments (such as a property segment, entity segment, or key segment)
- Utilizing the UriSegmentTranslator (
Middleware/UriSegmentTranslator.cs
) to convert each segment to an equivalent OData V4 segment - Building the V4 path
Translating each segment from V3 to V4 is a process of obtaining the relevant inner pieces of data of the V3 segment, looking up their equivalents in the V4 model, and constructing a new V4 segment with matching inner data.
There are only a few key differences between OData V3 and OData V4 when it comes to queries. Here is the list of OData query options and their differences:
Query option | Description | Syntactic difference between V3 and V4 |
---|---|---|
$expand | Expands related entities inline | None |
$filter | Filters the results, based on a Boolean condition | The same differences between URLs, for example the presence of keywords like "guid" and "datetime" |
$inlinecount | Tells the server to include the total count of matching entities in the response. | Changed in V4 to $count, either true or false (whereas $inlinecount is allpages or none) |
$orderby | Sorts the results | None |
$select | Selects which properties to include in the response | None |
$skip | Skips the first n results | None |
$top | Returns only the first n results | None |
The OData Migration extension translates a V3 query by looking for either filter or inlinecount options and translating them. The $filter option is translated by:
- Parsing the filter expression in OData V3
- Translating the OData v3 representation of the filter expression into an OData v4 representation of the filter expression
- Inserting the OData v4 representation back into the URL.
The $inlinecount option is translated by changing "allpages" to "true" and "none" to "false".
Request body translation is done by overridding the OData V4 ASP.NET Core InputFormatter. InputFormatters in ASP.NET Core are responsible for deserializing incoming request bodies.
In OData V4, the ODataInputFormatter uses a set of deserializers that are each responsible for a certain kind of request body payload. For example, there is an ODataResourceDeserializer,
which is responsible for deserializing incoming entities and complex types. These deserializers are dispatched by the ODataDeserializerProvider; in OData V4 the implementation for the
default provider is found in the DefaultODataDeserializerProvider.cs
class.
The main difference between the ODataMigrationInputFormatter and ODataInputFormatter is that the ODataMigrationInputFormatter uses a customized provider found in
ODataMigrationDeserializerProvider.cs
. This is because OData Migration handles incoming OData V3 request bodies by dispatching customized deserializers.
Each of these custom deserializers overrides the default OData V4 deserializer in order to insert logic that modifies the incoming request body before
it is deserialized in OData V4.
One example of an incoming OData V3 request that would pose a problem to a V4 deserializer is:
{
"MyName": "John",
"MyLong": "1000000000"
}
In OData V3, long
types in request bodies are surrounded by quotes. However, an OData V4 deserializer reading this request body will throw an exception
at model validation, because the property MyLong
is supposed to be a long
type but a string
is found instead.
In the ODataMigrationInputFormatter, the ODataMigrationResourceDeserializer would be dispatched by the ODataMigrationDeserializerProvider, and would therefore be able to modify the request body to be V4 compatible before passing it to the ODataResourceDeserializer.
This extension overrides the resource, collection (an array of non-entity/non-complex values), and action payload deserializers. Some of the OData v4 deserializers did not require modification to be V3 compatible, while others, such as the ODataResourceSetDeserializer, end up depending on the customized ODataMigrationResourceDeserializer.
The following table explains why each deserializer was or was not overridden:
OData V4 ASP.NET Core Deserializer | Overridden? | Reason |
---|---|---|
ODataActionPayloadDeserializer | Yes | Action parameters can consist of V4 types that are represented differently in V3 |
ODataCollectionDeserializer | Yes | Collections are arrays of simple values, and these simple values could be V4 types that are represented differently in V3 |
ODataEntityReferenceLinkDeserializer | No | Entity reference link translation is untested/unsupported (i.e., not yet) |
ODataEnumDeserializer | No | No need |
ODataPrimitiveDeserializer | No | In deserialization process, OData libraries will convert by casting primitives to their types. For example, the quoted long would be casted to a long |
ODataResourceDeserializer | Yes | Resources can contain V4 types that are represented differently in V3 |
ODataResourceSetDeserializer | No | ODataResourceSetDeserializer relies on ODataResourceDeserializer, which has been overridden |
The following picture shows the flow from the formatter to the service through the deserializers.
Details on each of the custom deserializers can be found in the classes under Formatters/Deserialization
.
Response body translation is done in a similar way to request body translation by overriding the OData V4 ASP.NET Core OutputFormatter. OutputFormatters in ASP.NET Core are responsible for serializing response payloads to the response content type (in this case, JSON). There is a similar architecture where the DefaultODataSerializerProvider is responsible for dispatching the appropriate ODataSerializers to serialize the outgoing data.
In OData Migration, the ODataMigrationOutputFormatter overrides the ODataOutputFormatter in order to use a customized ODataMigrationSerializerProvider. This customized provider dispatches customized ODataSerializers to translate the outgoing response types to match OData V3 conventions. Not all of the ODataSerializers are overridden; only the ODataResourceSerializer, ODataResourceSetSerializer, ODataCollectionSerializer, ODataPrimitiveSerializer have been replaced by their OData Migration counterparts.
In a similar example to what we examined in the request body translation section, suppose we have this response body:
{
"MyName": "John",
"MyLong": 1000000000
}
The custom OData Migration serializers would add quotes to the long
value in this case, because an OData V3 client would be expecting a quoted value
if it knew the entity type was Edm.Int64.
The following table explains why each serializer was or was not overridden:
OData V4 ASP.NET Core Serializer | Overridden? | Reason |
---|---|---|
ODataCollectionDeserializer | Yes | |
ODataDeltaFeedSerializer | No | Delta feeds are written inline to the response stream so the translation happens in the Resource or ResourceSet serializers |
ODataEntityReferenceLinkSerializer | No | Entity reference link translation is untested/unsupported (i.e., not yet) |
ODataEntityReferenceLinksSerializer | No | See above |
ODataEnumSerializer | No | No need |
ODataErrorSerializer | No | No need |
ODataMetadataSerializer | No | No need |
ODataPrimitiveSerializer | Yes | Unlike in the deserialization process, OData libraries will write V4 types like long as long, however V3 clients expect, in this example, strings, therefore a translation process must occur |
ODataRawValueSerializer | No | No need |
ODataResourceSerializer | Yes | Resources can contain V4 types that are represented differently in V3 |
ODataResourceSetSerializer | Yes | Unlike in the deserialization process, because the response stream sends when closed, the method for writing a resource (WriteObject) and writing a resource inline (WriteObjectInline) are separate in serialization. Therefore, the ResourceSetSerializer doesn't make use of the ResourceSerializer in the same way, and must be treated differently. |
ODataServiceDocumentSerializer | No | No need |
Details on each of the custom serializers can be found in the classes under Formatters/Serialization
.
The Microsoft.Extensions.OData.Migration.Tests project, found by opening the ODataMigration.sln in VS 2017+, contains unit tests for URL translation and E2E tests for request/response body translation. All tests should be pass at the time of this writing. Note that the E2E tests only test the request/response body translation and the URL translation is not being tested at the same time.
One feature that would be nice is the automatic conversion of an OData V4 model to an OData V3 model. Currently no such generalized process exists that we know of, and having this process would allow the usage of this package to be even more seamless, because you would only need to provide the V4 IEdmModel of the service.