Click here to Skip to main content
15,884,537 members
Articles / Programming Languages / C#

O-Data Rest API in .NET with Entity Framework and AutoFac

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
8 Dec 2019CPOL7 min read 13.4K   5   1
This is an introductory article about creating an Odata Rest API in collaboration with Entity Framework. We are also going to use Autofac as our IOC container. Also, Repository and Unit of Work pattern will be used for cleaner access of persistence model through our ORM (Entity Framework).

Introduction

The conventional REST API method does not provide much flexibility when it comes to customizing the CRUD operation. This results in adding custom method(s) in the controller of our API that suits our need. But, when it comes with OData Rest API, certain things are available out of the box that we can use right from the "URL" instead of writing separate action method(s) in our controller. This is a huge time saver and reduces the code size. Since each technology has its own pros and cons. I wouldn't say Odata is Ideal for Rest API development. You should always look at the situation and make a pragmatic decision. However, my motto is to let you know how to create Oata res API in collaboration with Entity Framework 6.

We are also going to use Autofac as our IOC container.

We are also going to implement Repository and Unit of Work pattern for cleaner access to persistence model(database) through our ORM.

Background

  • OData: OData or Open Data Protocol is an open protocol which allows the creation and consumption of queryable and interoperable RESTful APIs in a simple and standard way.
  • Rest API: It is a named or representation state transfer application programming interface that makes use of HTTP verbs like Get for getting some info, Put/Patch for updating info, Delete for delete and so on
  • IOC: IoC Container (or Dependency Injection Container) is a framework for implementing automatic dependency injection. Example of such IOC containers are Autofac, Castle Windsor, Unity, etc.
  • ORM: Object-Relational Mapping (ORM) is a technique that lets you query and manipulate data from a database using an object-oriented paradigm.
  • MVC: An Architectural style that helps in segregating Model ("business model and rules"), Controller ("manipulate or operate on those model based on a request received by means of routing") and Views (presentation)
  • Repository and Unit of Work Pattern: It's a bridge between database tables and Entity Framework. It(repository pattern) gives an illusion that database tables are nothing but a property or an in memory object. It also helps to insulate your application from changes in the data store (unit of work). It provides cleaner access to persistence model(data base) through our ORM. Some people don't prefer to use this pattern and rely solely on datacontext class of Entity Framework. Well, this is a subjective topic but I personally feel that Repository and Unit of Work provides a cleaner way to do data centric operations.

Let's Get Started

Step 1: Initial Setup

Open Visual Studio (I am using VS 2019) and create ASP.NET web application by name of project as ODataRestApiWithEntityFramework, you can keep the same name for solution.

Select Project Template as WebApi.

Step 2: Create Some Infrastructure

Add a solution folder with name "Infrastructure". Inside this folder, add four library projects with below names:

  • OData.Business: This will hold our domain classes.
  • OData.InternalDataService: This will be our internal communication channel or data service.
  • OData.IOC: This will be used it for Dependency Injection.
  • OData.ORM: This will be used for Entity Framework related operation.

Your complete solution structure should look as in the image below:

Image 1

Step 3: Adding Model Files

Create folder DomainClasses in OData.Business and all model files as Project.cs and ProjectDetail.cs.

Project.cs

C#
using System.ComponentModel.DataAnnotations;

namespace OData.Business.DomainClasses
{
    public class Project
    {
        [Key]
        [Required]
        public long Id { get; set; }

        [Required]
        public string ProjectNumber { get; set; }

        [Required]
        [MaxLength(300)]
        public string ProjectName { get; set; }

        public virtual ProjectDetail Detail { get; set; }
    }
}

ProjectDetail.cs

C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace OData.Business.DomainClasses
{
    public class ProjectDetail
    {
        [Key, ForeignKey("Project")]
        [Required]
        public long Id { get; set; }

        public Project Project { get; set; }

        public string TechnologiesUsed { get; set; }

        public string ManagerName { get; set; }

        public string Description { get; set; }

        public int TeamSize { get; set; }

        public double PlannedBudget { get; set; }
    }
}

