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.StartWith
, string.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)
{
if (!Authorize.Authorize("Film.Create"))
throw new InvalidOperationException(Localize.GetString("AccessError"));
ValidateFilmPolicy(film);
film.ReleaseDate = DateTime.Now;
base.Create(film);
VideoClubUnitOfWork.DoSomeOperation(film.Id);
MessageBus.Post(new CreatedMessage<Film>(film, this));
}
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 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",
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)
{
return base.Get(id, include);
}
}
}
Bellow is shown the complete Web API configuration.
namespace VideoClub
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = RouteParameter.Optional, id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter { CamelCaseText = false });
var container = DataServices.Configure(config,
"VideoClub.Models",
"VideoClub.Models.Responses",
new Assembly[] { Assembly.GetExecutingAssembly() });
container.Register<IMessageBusService>(() =>
{
var messageBus = new DomainMessageBusService();
messageBus.RegisterProcessor<CreatedMessage<Film>, IMessageProcessor<CreatedMessage<Film>>>();
return messageBus;
}, LifeType.Scope);
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>© @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) {
}
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) {
}
callback(false, undefined);
}
);
}
}
}]);
})();
(function () {
'use strict';
function FilmsController($scope, filmService) {
var timeoutHandler = null;
var filterableProps = ['Name', 'AuthorName', 'AuthorLastName'];
$scope.films = [];
$scope.orders = {};
function getAllFilms(filter, orderby) {
filmService.getAll(filter, orderby, function (success, films) {
if (success) {
$scope.films = films;
} else {
alert('request fails');
}
});
}
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;
}
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;
}
$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());
}
$scope.onSearchValueChanged = function () {
if (timeoutHandler != null)
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(function () {
getAllFilms(getFilterString(), getOrderByString());
}, 500);
}
getAllFilms();
}
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