To build an Edm model using non-convention model builder is to create an IEdmModel object by directly call fluent APIs of ODataModelBuilder. The developer should take all responsibility to add all Edm types, operations, associations, etc into the data model one by one. Let’s see how to build the Ccustomer-Order* business model by ODataModelBuilder.

CLR Models

Non-convention model builder is based on CLR classes to build the Edm Model. The Customer-Order business CLR classes are present in abstract section.

Enum Type

The following codes are used to add an Enum type Color:

var color = builder.EnumType<Color>();
color.Member(Color.Red);
color.Member(Color.Blue);
color.Member(Color.Green);

It will generate the below metadata document:

<EnumType Name="Color">
   <Member Name="Red" Value="0" />
   <Member Name="Blue" Value="1" />
   <Member Name="Green" Value="2" />
</EnumType>

Complex Type

Basic Complex Type

The following codes are used to add a complex type Address:

var address = builder.ComplexType<Address>();
address.Property(a => a.Country);
address.Property(a => a.City);

It will generate the below metadata document:

<ComplexType Name="Address">
  <Property Name="Country" Type="Edm.String" />
  <Property Name="City" Type="Edm.String" />
</ComplexType>

Derived Complex type

The following codes are used to add a derived complex type SubAddress:

var subAddress = builder.ComplexType<SubAddress>().DerivesFrom<Address>();
subAddress.Property(s => s.Street);

It will generate the below metadata document:

<ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
  <Property Name="Street" Type="Edm.String" />
</ComplexType>

Abstract Complex type

The following codes are used to add an abstract complex type:

builder.ComplexType<Address>().Abstract();
......

It will generate the below metadata document:

<ComplexType Name="Address" Abstract="true">
  ......
</ComplexType>

Open Complex type

In order to build an open complex type, you should change the CLR class by adding an IDictionary<string, object> property, the property name can be any name. For example:

public class Address
{
    public string Country { get; set; }
    public string City { get; set; }
    public IDictionary<string, object> Dynamics { get; set; }
}

Then you can build the open complex type by call HasDynamicProperties():

var address = builder.ComplexType<Address>();
address.Property(a => a.Country);
address.Property(a => a.City);
address.HasDynamicProperties(a => a.Dynamics);

It will generate the below metadata document:

<ComplexType Name="Address" OpenType="true">
  <Property Name="Country" Type="Edm.String" />
  <Property Name="City" Type="Edm.String" />
</ComplexType>

You can find that the complex type Address only has two properties, while it has OpenType="true" attribute.

Entity Type

Basic Entity Type

The following codes are used to add two entity types Customer & Order:

var customer = builder.EntityType<Customer>();
customer.HasKey(c => c.CustomerId);
customer.ComplexProperty(c => c.Location);
customer.HasMany(c => c.Orders);

var order = builder.EntityType<Order>();
order.HasKey(o => o.OrderId);
order.Property(o => o.Token);

It will generate the below metadata document:

<EntityType Name="Customer">
    <Key>
        <PropertyRef Name="CustomerId" />
    </Key>
    <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
    <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
    <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
</EntityType>
<EntityType Name="Order">
    <Key>
        <PropertyRef Name="OrderId" />
    </Key>
    <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
    <Property Name="Token" Type="Edm.Guid" Nullable="false" />
</EntityType>

Abstract Open type

The following codes are used to add an abstract entity type:

builder.EntityType<Customer>().Abstract();
......

It will generate the below metadata document:

<EntityType Name="Customer" Abstract="true">
  ......
</EntityType>

Open Entity type

In order to build an open entity type, you should change the CLR class by adding an IDictionary<string, object> property, while the property name can be any name. For example:

public class Customer
{
    public int CustomerId { get; set; }
    public Address Location { get; set; }
    public IList<Order> Orders { get; set; }
    public IDictionary<string, object> Dynamics { get; set; }
}

Then you can build the open entity type as:

var customer = builder.EntityType<Customer>();
customer.HasKey(c => c.CustomerId);
customer.ComplexProperty(c => c.Location);
customer.HasMany(c => c.Orders);
customer.HasDynamicProperties(c => c.Dynamics);

It will generate the below metadata document:

<EntityType Name="Customer" OpenType="true">
    <Key>
        <PropertyRef Name="CustomerId" />
    </Key>
    <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
    <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
    <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
</EntityType>

You can find that the entity type Customer only has three properties, while it has OpenType="true" attribute.

Entity Container

Non-convention model builder will build the default entity container automatically. However, you should build your own entity sets as:

builder.EntitySet<Customer>("Customers");
builder.EntitySet<Order>("Orders");

It will generate the below metadata document:

<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
    <EntityContainer Name="Container">
        <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
          <NavigationPropertyBinding Path="Orders" Target="Orders" />
        </EntitySet>
        <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
    </EntityContainer>
</Schema>

Besides, you can call Singleton<T>() to add singleton into entity container.

Function

It’s very simple to build function (bound & unbound) in Web API OData. The following codes define two functions. The first is bind to Customer, the second is unbound.

var function = customer.Function("BoundFunction").Returns<string>();
function.Parameter<int>("value");
function.Parameter<Address>("address");

function = builder.Function("UnBoundFunction").Returns<int>();
function.Parameter<Color>("color");
function.EntityParameter<Order>("order");

It will generate the below metadata document:

<Function Name="BoundFunction" IsBound="true">
   <Parameter Name="bindingParameter" Type="WebApiDocNS.Customer" />
   <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
   <Parameter Name="address" Type="WebApiDocNS.Address" />
   <ReturnType Type="Edm.String" Unicode="false" />
</Function>
<Function Name="UnBoundFunction">
   <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
   <Parameter Name="order" Type="WebApiDocNS.Order" />
   <ReturnType Type="Edm.Int32" Nullable="false" />
</Function>

Besides, Web API OData will automatically add function imports for all unbound functions. So, the metadata document should has:

<FunctionImport Name="UnBoundFunction" Function="Default.UnBoundFunction" IncludeInServiceDocument="true" />

Action

Same as function, it’s also very simple to build action (bound & unbound) in Web API OData. The following codes define two actions. The first is bind to collection of Customer, the second is unbound.

var action = customer.Collection.Action("BoundAction");
action.Parameter<int>("value");
action.CollectionParameter<Address>("addresses");

action = builder.Action("UnBoundAction").Returns<int>();
action.Parameter<Color>("color");
action.CollectionEntityParameter<Order>("orders");

It will generate the below metadata document:

<Action Name="BoundAction" IsBound="true">
    <Parameter Name="bindingParameter" Type="Collection(WebApiDocNS.Customer)" />
    <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
    <Parameter Name="addresses" Type="Collection(WebApiDocNS.Address)" />
</Action>
<Action Name="UnBoundAction">
    <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
    <Parameter Name="orders" Type="Collection(WebApiDocNS.Order)" />
    <ReturnType Type="Edm.Int32" Nullable="false" />
</Action>

Same as function, Web API OData will automatically add action imports for all unbound actions. So, the metadata document should has:

 <ActionImport Name="UnBoundAction" Action="Default.UnBoundAction" />

Summary

Let’s put all codes together:

public static IEdmModel GetEdmModel()
{
    var builder = new ODataModelBuilder();

    // enum type
    var color = builder.EnumType<Color>();
    color.Member(Color.Red);
    color.Member(Color.Blue);
    color.Member(Color.Green);

    // complex type
    // var address = builder.ComplexType<Address>().Abstract();
    var address = builder.ComplexType<Address>();
    address.Property(a => a.Country);
    address.Property(a => a.City);
    // address.HasDynamicProperties(a => a.Dynamics);

    var subAddress = builder.ComplexType<SubAddress>().DerivesFrom<Address>();
    subAddress.Property(s => s.Street);

    // entity type
    // var customer = builder.EntityType<Customer>().Abstract();
    var customer = builder.EntityType<Customer>();
    customer.HasKey(c => c.CustomerId);
    customer.ComplexProperty(c => c.Location);
    customer.HasMany(c => c.Orders);
    // customer.HasDynamicProperties(c => c.Dynamics);

    var order = builder.EntityType<Order>();
    order.HasKey(o => o.OrderId);
    order.Property(o => o.Token);

    // entity set
    builder.EntitySet<Customer>("Customers");
    builder.EntitySet<Order>("Orders");

    // function
    var function = customer.Function("BoundFunction").Returns<string>();
    function.Parameter<int>("value");
    function.Parameter<Address>("address");

    function = builder.Function("UnBoundFunction").Returns<int>();
    function.Parameter<Color>("color");
    function.EntityParameter<Order>("order");

    // action
    var action = customer.Collection.Action("BoundAction");
    action.Parameter<int>("value");
    action.CollectionParameter<Address>("addresses");

    action = builder.Action("UnBoundAction").Returns<int>();
    action.Parameter<Color>("color");
    action.CollectionEntityParameter<Order>("orders");

    return builder.GetEdmModel();
}

And the final XML will be:

<?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="WebApiDocNS" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <ComplexType Name="Address">
        <Property Name="Country" Type="Edm.String" />
        <Property Name="City" Type="Edm.String" />
      </ComplexType>
      <ComplexType Name="SubAddress" BaseType="WebApiDocNS.Address">
        <Property Name="Street" Type="Edm.String" />
      </ComplexType>
      <EntityType Name="Customer" OpenType="true">
        <Key>
          <PropertyRef Name="CustomerId" />
        </Key>
        <Property Name="CustomerId" Type="Edm.Int32" Nullable="false" />
        <Property Name="Location" Type="WebApiDocNS.Address" Nullable="false" />
        <NavigationProperty Name="Orders" Type="Collection(WebApiDocNS.Order)" />
      </EntityType>
      <EntityType Name="Order">
        <Key>
          <PropertyRef Name="OrderId" />
        </Key>
        <Property Name="OrderId" Type="Edm.Int32" Nullable="false" />
        <Property Name="Token" Type="Edm.Guid" Nullable="false" />
      </EntityType>
      <EnumType Name="Color">
        <Member Name="Red" Value="0" />
        <Member Name="Blue" Value="1" />
        <Member Name="Green" Value="2" />
      </EnumType>
    </Schema>
    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <Function Name="BoundFunction" IsBound="true">
        <Parameter Name="bindingParameter" Type="WebApiDocNS.Customer" />
        <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
        <Parameter Name="address" Type="WebApiDocNS.Address" />
        <ReturnType Type="Edm.String" Unicode="false" />
      </Function>
      <Function Name="UnBoundFunction">
        <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
        <Parameter Name="order" Type="WebApiDocNS.Order" />
        <ReturnType Type="Edm.Int32" Nullable="false" />
      </Function>
      <Action Name="BoundAction" IsBound="true">
        <Parameter Name="bindingParameter" Type="Collection(WebApiDocNS.Customer)" />
        <Parameter Name="value" Type="Edm.Int32" Nullable="false" />
        <Parameter Name="addresses" Type="Collection(WebApiDocNS.Address)" />
      </Action>
      <Action Name="UnBoundAction">
        <Parameter Name="color" Type="WebApiDocNS.Color" Nullable="false" />
        <Parameter Name="orders" Type="Collection(WebApiDocNS.Order)" />
        <ReturnType Type="Edm.Int32" Nullable="false" />
      </Action>
      <EntityContainer Name="Container">
        <EntitySet Name="Customers" EntityType="WebApiDocNS.Customer">
          <NavigationPropertyBinding Path="Orders" Target="Orders" />
        </EntitySet>
        <EntitySet Name="Orders" EntityType="WebApiDocNS.Order" />
        <FunctionImport Name="UnBoundFunction" Function="Default.UnBoundFunction" IncludeInServiceDocument="true" />
        <ActionImport Name="UnBoundAction" Action="Default.UnBoundAction" />
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>