3

Working With OData in ASP.NET Core | Pluralsight

 3 years ago
source link: https://www.pluralsight.com/blog/software-development/odata-asp-net-core
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction to OData

According to the people who designed it, OData (the open data protocol), is "the best way to REST".

REST, as you may know, is an architectural style.  It provides us with a set of constraints and sub constraints, like the fact that client-server interaction should be stateless, the fact that each message must include enough information that describes how to process the message, and so on. 

But REST is not a standard.  Next to that it also leaves quite a few things open for us, developers, to decide upon.  Take resource naming guidelines, for example.  You've undoubtedly seen resources named like "api/employee/1" (singular noun approach), "api/employees/1" (plural noun approach), "api/meaninglessname/1" or "api/nomeaning/123/evenlessmeaning/234" - all of these examples can be ways to refer the exact same employee resource. 

While being compliant to REST, some of these are not exactly developer-friendly, especially when the project you're working on consists of a mix of all these different resource naming styles.  This is just a simple example.  Once more exotic constraints like HATEOAS need to be implemented in your RESTful system, deciding on how to define the contracts for those leaves even more open for interpretation. 

That's where OData comes in.  OData is, essentially, a way to try and standardize REST.  It's an open protocol that allows the creating and consumption of queryable and interoperable RESTful APIs in a simple and standard way.  It describes things like which HTTP method to use for which type of request, which status codes to return when, but also: URL conventions.

And it also includes information on querying data - filtering and paging, but also: calling custom functions and actions, working with batch requests, and more. 

The current version is v4, which consists of 2 main parts.

Part 1 is the protocol itself, which contains descriptions of the headers to use, the potential response codes, definitions on what an entity is, a singleton, derived, expanded or projected entities and so on, including how a request & response should look. 

Part 2 is all about those URL conventions.

A part of this article will deal with the ability to easily query data, one of the main drivers for companies to adopt OData-implementing frameworks.  To enable that it's important to agree upon what a URL to query data should look like.  That's what's described in that part of the standard.

Prerequisites - Adding the Correct NuGet Packages

The first thing you'll want to do, after starting a new API project, is adding the necessary OData NuGet package.  The one you want is Microsoft.AspNetCore.OData. This is supported for .NET Core and for .NET 5.  Version 7.x of the package is for .NET Core, version 8.x is for .NET 5.  At the moment of writing, the .NET 5 version is still in preview mode, but chances are that by the time you're reading this it will be out of preview.  While preparing for this article and a refresh to my OData course I worked with both the 7.x and 8.x packages and the differences are absolutely minimal.  For this article I'll use the .NET 5 version, but what you'll read here applies to both unless mentioned otherwise.   

NuGet Package Manager ASP.NET Core OData

After adding the package, the Entity Data Model should be defined.

Defining an Entity Data Model

The Entity Data Model, or EDM, is the abstract data model that is used to describe the data exposed by an OData service.   You could consider this the "heart" of your OData services.

If you've worked with Entity Framework Core, this concept will sound familiar - the EDM is not at all exclusive to OData, EF Core works on something likewise.  It was introduced as a set of concepts that describe the structure of data, regardless of its stored form.  The EDM makes the stored form of data irrelevant to application design and development. And, because entities and relationships describe the structure of data as it is used in an application (not its stored form), they can evolve as an application evolves.

You can see an example of that here.  This is an EDM that exposes a set of people and a set of vinyl records.

public class AirVinylEntityDataModel
{
 
    	public IEdmModel GetEntityDataModel()
    	{
        	var builder = new ODataConventionModelBuilder();
        		builder.Namespace = "AirVinyl";
        		builder.ContainerName = "AirVinylContainer";
 
            	builder.EntitySet<Person>("People");
            	builder.EntitySet<VinylRecord>("VinylRecords");
 
        		return builder.GetEdmModel();
    	}
}

Once the EDM has been defined, we need to register it.  To do so, we should call into AddOData on the services collection in the ConfigureServices method on our Startup class.  This registers OData services on the IoC container, and it accepts a delegate to configure ODataOptions.  One of the things we can do through that is register the EDM model by calling into AddModel and passing through a model prefix (which will shine through in the route) and a model instance.

 public void ConfigureServices(IServiceCollection services)
{
  	// … other code …
 
   	services.AddOData(opt => opt.AddModel(
		"odata", 
		new AirVinylEntityDataModel().GetEntityDataModel()));
}

This is the only place in this article where I have to point out a change between v8.x and v7.x.  In v7.x, setting this up was still split up across 2 pieces of code.  You'd first register the necessary services on the container via a call into AddOData.