Step 4: Adding Db Context

Inside OData.ORM, add a folder named Context. Add file name ODataDbContext.cs inside it.

Also, install the Entity Framework as a nuget package.

C#
using OData.Business.DomainClasses;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace OData.ORM.Context
{
    public class ODataDbContext : DbContext
    {
        public ODataDbContext() : base("ODataDbContext") { }
        public DbSet<Project> Projects { get; set; }
        public DbSet<ProjectDetail> ProjectDetails { get; set; }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }
}

Step 5: Add ConnectionString

XML
<connectionStrings>
    <add name="ODataDbContext" connectionString="Data Source = .\SQL14; 
     Initial Catalog=ODataDb;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>

Step 6: Add Migration and Update Database

I am using MS SQL Server. Make sure you have SQL Server installed. You can also make use of SQL Lite but you need to make the changes accordingly with connection string, etc.

Open package manager console (Tools => Nuget Package Manager => Package Manager Console).

And type the following commands in sequence inside OData.ORM.

Image 2

PowerShell
PM> enable-migrations

enable-migrations will enable the migration for the lifetime of application development. This will create a Configuration.cs file with Migration folder inside OData.ORM.

Now, add the migration, (in case you have my code from github, simply do enable-migration and update-database).

PowerShell
PM> add-migration "InitialScaffolding"

Now, update the database with migration:

PowerShell
PM> update-database

This will create a database ODataDb with the table, Projects and ProjectDetails.

Note: For some reason, the connectionString placed inside app.config file in OData.ORM is not getting recognized so I have to place it in web.config in ODataRestApiWithEntityFramework. If it works for you in OData.ORM, then go for it.

Step 7: Implementing Repository and Unit Of Work Pattern in OData.ORM

Add Interface IGenericRepository.cs and its implementation class GenericRepository.cs.

For repository and unit of work pattern, we are following a generic approach so that our data service can use it with any entity type (data table type).

Note: Please refer to my github link for accuracy.

IGenericRepository.cs:

C#
public interface IGenericRepository<TEntity> : IDisposable where TEntity : class
    {
        TEntity Get<TDataType>(TDataType id) where TDataType : struct;

        TEntity Get<TDataType>(TDataType id
            , Expression<Func<TEntity, object>> includes
            , Expression<Func<TEntity, bool>> predicate) where TDataType : struct;

        IEnumerable<TEntity> GetAll();
        
        IEnumerable<TEntity> GetAll(Expression<Func<TEntity, object>> includes);
        
        IEnumerable<TEntity> GetAll(Expression<Func<TEntity, object>> includes, 
                             Expression<Func<TEntity, bool>> predicate);
        
        void Add(TEntity entity);
        
        void AddRange(IEnumerable<TEntity> entities);
        
        void Update(TEntity entity);
        
        void Remove(TEntity entity);
    }

GenericRepository.cs:

C#
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
    {
        protected readonly DbContext Context;
        private bool _disposed;

        public GenericRepository(DbContext context)
        {
            Context = context;
        }

        public virtual void Dispose(bool disposing)
        {
            if (!_disposed)
                if (disposing)
                    Context.Dispose();

            _disposed = true;
        }

        public TEntity Get<TDataType>(TDataType id) where TDataType : struct
        {
            return Context.Set<TEntity>().Find(id);
        }

        public TEntity Get<TDataType>(TDataType Id
            , Expression<Func<TEntity, object>> includes
            , Expression<Func<TEntity, bool>> predicate) where TDataType : struct
        {
            return Context.Set<TEntity>().Include(includes).SingleOrDefault(predicate);
        }

        public IEnumerable<TEntity> GetAll()
        {
            return Context.Set<TEntity>().ToList();
        }

        public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, object>> includes)
        {
            return Context.Set<TEntity>().Include(includes).ToList();
        }

        public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, object>> includes, 
               Expression<Func<TEntity, bool>> predicate)
        {
            return Context.Set<TEntity>().Include(includes).Where(predicate).ToList();
        }

        public void Add(TEntity entity)
        {
            Context.Set<TEntity>().Add(entity);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            Context.Set<TEntity>().AddRange(entities);
        }

        public virtual void Update(TEntity entity)
        {
            this.Context.Entry<TEntity>(entity).State = 
                                    System.Data.Entity.EntityState.Modified;
        }
        public void Remove(TEntity entity)
        {
            Context.Set<TEntity>().Attach(entity);
            Context.Entry(entity).State = EntityState.Deleted;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

IUnitOfWork.cs:

C#
public interface IUnitOfWork : IDisposable
    {
        void SaveChanges();
        Task<int> SaveChangesAsync();
        IGenericRepository<T> Repository<T>() where T : class;
    }

UnitOfWork.cs:

C#
public class UnitOfWork : IUnitOfWork
    {
        private bool _disposed;
        private readonly DbContext _context;
        private Hashtable _repositories;

        public UnitOfWork(DbContext context)
        {
            _context = context;

        }

        public void SaveChanges()
        {
            _context.SaveChanges();
        }

        public async Task<int> SaveChangesAsync()
        {
            return await _context.SaveChangesAsync();
        }

        public IGenericRepository<T> Repository<T>() where T : class
        {
            if (_repositories == null)
                _repositories = new Hashtable();

            var type = typeof(T).Name;

            if (!_repositories.ContainsKey(type))
            {
                var repositoryType = typeof(GenericRepository<>);
                var repositoryInstance = Activator.CreateInstance
                     (repositoryType.MakeGenericType(typeof(T)), _context);
                _repositories.Add(type, repositoryInstance);
            }

            return (IGenericRepository<T>)_repositories[type];
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public virtual void Dispose(bool disposing)
        {
            if (!_disposed)
                if (disposing)
                    _context.Dispose();

            _disposed = true;
        }
    }

Step 8: Add IOC Container Autofac in OData.IOC

Install Autofac nuget package in OData.IOC.

Add 2 files, DataServiceModule.cs and OrmModule.cs:

C#
public class DataServiceModule : Module
    {
        public DataServiceModule(){ }

        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<ProjectRepository>().As<IProjectRepository>();
            builder.RegisterType<ProjectDetailRepository>().As<IProjectDetailRepository>();
            
            base.Load(builder);
        }
    }
C#
public class OrmModule : Module
    {
        public OrmModule(){ }

        protected override void Load(ContainerBuilder builder)
        {
            builder.Register(c => new ODataDbContext()).As<DbContext>();

            builder.RegisterGeneric
              (typeof(GenericRepository<>)).As(typeof(IGenericRepository<>));
            builder.RegisterType<UnitOfWork>().As<IUnitOfWork>();
            
            base.Load(builder);
        }
    }

Also, add AutoFacConfig.cs in project ODataRestApiWithEntityFramework.

C#
public class AutofacConfig
    {
        public static void ConfigureContainer(HttpConfiguration config)
        {
            var builder = new ContainerBuilder();

            // Register your Web API controllers.
            builder.RegisterApiControllers(Assembly.GetExecutingAssembly());

            // OPTIONAL: Register the Autofac filter provider.
            builder.RegisterWebApiFilterProvider(config);

            // Register dependencies in controllers/
            // WebApiApplication is a class inside Global.asax.cs
            builder.RegisterControllers(typeof(WebApiApplication).Assembly);

            // Register dependencies in filter attributes
            builder.RegisterFilterProvider();

            // Register our Data dependencies
            builder.RegisterModule(new DataServiceModule());

            //Register our Service dependencies
            builder.RegisterModule(new OrmModule());

            builder.RegisterHttpRequestMessage(config);

            var container = builder.Build();

            config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

            // Set MVC DI resolver to use our Autofac container
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
        }
    }

Make changes in Global.asax file in project ODataRestApiWithEntityFramework.

C#
public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AutofacConfig.ConfigureContainer(GlobalConfiguration.Configuration);
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }
    }

Step 9: Add Internal Data Service (Repository Service) in OData.InternalDataService

Add Service.cs that will be our base for UnitOfWork:

C#
public class Service<TEntity> where TEntity : class
    {
        protected readonly IUnitOfWork _unitOfWork;

        public Service(IUnitOfWork unitOfWork)
        {
            this._unitOfWork = unitOfWork;
        }
    }

In order to access our data table like Projects, ProjectDetails, etc., we will make use of our generic repository. For that, add classes like ProjectRepository.cs (this class will act as an illusion of working with Project table in database).

ProjectRepository.cs:

C#
public class ProjectRepository : Service<Project>, IProjectRepository
    {
        private readonly IGenericRepository<Project> _ProjectRepository;

        public ProjectRepository(IUnitOfWork unitOfWork)
            : base(unitOfWork)
        {
            this._ProjectRepository = unitOfWork.Repository<Project>();
        }

        public Project GetProject(long id)
        {
            return _ProjectRepository.Get(id,
                proj => proj.Detail, proj => proj.Id == id);
        }

        public IEnumerable<Project> GetProjects()
        {
            return _ProjectRepository.GetAll();
        }

        public void AddProject(Project project)
        {
            _ProjectRepository.Add(project);
            _unitOfWork.SaveChanges();
        }

        public void UpdateProject(Project project)
        {
            this._ProjectRepository.Update(project);
            _unitOfWork.SaveChanges();
        }

        public void RemoveProject(Project project)
        {
            this._ProjectRepository.Remove(project);
            _unitOfWork.SaveChanges();
        }
    }

Note: I've also added additional Repository class (ProjectDetailRepository.cs) in github project, you can refer to it for more information.

IProjectRepository.cs:

C#
public interface IProjectRepository
    {
        void AddProject(Project project);
        Project GetProject(long id);
        IEnumerable<Project> GetProjects();
        void RemoveProject(Project project);
        void UpdateProject(Project project);
    }

Step 10: Adding OData in Project "ODataRestApiWithEntityFramework"

Kindly install nuget packages like Microsoft.AspNet.OData and Microsoft.AspNet.WebApi.OData.

Arrange your WebApiConfig.cs as below:

C#
public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapODataServiceRoute("odata", null, GetEdmModel(), 
                   new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
            config.EnsureInitialized();

        }

        private static IEdmModel GetEdmModel()
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder
            {
                Namespace = "ODataRestAPI",
                ContainerName = "DefaultContainer"
            };

            builder.EntitySet<Project>("Project");
            builder.EntitySet<ProjectDetail>("ProjectDetail");

            return builder.GetEdmModel();
        }
    }

Note: Make sure that you don't mix the OData V4 and OData V3 packages. Else, this will create a problem to display data on browser or client side and you won't be getting an error.

Also make sure that if you have ODate V3/V4 packages, then the namespaces should be the same in that of webapiconfig.cs and API controller.

In case you get an Entity Framework error, then do install Entity Framework in project ODataRestApiWithEntityFramework as well.

Step 11: Finally, Add ProjectController.cs with REST Full Methods

C#
public class ProjectController : ODataController
    {
        private readonly IProjectRepository _projectRepository;

        public ProjectController(IProjectRepository projectRepository)
        {
            _projectRepository = projectRepository;
        }

        // GET: localhost/Project
        [EnableQuery]
        [HttpGet]
        public IHttpActionResult Get()
        {
            var projects = _projectRepository.GetProjects();
            return Ok(projects.ToList());
        }

        // GET: localhost/Project(1)
        [EnableQuery]
        [HttpGet]
        public IHttpActionResult Get(long key)
        {
            Project temp = null;
            try
            {
                var resource = _projectRepository.GetProject(key);
                temp = resource;
            }
            catch (ArgumentNullException)
            {
                return BadRequest();
            }
            catch (ArgumentException)
            {
                return BadRequest();
            }
            catch (InvalidOperationException)
            {
                return Conflict();
            }
            return Ok(temp);
        }

        [HttpPost]
        public IHttpActionResult Post(Project project)
        {
            try
            {
                _projectRepository.AddProject(project);
            }
            catch (ArgumentNullException)
            {
                return BadRequest();
            }
            catch (ArgumentException)
            {
                return BadRequest();
            }
            catch (InvalidOperationException)
            {
                return Conflict();
            }

            return Ok("Record saved successfully");
        }

        [HttpPut]
        public IHttpActionResult Put(Project project)
        {
            try
            {
                _projectRepository.UpdateProject(project);
            }
            catch (ArgumentNullException)
            {
                return BadRequest();
            }
            catch (ArgumentException)
            {
                return BadRequest();
            }
            catch (InvalidOperationException)
            {
                return Conflict();
            }

            return Ok("Record updated successfully");
        }

        [HttpDelete]
        public IHttpActionResult Delete(Project project)
        {
            try
            {
                _projectRepository.RemoveProject(project);
            }
            catch (ArgumentNullException)
            {
                return BadRequest();
            }
            catch (ArgumentException)
            {
                return BadRequest();
            }
            catch (InvalidOperationException)
            {
                return Conflict();
            }

            return Ok("Record deleted successfully");
        }
    }

Test the OData Rest API with PostMan

Just to play around, add some records in the Projects and ProjectDetails table manually or by using seed method inside Configuration.cs. However, it is recommended that you can add the records using the Post. Post usually expects an object so it would be recommended to use PostMan or similar framework to do that job.

Build the entire solution and run the project ODataRestApiWithEntityFramework.

You will get the following output on screen:

JavaScript
{
  "@odata.context":"https://localhost:44385/$metadata","value":[
    {
      "name":"Project","kind":"EntitySet","url":"Project"
    }
  ]
}

Now to fetch all the records from Project table, use https://localhost:44385/Project.

Image 3

To fetch a specific record from Project table, use https://localhost:44385/Project(1).

Image 4

To fetch the inner known properties if any inside Project, then use https://localhost:44385/Project?$expand=Detail.

Image 5

You can try for the Post/Put/Delete.


Also there are various OData query options, some common are as under:

  • $top: Can be used to retrieve top n records from a data store
  • $orderby: Can be used to sort the data in ascending or descending order
  • $filter: Can be used to filter the data based on some condition
  • $skip: Can be used along with $orderby to skip certain number of records

Github Link

You can also download the source code from my github page at https://github.com/SunnyMakode/OData.NetFramework.git.

Point of Interest

  1. The steps given above can be reduced if you are using dot net core, since it now has support for Dependency Injection.
  2. We've invested a favorable amount of time in infrastructure because that is the core idea to make this application as loosely coupled as possible so that it can be extended and unit tested.
  3. It is recommended that we should use asynchronous call for our action method.
  4. GraphQL has some similarity with Odata as both of them support URL query based operation.
  5. Special care must be taken that we are not mixing any package of OData v4 or OData V3. Your application on server side won't break however, your browser will not accept the data due to inconsistency.
  6. Make sure that webapiconfig.cs and API controller have the same namespaces with respect to the OData. If you mix it with OData v4 and OData V3, then point 5 might occur and there is no proper documentation available for it.
  7. If your client application consuming this API is not in the same domain as your REST API, then consider adding CORS support in your API application.
  8. Odata are not specific with MVC web API, it can be used with Rest full WCF, etc.

History

  • 8th December, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
QuestionODataQueryOptions $count Pin
Member 1138916315-Sep-20 7:42
Member 1138916315-Sep-20 7:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.