OData Using C#, MVC, WebAPI, Entity Framework, Dependency Injection (DI)/Inversion of Control (IoC) and Kendo UI

API Area Registration

Here’s what the top end of the APIAreaRegistration.CS loos like:


public class APIAreaRegistration : AreaRegistration
 {
 public override string AreaName
 {
 get
 {
 return "api";
 }
 }
...

DAL Setup

We are using Entity Framework Database first. It is recommended that you are familiar with how to set this up: http://msdn.microsoft.com/en-us/data/jj206878.aspx .

Since we will be accessing our data through a generic repository, a few modifications need to be made to the database context class. First create a DbContextExtension class and include it directly in your EF DAL project. Next, create an IDbContext interface in the same project then refactor the top portion of *Context.cs class:


public static class DbContextExtension
{
 public static void ApplyStateChanges(this DbContext dbContext)
 {
 //if (dbContext.ChangeTracker.Entries().Select(dbEntityEntry =>
 // dbEntityEntry.Entity as IObjectState).Any(entityState => entityState == null))

foreach (var dbEntityEntry in dbContext.ChangeTracker.Entries())
 {
 var entityState = dbEntityEntry.Entity as IObjectState;
 if (entityState == null)
 {
 throw new InvalidCastException(
 "All entities must implement the IObjectState interface." +
 "This interface must be implemented so each entity state " +
 "can be explicitly determined when updating graphs.");
 }
 }
 }

private static EntityState ConvertState(ObjectState state)
 {
 switch (state)
 {
 case ObjectState.Added:
 return EntityState.Added;
 case ObjectState.Modified:
 return EntityState.Modified;
 case ObjectState.Deleted:
 return EntityState.Deleted;
 default:
 return EntityState.Unchanged;
 }
 }
}

IDbContext.cs:


public interface IDbContext
{
 IDbSet<T> Set<T>() where T : class;
 int SaveChanges();
 DbEntityEntry Entry(object o);
 void Dispose();
}

Next we have the refactored *Context.cs class (pointed out in the first image of our solution):


public partial class AtmsContext : DbContext, IDbContext
{
 static AtmsContext()
 {
 // if required, at this point we can check for a db's existance,
 // create a new database (code first), or seed the db with values
 Database.SetInitializer<AtmsContext>(null);
 }

public AtmsContext()
 : base("name=AtmsContext")
 {
 // disable lazy loading for best practices - keeps from accidentaly loading large entity graphs
 Configuration.LazyLoadingEnabled = false;
 }

// refactored
 public new IDbSet<T> Set<T>() where T : class
 {
 return base.Set<T>();
 }

public override int SaveChanges()
 {
 // this.ApplyStateChanges();
 return base.SaveChanges();
 }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
 {
 throw new UnintentionalCodeFirstException();
 }

If you don’t understand the concept of how this works, there are a lot of other resources available.

Data Access

We are dealing with a USERS and USERGROUPS table. The database contains a link table between the two, establishing a 1..* relationship between USERS and USERGROUPS. You will not see this link table as a part of the entities generated by EF DB First. Rather, EF maintains these relationships on its own, and accessed through navigation properties

Accessing our data is done through a RESTful interface, where two classes are responsible for returning data –UserGroupsController.cs and UsersController.cs.

UserGroupsController.cs:


public class UserGroupsController : ODataController
{
 //
 // GET: ~/api/UserGroups
 //

private readonly IDbContext _dbContext;

public UserGroupsController(IDbContext dbContext)
 {
 if (dbContext == null)
 {
 throw new ArgumentNullException("dbContext");
 }
 _dbContext = dbContext;
 }

[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
 public IQueryable<USERGROUP> Get()
 {
 var unitOfWork = new ATMS.Repository.UnitOfWork(_dbContext);

var userGroups = unitOfWork.Repository<USERGROUP>()
 .Query()
 .Include(u => u.USERS)
 .Get();

unitOfWork.Save(); // includes Dispose()

return userGroups.AsQueryable();
 }
}

We inherit from ODataController, and not ApiController (as we would with WebAPI).

Dependency Injection

You will also notice the single constructor that follows the requirements for our dependency injection / inversion of control container, Simple Injector.

Data Access URL

To retrieve data we access the following url: ~/api/UserGroups?$orderby=GROUPNAME 

The service returns a list of groups, and the OData filter is then applied to the result. This is achieved through the QueryableAttribute that decorates the Get() method.

UsersController.cs:


/// <summary>
/// Controller class for WebAPI-related data
///
/// Using OData, we inherit ODataController instead of ApiController
///
/// Method return types should be IQueryable rather than IEnumerable for OData support
///
/// See http://www.odata.org/documentation/odata-v2-documentation/uri-conventions/ for OData URI conventions
/// OData: ~/api/Users?$inlinecount=allpages&top=2
/// OData: ~/api/Users?$inlinecount=allpages - includes odata.count
/// OData: inline filtering: ~/api/Users?$filter=USERNAME eq 'asgro'
///
/// Supporting OData Query Options
/// http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options
///
/// To get all users in a specific group, use the OData syntax:
/// /api/Users?$filter=USERGROUPS/any(usergroup: usergroup/ID eq 'a3a7ba66e3524703bc42703c5176097e')
///
///
/// </summary>
public class UsersController : ODataController
//public class UsersController : EntitySetController<ID, string>
{
 private readonly IDbContext _dbContext;

public UsersController(IDbContext dbContext)
 {
 if (dbContext == null)
 {
 throw new ArgumentNullException("dbContext");
 }
 _dbContext = dbContext;
 }

/// <summary>
 /// This is the primary GET method for OData Requests to ~/api/Users
 ///
 /// Server-driven Paging: http://www.asp.net/web-api/overview/odata-support
 /// -in-aspnet-web-api/supporting-odata-query-options#server-paging
 /// To limit the # of records returned, use the PageSize attribute.
 /// The response will contain a link to the next set of data.
 ///
 /// </summary>
 /// <returns></returns>
 //[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
 public IEnumerable<USER> Get(ODataQueryOptions<USER> options)
 {
 // Override Optional query settings
 //ODataQuerySettings settings = new ODataQuerySettings()
 //{
 // PageSize = 10
 //};


 var unitOfWork = new ATMS.Repository.UnitOfWork(_dbContext);

var users = options.ApplyTo(unitOfWork.Repository<USER>().Queryable
 .Include(u => u.USERGROUPS)
 .OrderBy(order => order.USERNAME))
 .Cast<USER>().ToList();

unitOfWork.Save(); // includes Dispose()

return users;
 }

[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
 public IQueryable<USER> Get(string key)
 {
 var unitOfWork = new ATMS.Repository.UnitOfWork(_dbContext);

//Inspect the query options passed in on the query string
 var opts = this.Request;


 var user = unitOfWork.Repository<USER>()
 .Query()
 .Filter(u => u.ITEMID == key)
 .Get();
 unitOfWork.Save();
 return user.AsQueryable();
 }
}

You will notice that the Get() method returns IEnumerable, and not IQueryable. We have also removed the Queryable attribute. This is because we want Entity Framework to perform the filtering, that is, OData will pass along the OdataQueryOption to EF. Otherwise, we run into issue where all data is returned from the database, then filtering occurring later, which is inefficient.

— NOTEWORTHY —

You don’t mix and match QueryableAttribute and ODataQueryOptions<T>. Pick one depending on whether you want manual control over applying the query options (ODataQueryOptions<T>) or make it happen automatically (QueryableAttribute).

You have two options:


public IEnumerable<USER> Get(ODataQueryOptions<USER> options)
{
 var dbContext = new ATMS.DAL.AtmsContext();
 var ret = options.ApplyTo(dbContext.USERS).Cast<USER>();
 return ret;
}

or


[Queryable]
public IEnumerable<USER> Get(ODataQueryOptions<USER> options)
{
 var dbContext = new ATMS.DAL.AtmsContext();
 var ret = dbContext.USERS;
 return ret;
}

If using options.ApplyTo() and you include the Queryable attribute, a second filter will be applied to the result, which is not what we want.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s