public void ConfigureServices(IServiceCollection services)
{
   	// … other code …
 
   	services.AddOData();
}

In the Configure method you'd additionally add a call into MapODataRoute to map routes to OData controllers, and that's where the EDM is injected.

app.UseEndpoints(endpoints =>
     {
            	endpoints.MapControllers();
          	
                endpoints.MapODataRoute(
                              "AirVinyl OData", 
                              "odata", 
                               new AirVinylEntityDataModel().GetEntityDataModel());
       });

Regardless of the version you're using, you should now be able to run your application and surf to the application root, followed by /odata.  That "odata" prefix is coming from when we registered the Odata model. 

GET https://localhost:44376/odata/

This gives you the service document. 

{
    "@odata.context": "https://localhost:44376/odata/$metadata",
    "value": [
        {
            "name": "People",
            "kind": "EntitySet",
            "url": "People"
        },
        {
            "name": "VinylRecords",
            "kind": "EntitySet",
            "url": "VinylRecords"
        }
    ]
}

Add $metadata to the URL, and you get the metadata document. 

<?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="AirVinyl" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Person">
                <Key>
                    <PropertyRef Name="PersonId" />
                </Key>
                <Property Name="PersonId" Type="Edm.Int32" Nullable="false" />
                <Property Name="Email" Type="Edm.String" />
                <Property Name="FirstName" Type="Edm.String" Nullable="false" />
                <Property Name="LastName" Type="Edm.String" Nullable="false" />
                <Property Name="DateOfBirth" Type="Edm.DateTimeOffset" Nullable="false" />
                <Property Name="Gender" Type="AirVinyl.Gender" Nullable="false" />
                <Property Name="NumberOfRecordsOnWishList" Type="Edm.Int32" Nullable="false" />
                <Property Name="AmountOfCashToSpend" Type="Edm.Decimal" Nullable="false" />
                <NavigationProperty Name="VinylRecords" Type="Collection(AirVinyl.VinylRecord)" />
            </EntityType>
            <EntityType Name="VinylRecord">
                <Key>
                    <PropertyRef Name="VinylRecordId" />
                </Key>
                <Property Name="VinylRecordId" Type="Edm.Int32" Nullable="false" />
                <Property Name="Title" Type="Edm.String" Nullable="false" />
                <Property Name="Artist" Type="Edm.String" Nullable="false" />
                <Property Name="CatalogNumber" Type="Edm.String" />
                <Property Name="Year" Type="Edm.Int32" />
                <Property Name="PressingDetailId" Type="Edm.Int32" />
                <Property Name="PersonId" Type="Edm.Int32" />
                <NavigationProperty Name="PressingDetail" Type="AirVinyl.PressingDetail">
                    <ReferentialConstraint Property="PressingDetailId" ReferencedProperty="PressingDetailId" />
                </NavigationProperty>
                <NavigationProperty Name="Person" Type="AirVinyl.Person">
                    <ReferentialConstraint Property="PersonId" ReferencedProperty="PersonId" />
                </NavigationProperty>
            </EntityType>
            <EntityType Name="PressingDetail">
                <Key>
                    <PropertyRef Name="PressingDetailId" />
                </Key>
                <Property Name="PressingDetailId" Type="Edm.Int32" Nullable="false" />
                <Property Name="Grams" Type="Edm.Int32" Nullable="false" />
                <Property Name="Inches" Type="Edm.Int32" Nullable="false" />
                <Property Name="Description" Type="Edm.String" Nullable="false" />
            </EntityType>
            <EnumType Name="Gender">
                <Member Name="Female" Value="0" />
                <Member Name="Male" Value="1" />
                <Member Name="Other" Value="2" />
            </EnumType>
            <EntityContainer Name="AirVinylContainer">
                <EntitySet Name="People" EntityType="AirVinyl.Person">
                    <NavigationPropertyBinding Path="VinylRecords" Target="VinylRecords" />
                </EntitySet>
                <EntitySet Name="VinylRecords" EntityType="AirVinyl.VinylRecord">
                    <NavigationPropertyBinding Path="Person" Target="People" />
                </EntitySet>
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

This metadata document is important.  As you can see, this is a representation of the data model that describes the data and operations exposed by our still-to-be-implemented OData service.  We can find the People and VinylRecords & related entity types described here, amongst other things.  It's documents like these that allow for easy integration & data generation scenarios: client applications can read out this document and, from interpreting it, learn how to interact with this OData service.  Generating DTOs or even full clients for interaction with this service are possible thanks to this.  Stepping back to REST, of which OData is an implementation, this document helps with conforming to the HATEOAS subconstraint.  


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK