Click here to Skip to main content
13,202,960 members (57,843 online)
Click here to Skip to main content
Add your own
alternative version

Stats

14.3K views
222 downloads
19 bookmarked
Posted 14 Apr 2017

Developing back-ends with Enterlib.NET and WebAPI

, 11 Jul 2017
Rate this:
Please Sign up or sign in to vote.
Develop SOLID back-ends exposed through a REST API with ODATA support and DTO mapping using Enterlib.NET.

Introduction

Enterlib.NET is library that can be usefull for developing back-end applications. It was initially designed as the back-end support for android applications built with Enterlib Android, another member of the Enterlib suite .Enterlib.NET provides several advantages by using a SOLID component's architecture which developers can use to build its back-ends. Also it provides ODATA support with extended features and mapping from queries results  to DTO among other features. 

So in more details, Enterlib.NET defines abstractions for some well known patterns like the Repository and UnitOfWork. Those abstractions are implemented with EntityFramework 6.0 but could be implemented using others ORM like NHibertane for instance or a custom one.  The advantage of using such patterns results application layers more robust and reliable and also unit test friendly. The IRepository and IUnitOfWork contracts are shown bellow.

namespace Enterlib.Data
{
     public interface IRepository<TEntity> : IDisposable
            where TEntity : class        
    {
        bool IsDisposed { get; }
    
        TEntity Find(params object[] keyValues);

        TEntity Find(Expression<Func<TEntity, bool>> filter, 
                     Expression<Func<TEntity, object>>[] include = null);
    
        Task<TEntity> FindAsync(Expression<Func<TEntity, bool>> filter,
                                Expression<Func<TEntity, object>>[] include = null);
    
        IQueryable<TEntity> Query(Expression<Func<TEntity, object>>[] include = null);
    
        TEntity Create();
    
        TEntity Create(TEntity value);        
    
        bool Update(TEntity value);
    
        void Delete(TEntity value);
    
        void Delete(IEnumerable<TEntity>entities);
    
        void Reset(TEntity value);
    
        Task ResetAsync(TEntity value);
      
    }
}
namespace Enterlib.Data
{

    public interface IUnitOfWork : IDisposable
    {
 
        IRepository<TEntity> GetRepository<TEntity>()
            where TEntity : class;
                  
        int SaveChanges();
        
        Task<int> SaveChangesAsync();
        
        IEnumerable<TEntity> Invoke<TEntity>(string procedure, IDictionary<string, object> parameters);
        
        IEnumerable<TEntity> Invoke<TEntity>(string procedure, object parameters);
        
        int Invoke(string procedure, IDictionary<string, object> parameters);
        
        int Invoke(string procedure, object parameters);
    }
}

Futhermore the library defines a set of interfaces and classes that will increase reausability, robustnes and productivity when developing business logic. All bounded together by a flexible dependency injection engine with autodiscovering of depencencies.

Typically developers coded business logics into simple service type classes that will handle a specific part of your business. Then by combining those classes more complex logics can be constructed. The dependency injection mechanism of Enterlib.NET allow you to do that in a loosly couple and independent way. But it goes beyond that by providing a event comsuming/subscription mechanism throught a message bus inteface that promotes independent service comunications. 

Using Enterlib.NET the main interface your business object will implement is IDomainService this contract provide access to the dependecy injection container, logging, localization and message bus services.

namespace Enterlib
{
    public interface IDomainService: IProviderContainer
    {                       
    	ILogService Logger { get; set; }
    
    	IMessageBusService MessageBus { get; set; }
    
    	ILocalizationService Localize { get; set; }
    }    
}

There is also a more specialized contract IEntityService<T> that defines common business operations on POCO (Plan old clr objects) like Read,Create,Update,Delete also known as CRUD operations.

 namespace Enterlib
{
    public interface IEntityService<T>: IDomainService
        where T :class
    {
        IQueryable<T> Query(Expression<Func<T, object>>[] include = null);

        T Find(Expression<Func<T, bool>> filter, Expression<Func<T, object>>[] include = null);      

        void Create(T item);        

        void Update(T item);        
     
        int Delete(IEnumerable<T> entities);

        int Delete(T value);                
    }
}   

On ther other hand, as mentionen at the begining of this article, Enterlib.NET provides support for ODATA (Open Data Protocol) that can be integrated into WebAPI controllers. It works by converting strings containing conditions, include and orderby expressions into LINQ expressions that can be applied to IQuerables. That is achived thanks to a class that implements the following interface:

namespace Enterlib.Parsing
{
    public interface IExpressionBuilder<TModel>
    {
        IQueryable<TModel> OrderBy(IQueryable<TModel> query, string expression);

        Expression<Func<TModel, bool>> Where(string expression);

        IQueryable<TModel> Query(IQueryable<TModel> query, string filter, string orderby=null, int skip=-1, int take=-1);

        IQueryable<TResponse> Query<TResponse>(IQueryable<TModel> query, string filter = null, string orderby = null, int skip = -1, int take = -1, string include = null);

        Expression<Func<TModel, TResponse>> Select<TResponse>(string include = null);

        Expression<Func<TModel, object>>[] Include(string expression);
    }
    
}

As you can see in the previus code listing using the IExpressionBuilder<T> you can map directly from the data model to the desire output model. The results is more eficiency due to the response model (DTO) is mapped directly from the database so no instances of the data models are created. Besides only the properties defined in the DTO are specified in the query.

The following operators are compiled by the current IExpressionBuilder<T> implementation:

  • or : ||
  • and : &&
  • true : true
  • false : false
  • gt: >
  • lt: <
  • ge: >=
  • le: <=
  • eq : ==
  • ne: !=
  • null: null
  • like: translated to string.StartWithstring.EndWith or string.Contains in dependence the expression fomat.
  • plus: +
  • sub: -
  • div: /

The following section will use an example to show how to use those components and how to integrates them into WebAPI.

Using the code

Now we are going to show how a backend for a film business can be implemented by integrating Enterlib.NET with ASP.NET WebAPI. Also we are going to write a simple view in AngularJS in order to show the usage of ODATA expressions and the DTO mapping.

The application will follows a code-first approach. Then first of all we are going to define the data models .

namespace VideoClub.Models
{
    public class Country
    {   
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(30)]
        public string Name { get; set; }

        [InverseProperty(nameof(Author.Country))]
        public virtual ICollection<Author> Authors { get; set; } = new HashSet<Author>();
    }
	
	public class Author
    {      
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(30)]
        public string Name { get; set; }

        [Required]
        [MaxLength(30)]
        public string LastName { get; set; }

        [ForeignKey(nameof(Country))]
        public int? CountryId { get; set; }

        public DateTime? BirthDate { get; set; }

        public virtual Country Country { get; set; }

        [InverseProperty(nameof(Film.Author))]
        public virtual ICollection<Film> Films { get; set; } = new HashSet<Film>();
    }
		
	public class Film
    {
        [Key]
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }        
       
        public DateTime ReleaseDate { get; set; }

        [ForeignKey(nameof(Author))]
        public int AuthorId { get; set; }

        public string ImageUrl { get; set; }

        public State? State { get; set; }

        public virtual Author Author { get; set; }     

        [InverseProperty(nameof(FilmCategory.Film))]
        public virtual ICollection<FilmCategory> Categories { get; set; }
                                                  = new HashSet<FilmCategory>();

    }
    
    public enum State
    {
        Available,
        Unavailable,
        CommingSoon
    }
    
    public class Category
    {
    	[Key]
    	public int Id { get; set; }
    
    	[Required]
    	[MaxLength(50)]
    	public String Name { get; set; }
    
    	 [InverseProperty(nameof(Models.FilmCategory.Category))]
    	public virtual ICollection<FilmCategory> FilmCategories { get; set; }
                                                = new HashSet<FilmCategory>();
    
    }
	
	public class FilmCategory
    {
        [Key]
        [Column(Order = 0)]
        [ForeignKey(nameof(Film))]
        public int FilmId { get; set; }

        [Key]
        [Column(Order = 1)]
        [ForeignKey(nameof(Category))]
        public int CategoryId { get; set; }

        public virtual Film Film { get; set; }
       
        public virtual Category Category { get; set; }
    }
    
    public class Client
    {
    
    	[Key]
    	public int Id { get; set; }
    
    	[Required]
    	[MaxLength(30)]
    	public String Name { get; set; }
    
    	[Required]
    	[MaxLength(30)]
    	public String LastName { get; set; }
    }
    
}

In the previus listings the data models were defined. Entity Framework and mapping by attributes was used but Fluent API could be used as well.

namespace VideoClub.Models
{
    public class VideoClubContext:DbContext
    {
        public VideClubContext():
            base("DefaultConnection")
        {
            Configuration.LazyLoadingEnabled = false;
        }

        public DbSet<Country> Countries { get; set; }

        public DbSet<Author> Authors { get; set; }

        public DbSet<Film> Films { get; set; }
        
        public DbSet<Category> Categories { get; set; }

        public DbSet<FilmCategory> FilmCategories { get; set; }
        
        public DbSet<Client> Clients { get; set; }
    }
}

Integrating Enterlib.NET's Dependency Injection Container into WebAPI is really simple, just by using the following helper method:

namespace Enterlib.WebApi
{
    public static class DataServices
    {
        public static DependencyResolverContext Configure(HttpConfiguration configuration,
                string modelsNamespace, 
                string serviceResponseNamespace, 
                Assembly[] assemblies);
    }
}

The modelsNamespace parameter is the namespace where the data models are defined, the serviceResponseNamespace is optional but when supplied is the namespace where the DTO for the services responses are defined. By last the assemblies parameter is and array of all the assemblies where your components are located.

For instance the setup for our sample app will be:

var container = DataServices.Configure(config, 
        "VideoClub.Models", 
        "VideoClub.Models.Responses", 
        new Assembly[] { Assembly.GetExecutingAssembly() });

The helper method above returns a dependency container that you could use for register other dependencies in a scoped context by using factories. For instance after integrating the IOC container we need to configure the message bus service in order to provide inter-component comunications in a decouple way.

container.Register<IMessageBusService>(() =>
{
	var messageBus = new DomainMessageBusService();
	messageBus.RegisterProcessor<CreatedMessage<Film>, IMessageProcessor<CreatedMessage<Film>>>();
	return messageBus;

}, LifeType.Scope);    

In the code section above, a instance of DomainMessageBusService which implements IMessageBusService is created when a request scope begin. Then using the message bus, processors can be registered for messages posted on the bus. The processor must implement the IMessageProcessor<T> interface where T is the type of message or event. The current implementation for the IMessageProcessor<T> will be resolved by the Dependency Injection Container.

Generally business logics are written in a IDomainService implementation. The common aproach is to inherit from EntityService<T> due to it already defines common operations on entities. But it also provides access througout an Authorize property of type IAuthorizeService to security tasks like rolls and action authorization. For instance the business rules for manage Film entities are defined in a FilmsBusinessService class. This class provides additional funtionalities on films like the usage of security, validation , internationalization and message notifications. You can also see the class is registerd for IEntityService<Film> using the RegisterDependencyAttribute with LifeType set to Scoped. So when an IEntityService<Film> is requested and instance of this class will be returned.

namespace VideoClub.Business
{
 
    public class VideoClubBusinessService<T>: EntityService<T>
        where T:class
    {
    	public VideoClubBusinessService(IUnitOfWork unitOfWork) : base(unitOfWork)
    	{
    	}
    
    	public IVideoClubUnitOfWork VideoClubUnitOfWork => (IVideoClubUnitOfWork)UnitOfWork;
    }

    [RegisterDependency(typeof(IEntityService<Film>), LifeType = LifeType.Scope)]
    public class FilmsBusinessService : VideoClubBusinessService<Film>
    {
        public FilmsBusinessService(IUnitOfWork unitOfWork) : base(unitOfWork) { }       

        public override void Create(Film film)
        {          
            //check security access
            if (!Authorize.Authorize("Film.Create"))
               throw new InvalidOperationException(Localize.GetString("AccessError"));            

            //check films policy
            ValidateFilmPolicy(film);

            film.ReleaseDate = DateTime.Now;           

            base.Create(film);

           //run some operation at the database layer typically to invoke some store procedure
            VideoClubUnitOfWork.DoSomeOperation(film.Id);

            MessageBus.Post(new CreatedMessage<Film>(film, this));
        }        
       
        
        //Validates that the Film's name is unique
        private void ValidateFilmPolicy(Film film)
        {
            if (Find(x => x.Name == film.Name) != null)
                throw new ValidationException(Localize.GetString("OperationFails"))
                    .AddError(nameof(film.Name), string.Format(Localize.GetString("UniqueError"), nameof(film.Id) ));
        }
    }
}
namespace VideoClub.Messages
{   
    public class CreatedMessage<T>:IMessage
      where T : class
    {
        public CreatedMessage(T entity, IEntityService<T> sender)
        {
            Sender = sender;
            Entity = entity;
            Id = "EntityCreated";
        }

        public string Id { get; set; }

        public IEntityService<T> Sender { get; }

        public T Entity { get; }
    }
}

As you can see some service references are used in the create film workflow, like for example a custom IUnitOfWork specialization IVideoClubUnitOfWork that defines operations that execute store procedures at the database layer. An IAuthorizeService to check for user's rolls , internationalization using the Localize property of type ILocalizationService that returns strings based on the context's culture, and a message bus to post notifications when a film is succesfully created. The message posted on the bus can be intercepted by a processor to send email notifications to clients in order to inform them a new film has arrived to the store.

The next class will show an example for a CreatedMessage<Film> message processor.

namespace VideoClub.Business
{

    [RegisterDependency(new Type[] {
        typeof(IEntityService<Client>),
        typeof(IMessageProcessor<CreatedMessage<Film>>) }, LifeType = LifeType.Scope)]
    public class ClientBusinessService : VideoClubBusinessService<Client>, 
                                         IMessageProcessor<CreatedMessage<Film>>
    {
        public ClientBusinessService(IUnitOfWork unitOfWork) : base(unitOfWork)
        {

        }

        public bool Process(CreatedMessage<Film> msg)
        {
            IMailingService mailService = (IMailingService)ServiceProvider
            .GetService(typeof(IMailingService));
            
            mailService?.SendMail(Localize.GetString("NewFilmEmailSubject"),
                string.Format(Localize.GetString("NewFilmsEmailBody"), 
                msg.Entity.Name));

            //return true if we want to stop further processing of this message
            //else return false
            return false;
        }
    }
}

The RegisterDependency attribute allows you to register a class for several interfaces using the same lifetype. On the other hand if you want to provide asynchronous message processing you can implement the following interface:

namespace Enterlib
{
	public interface IAsyncMessageProcessor<T>
		where T : IMessage
	{
		Task<bool> ProcessAsync(T msg);
	}
}

And then post message using the await expression like:

await MessageBus.PostAsync(msg);

Next you will see the definitions for some of the services used by the sample back-end app. Even though I want to point out, that by using this architecture no wiring of component references are required. All can be handle declaratively by attributes in your interface implementations, besides using the message bus more complex workflow can be orchestrated in a way easy to evolve and maintain.

namespace VideoClub.Services
{
    [RegisterDependency(typeof(IAuthorizeService), LifeType = LifeType.Singleton)]
    public class VideoClubAuthorizeService : IAuthorizeService
    {
        public bool Authorize<T>(T service, [CallerMemberName] string action = null) where T : class
        {
            return Authorize(action);
        }

        public bool Authorize(string roll)
        {
            return Thread.CurrentPrincipal.IsInRole(roll);
        }

        public bool Authorize(params string[] rolls)
        {
            return rolls.All(x => Authorize(x));
        }
    }
	
	[RegisterDependency(typeof(ILocalizationService), LifeType = LifeType.Singleton)]
    public class LocalizeService : ILocalizationService
    {        
        public string GetString(string id)
        {
            return Resources.ResourceManager.GetString(id, Resources.Culture);
        }
    }
}

In most applications, it is required to call some store procedures during the execution of a business workflow. This can be a decision for increasing performance or for dealing with legacy systems. To handle this scenario you can define a particular IUnitOfWork implementation for your applications. For instance the VideoClub back-end application defines a IVideoClubUnitOfWork interface. The implementation of this interface will call a store procedure. The easiest way to do it, is by extending the base UnitOfWork class which is implemented with Entity Framework. This class is located in the Enterlib.EF assembly.

namespace VideoClub.Data
{
    public interface IVideoClubUnitOfWork : IUnitOfWork
    {
        void DoSomeOperation(int filmId);
    }
	
	[RegisterDependency(typeof(IUnitOfWork), LifeType = LifeType.Scope)]
    public class VideoClubUnitOfWork : UnitOfWork, IVideoClubUnitOfWork
    {
        public VideoClubUnitOfWork() : base(new VideoClubContext())
        {

        }
     
        public void DoSomeOperation(int filmId)
        {
            var result = Invoke<int>("sp_SampleStoreProcedure", new { FilmId = filmId }).First() > 0;
            if (!result)
                throw new InvalidOperationException();
        }
    }
}

After the core back-end is completed we can used it in varius platform, meaning web or desktop. This sample back-end will be exposed as REST services with ASP.NET WebAPI. But there is more, by using Enterlib.WebApi and the DataServices.Configure helper method, you are not longer required to implement a ApiController. The controller are automatically resolved by Enterlib, so your business services will be called to handle all operations defined by the IEntityService<T> interface.

Moreover you can define data transfer objects (DTO) at the REST API gateway layer for your busines services results. In order to apply the mapping you just need to pass the namespace where your DTO are located calling the DataServices.Configure method.

var container = DataServices.Configure(config, 
        "VideoClub.Models", 
        "VideoClub.Models.Responses", //namespace of DTOs
        new Assembly[] { Assembly.GetExecutingAssembly() });

Enterlib.WebApi follows a convention for resolving the DTO for an entity api controller. All DTO's names must end with 'Response' and the properties are mapped by name. Also you can include mappings for properties of related models. For instance in the code bellow some DTOs are defined:

namespace VideoClub.Models.Responses
{
     public class FilmResponse
    {
        public int Id { get; set; }

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

        public DateTime? ReleaseDate { get; set; }

        public int AuthorId { get; set; }


        [MaxLength(50)]
        public string ImageUrl { get; set; }

        public State? State { get; set; }
        
        public AuthorResponse Author { get; set; }

        [NavigationProperty(NavigationProperty = nameof(Models.Film.Author), 
                            Property = nameof(Models.Author.Name))]
        public string AuthorName { get; set; }

        [NavigationProperty(NavigationProperty = nameof(Models.Film.Author), 
                            Property = nameof(Models.Author.LastName))]
        public string AuthorLastName { get; set; }
    }
    
    public class AuthorResponse
    {		 
    	public int Id { get; set; }
    
    	public String Name { get; set; }
    
    	public String LastName { get; set; }
    	 
    	public int? CountryId { get; set; }
    	 
    	public DateTime? BirthDate { get; set; }
    
    	public CountryResponse Country { get; set; }
    
    	[NavigationProperty(NavigationProperty=nameof(Models.Author.Country),
                            Property=nameof(Models.Country.Name))]
    	public String CountryName { get; set; }		
    
    }
    
    public class CountryResponse
    {		 
    	public int Id { get; set; }
    
    	public String Name { get; set; }	
    }
 }   

You can now start making request to your REST API like for instance:

  • GET http://localhost:54697/api/film/get/
  • GET http://localhost:54697/api/film/get/1/
  • GET http://localhost:54697/api/film/get/?filter=Name like 'Ava%'&orderby=Name desc&include=Author.Country
  • POST http://localhost:54697/api/film/post/
  • PUT http://localhost:54697/api/film/put/
  • DELETE http://localhost:54697/api/film/delete/?filter=Id eq 1
  • GET http://localhost:54697/api/film/count/
  • GET http://localhost:54697/api/film/find/?filter=Id eq 1

You can use ODATA expressions for filtering, orderby and including related models in the response ,like include=Author.Country where the included models are also mapped DTOs. For instance the response for:

http://localhost:54697/api/film/get/?filter=Name like 'Ava%'&orderby=Name desc&include=Author.Country

will return the following json:

[
  {
    "Id": 4,
    "Name": "Avatar The last Airbender",
    "ReleaseDate": "2002-05-16 00:00:00",
    "AuthorId": 3,
    "State": 0,
    "ImageUrl": null,
    "Author": {
      "Id": 3,
      "Name": "Jhon",
      "LastName": "Doe",
      "CountryId": 1,
      "BirthDate": "1986-04-16 00:00:00",
      "Country": {
        "Id": 1,
        "Name": "Unite State"
      },
      "CountryName": "Unite State"
    },
    "AuthorName": "Jhon",
    "AuthorLastName": "Doe"
  },
  {
    "Id": 1,
    "Name": "Avatar",
    "ReleaseDate": "2012-02-10 00:00:00",
    "AuthorId": 1,
    "State": 0,
    "ImageUrl": "avatar.jpg",
    "Author": {
      "Id": 1,
      "Name": "James",
      "LastName": "Camerun",
      "CountryId": 1,
      "BirthDate": "1960-04-05 00:00:00",
      "Country": {
        "Id": 1,
        "Name": "Unite State"
      },
      "CountryName": "Unite State"
    },
    "AuthorName": "James",
    "AuthorLastName": "Camerun"
  }
]

Furthermore you can query many-to-many relationships using the following request pattern:

[baseurl]{relationship}__{model}/{action}/?targetId={entityId} (&(ODATA expressions) & distint=true|false)

the symbols '()' indicates optional values and '|' accepted alternatives:

For instance to query all Films associated with a Category with Id = 1 use the following request:

http://localhost:54697/api/filmcategory__film/get/?targetId=1&include=Author

The json response is shown bellow:

[
  {
    "Id": 1,
    "Name": "Avatar",
    "ReleaseDate": "2012-02-10 00:00:00",
    "AuthorId": 1,
    "State": 0,
    "ImageUrl": "avatar.jpg",
    "Author": {
      "Id": 1,
      "Name": "James",
      "LastName": "Camerun",
      "CountryId": 1,
      "BirthDate": "1960-04-05 00:00:00",
      "Country": null,
      "CountryName": "Unite State"
    },
    "AuthorName": "James",
    "AuthorLastName": "Camerun"
  }
]

Also Films entities that are not associated with a Category with Id =1  can be queried by appending distint=true to the request like:

http://localhost:54697/api/filmcategory__film/get/?targetId=1&include=Author&distint=true

In addition the result of validations routines are also returned to the client in a standart json format. Validations can be applied at the REST API layer after the model binding by the ApiController ModelState property or at the business layer by throwing a ValidationException. The ValidationException can be captured at the REST API layer by setting the following filter:

config.Filters.Add(new ValidateModelActionFilterAttribute());

The ValidateModelActionFilterAttribute resides in the Enterlib.WebApi.Filters namespace. It will capture the ValidationException from the business layer and returns a formatted response to the client. For instance when the film's name policy fails you will recieve the following json with http status code of 400 Bad Request.

{
  "ErrorMessage": "Operation Fails",
  "Members": [
    {
      "Member": "Name",
      "ErrorMessage": "Must be Unique"
    }
  ],
  "ContainsError": true
}

You could customize the REST API by defining a ApiController that inherits from EntityApiController<TModel, TResponse>. This scenario could be usefull for setting authorization attributes at the REST API layer as shown bellow:

namespace VideoClub.Controllers
{
    [Authorize(Roles = "FilmManager")]
	public class FilmController : EntityApiController<Film, FilmResponse>
	{
		public FilmController(IEntityService<Film> businessUnit) : base(businessUnit)
		{
		}

		public override Task<FilmResponse> Get(int id, string include = null)
		{
			//Customize the GET action
			return base.Get(id, include);
		}

		//TODO Defines other actions here 
	}
}

Bellow is shown the complete Web API configuration.

namespace VideoClub
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();
            
            config.Routes.MapHttpRoute(
            	name: "DefaultApi",
            	routeTemplate: "api/{controller}/{action}/{id}",
            	defaults: new { action = RouteParameter.Optional, id = RouteParameter.Optional }
            );
            
            //Configure Formatters for Javascript
            config.Formatters.JsonFormatter.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
            config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter { CamelCaseText = false });
            
            //Enterlib integration into Web API pipeline
            var container = DataServices.Configure(config, 
                                 "VideoClub.Models", 
                                 "VideoClub.Models.Responses", 
                                 new Assembly[] { Assembly.GetExecutingAssembly() });
            
            //Register Dependencies here       
            container.Register<IMessageBusService>(() =>
            {
            	var messageBus = new DomainMessageBusService();
            	messageBus.RegisterProcessor<CreatedMessage<Film>, IMessageProcessor<CreatedMessage<Film>>>();
            	return messageBus;
            
            }, LifeType.Scope);
            
            //Register a model validation action filter
            config.Filters.Add(new ValidateModelActionFilterAttribute());           
        }
    }
}

The Front-End

The Fron-End of the VideoClub is implemented with AngularJS. It consist of only one angularjs controller and a service to make the request to the REST API. The markup for the page layout is presented bellow.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")

</head>
<body ng-app="videoClub">   
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/angular")
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")    
    @Scripts.Render("~/bundles/app")

    @RenderSection("scripts", required: false)
</body>
</html>

And following the Home Index.cshtml page:

@{
    ViewBag.Title = "Home Page";
}


<h2>Films</h2>

<div ng-controller="FilmsController">
    <div id="filters" >      
        <span><b> Search:</b></span>
        <input type="text" width="100" ng-model="searchValue" 

                                       ng-change="onSearchValueChanged()" />     
    </div>

    <div id="grid" >
        <table>            
            <tr>
                <th></th>
                <th class="orderable" ng-click="orderBy('Name')">
                     Name <div class="{{orders.Name}}" ></div>
                </th>
                <th class="orderable" ng-click="orderBy('AuthorName')">
                    Author <div class="{{orders.AuthorName}}"></div> 
                </th>
                <th class="orderable" ng-click="orderBy('ReleaseDate')">
                     Release Date <div class="{{orders.ReleaseDate}}" ></div> 
                </th>
                <th >State</th>
            </tr>
            <tbody>
                <tr ng-repeat="film in films">
                    <td> 
                         <img src="~/Content/Images/{{film.ImageUrl}}" 

                              ng-show="film.ImageUrl" 

                              width="150" height="100" /> 
                    </td>
                    <td><a href="#" ng-click="loadFilm(film)">{{film.Name}}</a></td>
                    <td>{{film.AuthorName}} {{film.AuthorLastName}}</td>
                    <td>{{film.ReleaseDate}}</td>
                    <td>{{film.State}}</td>
                </tr>
            </tbody>
        </table>
    </div>

</div>

@section Scripts {
    @Scripts.Render("~/bundles/films")
}

The home page will present a table displaying some film's properties and allows to search and ordering of the rows.

At the front-end the action takes place in the javascript files. The angular app is composed of 2 files app.js where the module and services are defined and the films.js where we define the view controller.

Following are the service and controller definitions:

(function () {
    'use strict';
    
    var videoClub = angular.module('videoClub', []);

    videoClub.factory('filmService', ['$http', function ($http) {
        return {
            getById: function (id, callback) {
                $http.get('/api/film/get/' + id).then(
                    function success(response) {
                        callback(true, response.data);
                    },
                    function error(response) {
                        if (response.status == -1) {
                            //timeout
                        }
                        callback(false, undefined);
                    }
                )
            },
            getAll: function (filter, orderby, callback) {
                if (orderby == undefined)
                    orderby = 'Id';

                let q = ['orderby=' + encodeURIComponent(orderby)];
                if (filter != undefined)
                    q.push('filter=' + encodeURIComponent(filter));


                $http.get('/api/film/get/?' + q.join('&')).then(
                    function success(response) {
                        callback(true, response.data);
                    },
                    function error(response) {
                        if (response.status == -1) {
                            //timeout
                        }
                        callback(false, undefined);
                    }
                );
            }
        }
    }]);

})();
(function () {
    'use strict';

    function FilmsController($scope, filmService) {
        // handler for the search timeout 
        var timeoutHandler = null;

        //array of properties included in the search
        var filterableProps = ['Name', 'AuthorName', 'AuthorLastName'];

        //array for holding the films 
        $scope.films = [];      

        // dictionary for holding the states of the sorted columns
        $scope.orders = {};      
        
        function getAllFilms(filter, orderby) {
            filmService.getAll(filter, orderby, function (success, films) {              
                if (success) {
                    $scope.films = films;
                } else {
                    alert('request fails');
                }
            });
        }

        // return the orderby expression
        function getOrderByString() {
            let orderby = '';
            let count = 0;

            for (var column in $scope.orders) {
                let value = $scope.orders[column];
                if (value == undefined)
                    continue;

                if (count > 0)
                    orderby += ',';

                orderby += column;
                if (value == 'desc')
                    orderby += ' DESC';

                count++;
            }
            return orderby || undefined;
        }

        //return the filter expression
        function getFilterString() {
            let searchExpression = '';

            if ($scope.searchValue != null && $scope.searchValue.length > 0) {
                for (var prop of filterableProps) {
                    if (searchExpression != '')
                        searchExpression += ' OR ';                    
                    searchExpression += prop + " LIKE '%" + $scope.searchValue + "%'";
                }
            }
            return searchExpression || undefined;
        }

        // view action called for sorting a column
        $scope.orderBy = function (column) {
            if ($scope.orders[column] == undefined) {
                $scope.orders[column] = 'asc';
            } else if ($scope.orders[column] == 'asc') {
                $scope.orders[column] = 'desc';
            } else if ($scope.orders[column] == 'desc') {
                delete $scope.orders[column];
            }
                 
            getAllFilms(getFilterString(), getOrderByString());
        }                 

        //view action to be called when the search value changes
        $scope.onSearchValueChanged = function () {
            if (timeoutHandler != null)
                clearTimeout(timeoutHandler);

            timeoutHandler = setTimeout(function () {
                    getAllFilms(getFilterString(), getOrderByString());
             }, 500);            
        }

        //loads the films
        getAllFilms();
    }

    //register the controller
    angular.module('videoClub')
           .controller('FilmsController', ['$scope','filmService', FilmsController]);

})();

Inside films.js we found the FilmController which loads the films by calling filmsService.getall  and provides actions for sorting and searching like orderBy and onSearchValueChanged which is called while the user is typing in the search box.

Points of Interest

Resuming by using Enterlib.NET you can build a SOLID back-end with a REST API quickly with ODATA suppport and a layered architecture that promotes loosely coupling and isolated testing, good for ensuring quality and flexibility. Also by using DTO model  and automatic mapping from the models the performance is increased because only the necesary columns are retrieved from the database.

You can use Enterlib.NET by installing the following packages with nuget:

Install the core library with :

PM> Install-Package Enterlib

Install the Entity Framework implementation with:

PM> Install-Package Enterlib.EF

Install the WebApi integration with:

PM>Install-Package Enterlib.WebApi

License

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

Share

About the Author

Ansel Castro
Software Developer (Senior) Globant
Uruguay Uruguay
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionHow to use your IRepository and IUnitOfWork Pin
Mou_kol11-Jul-17 23:05
memberMou_kol11-Jul-17 23:05 
AnswerRe: How to use your IRepository and IUnitOfWork Pin
Ansel Castro12-Jul-17 5:31
professionalAnsel Castro12-Jul-17 5:31 
GeneralMy vote of 5 Pin
IdaRey29-Apr-17 5:31
memberIdaRey29-Apr-17 5:31 

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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.171020.1 | Last Updated 12 Jul 2017
Article Copyright 2017 by Ansel Castro
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid