使用 Enterlib.NET 和 WebAPI 开发后端






4.43/5 (13投票s)
使用 Enterlib.NET 开发支持 ODATA 和 DTO 映射的 SOLID 后端,并通过 REST API 暴露。
引言
Enterlib.NET 是一个可用于开发后端应用程序的库。它最初被设计为与 Enterlib Android(Enterlib 套件的另一个成员)一起构建的 Android 应用程序的后端支持。Enterlib.NET 通过使用 SOLID 组件架构为开发者提供了许多优势,可以用于构建后端。它还提供了 ODATA 支持,具有扩展功能,以及将查询结果映射到 DTO 等其他功能。
更详细地说,Enterlib.NET 定义了一些知名模式的抽象,例如 Repository 和 UnitOfWork。这些抽象使用 EntityFramework 6.0 实现,但也可以使用其他 ORM 实现,例如 NHibernate,或者自定义 ORM。使用这些模式的优势在于使应用程序层更健壮、更可靠,并且易于进行单元测试。IRepository 和 IUnitOfWork 合约如下所示。
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);
}
}
此外,该库还定义了一组接口和类,这些接口和类将在开发业务逻辑时提高可重用性、健壮性和生产力。所有这些都通过一个灵活的依赖注入引擎绑定在一起,该引擎支持依赖项的自动发现。
通常,开发人员会将业务逻辑编码到简单的服务类中,这些类将处理业务的特定部分。然后,通过组合这些类来构建更复杂的逻辑。Enterlib.NET 的依赖注入机制允许您以松耦合和独立的方式完成此操作。但它还通过提供一个事件消费/订阅机制(通过消息总线接口)超越了这一点,该机制促进了独立的服务通信。
使用 Enterlib.NET 时,您的业务对象将实现的主要接口是 `IDomainService`。此合约提供了对依赖注入容器、日志记录、本地化和消息总线服务的访问。
namespace Enterlib
{
public interface IDomainService: IProviderContainer
{
ILogService Logger { get; set; }
IMessageBusService MessageBus { get; set; }
ILocalizationService Localize { get; set; }
}
}
还有一个更专业的合约 `IEntityService<T>`,它定义了对 POCO(纯 CLR 对象)的常见业务操作,例如读取、创建、更新、删除,也称为 CRUD 操作。
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);
}
}
另一方面,正如本文开头提到的,Enterlib.NET 支持 ODATA(Open Data Protocol),它可以集成到 WebAPI 控制器中。它通过将包含 *条件*、*包含* 和 *排序* 表达式的字符串转换为 LINQ 表达式来工作,这些表达式可以应用于 IQuerables。这得益于一个实现了以下接口的类。
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);
}
}
如上一个代码列表所示,使用 `IExpressionBuilder<T>`,您可以直接从数据模型映射到所需的输出模型。结果更有效率,因为响应模型 (DTO) 直接从数据库映射,因此不会创建数据模型的实例。此外,查询中仅指定了 DTO 中定义的属性。
当前 `IExpressionBuilder<T>` 实现编译以下运算符
- 或 : ||
- 和 : &&
- 真 : true
- 假 : false
- 大于: >
- 小于: <
- 大于等于: >=
- 小于等于: <=
- 等于 : ==
- 不等于: !=
- 空: null
- like: 根据表达式格式,翻译为 `string.StartWith`、`string.EndWith` 或 `string.Contains`。
- 加: +
- 减: -
- 除: /
下一节将通过一个示例展示如何使用这些组件以及如何将它们集成到 WebAPI 中。
使用代码
现在我们将展示如何通过将 Enterlib.NET 与 ASP.NET WebAPI 集成来实现一个电影业务的后端。我们还将编写一个简单的 AngularJS 视图来展示 ODATA 表达式和 DTO 映射的用法。
应用程序将遵循代码优先的方法。首先,我们将定义数据模型。
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; }
}
}
在前面的列表中定义了数据模型。使用了 Entity Framework 和属性映射,但也可以使用 Fluent API。
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; }
}
}
将 Enterlib.NET 的依赖注入容器集成到 WebAPI 中非常简单,只需使用以下辅助方法即可。
namespace Enterlib.WebApi
{
public static class DataServices
{
public static DependencyResolverContext Configure(HttpConfiguration configuration,
string modelsNamespace,
string serviceResponseNamespace,
Assembly[] assemblies);
}
}
`modelsNamespace` 参数是要定义数据模型的命名空间,`serviceResponseNamespace` 是可选的,但如果提供了,则表示响应 DTO 的命名空间。最后,`assemblies` 参数是一个包含所有组件所在程序集的数组。
例如,我们示例应用程序的设置如下。
var container = DataServices.Configure(config,
"VideoClub.Models",
"VideoClub.Models.Responses",
new Assembly[] { Assembly.GetExecutingAssembly() });
上面的辅助方法返回一个依赖项容器,您可以使用它通过工厂在作用域上下文中注册其他依赖项。例如,在集成 IOC 容器后,我们需要配置消息总线服务,以便以解耦的方式提供组件间通信。
container.Register<IMessageBusService>(() =>
{
var messageBus = new DomainMessageBusService();
messageBus.RegisterProcessor<CreatedMessage<Film>, IMessageProcessor<CreatedMessage<Film>>>();
return messageBus;
}, LifeType.Scope);
在上面的代码段中,当请求作用域开始时,将创建一个实现 `IMessageBusService` 的 `DomainMessageBusService` 实例。然后,使用消息总线,可以为发布在总线上的消息注册处理器。处理器必须实现 `IMessageProcessor<T>` 接口,其中 `T` 是消息或事件的类型。`IMessageProcessor<T>` 的当前实现将由依赖注入容器解析。
通常,业务逻辑是用 `IDomainService` 的实现编写的。常见的做法是继承 `EntityService<T>`,因为它已经定义了实体上的常见操作。但它还通过 `IAuthorizeService` 类型的 `Authorize` 属性提供对角色和操作授权等安全任务的访问。例如,管理 `Film` 实体的业务规则定义在 `FilmsBusinessService` 类中。此类提供了电影的附加功能,例如安全性、验证、国际化和消息通知。您还可以看到该类已使用 `RegisterDependencyAttribute` 注册为 `IEntityService<Film>`,并将 `LifeType` 设置为 `Scoped`。因此,当请求 `IEntityService<Film>` 时,将返回此类的实例。
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; }
}
}
正如您所见,在创建电影工作流中使用了一些服务引用,例如自定义的 `IVideoClubUnitOfWork` 接口,它定义了在数据库层执行存储过程的操作。使用 `IAuthorizeService` 来检查用户角色,使用 `ILocalizationService` 类型的 `Localize` 属性进行国际化,该属性根据上下文的文化返回字符串,并使用消息总线发布成功创建电影时的通知。总线上传递的消息可以被处理器拦截,以便向客户端发送电子邮件通知,告知他们商店新到了一部电影。
下一个类将展示一个 `CreatedMessage<Film>` 消息处理器的示例。
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;
}
}
}
`RegisterDependency` 属性允许您使用相同的生命周期类型为多个接口注册一个类。另一方面,如果您想提供异步消息处理,可以实现以下接口。
namespace Enterlib
{
public interface IAsyncMessageProcessor<T>
where T : IMessage
{
Task<bool> ProcessAsync(T msg);
}
}
然后使用 `await` 表达式发布消息,例如。
await MessageBus.PostAsync(msg);
接下来,您将看到示例后端应用程序使用的某些服务的定义。尽管如此,我想指出的是,通过使用这种架构,不需要手动连接组件引用。所有这些都可以通过接口实现中的属性以声明方式处理,此外,使用消息总线可以以易于演进和维护的方式协调更复杂的工作流。
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);
}
}
}
在大多数应用程序中,在业务工作流执行期间都需要调用一些存储过程。这可能是为了提高性能或处理遗留系统。要处理这种情况,您可以为您的应用程序定义一个特定的 `IUnitOfWork` 实现。例如,VideoClub 后端应用程序定义了一个 `IVideoClubUnitOfWork` 接口。此接口的实现将调用一个存储过程。最简单的方法是扩展基类 `UnitOfWork`,该类使用 Entity Framework 实现。此类位于 Enterlib.EF 程序集中。
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();
}
}
}
核心后端完成后,我们可以在各种平台(即 Web 或桌面)中使用它。此示例后端将作为 ASP.NET WebAPI 的 REST 服务公开。但还有更多,通过使用 Enterlib.WebApi 和 `DataServices.Configure` 辅助方法,您不再需要实现 `ApiController`。控制器由 Enterlib 自动解析,因此您的业务服务将被调用来处理 `IEntityService<T>` 接口定义的所有操作。
此外,您可以在业务服务结果的 REST API 网关层定义数据传输对象 (DTO)。为了应用映射,您只需通过调用 `DataServices.Configure` 方法传递 DTO 所在的命名空间。
var container = DataServices.Configure(config,
"VideoClub.Models",
"VideoClub.Models.Responses", //namespace of DTOs
new Assembly[] { Assembly.GetExecutingAssembly() });
Enterlib.WebApi 遵循一个约定来解析实体 API 控制器的 DTO。所有 DTO 的名称都必须以“Response”结尾,并且属性按名称映射。您还可以包含相关模型的属性映射。例如,在下面的代码中定义了一些 DTO。
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; }
}
}
您现在可以开始向您的 REST API 发送请求,例如。
- GET https://:54697/api/film/get/
- GET https://:54697/api/film/get/1/
- GET https://:54697/api/film/get/?filter=Name like 'Ava%'&orderby=Name desc&include=Author.Country
- POST https://:54697/api/film/post/
- PUT https://:54697/api/film/put/
- DELETE https://:54697/api/film/delete/?filter=Id eq 1
- GET https://:54697/api/film/count/
- GET https://:54697/api/film/find/?filter=Id eq 1
您可以使用 ODATA 表达式进行过滤、排序和包含相关模型到响应中,例如 `include=Author.Country`,其中包含的模型也是映射的 DTO。例如,以下请求的响应:
https://:54697/api/film/get/?filter=Name like 'Ava%'&orderby=Name desc&include=Author.Country
将返回以下 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"
}
]
此外,您可以使用以下请求模式查询多对多关系。
[baseurl]{relationship}__{model}/{action}/?targetId={entityId} (&(ODATA expressions) & distint=true|false)
符号 '()' 表示可选值,'|' 表示可接受的替代项。
例如,要查询与 ID 为 1 的 `Category` 相关联的所有 `Films`,请使用以下请求。
https://:54697/api/filmcategory__film/get/?targetId=1&include=Author
json 响应如下所示。
[
{
"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"
}
]
此外,可以通过在请求中附加 `distint=true` 来查询与 ID 为 1 的 `Category` **不**相关联的 `Films` 实体,例如。
https://:54697/api/filmcategory__film/get/?targetId=1&include=Author&distint=true
此外,验证例程的结果也以标准的 json 格式返回给客户端。验证可以在 REST API 层模型绑定后通过 `ApiController ModelState` 属性应用,也可以在业务层通过抛出 `ValidationException` 来应用。`ValidationException` 可以在 REST API 层通过设置以下过滤器捕获。
config.Filters.Add(new ValidateModelActionFilterAttribute());
`ValidateModelActionFilterAttribute` 位于 `Enterlib.WebApi.Filters` 命名空间。它将捕获业务层中的 `ValidationException` 并向客户端返回格式化的响应。例如,当电影名称策略失败时,您将收到以下具有 HTTP 状态码 400 `Bad Request` 的 json。
{
"ErrorMessage": "Operation Fails",
"Members": [
{
"Member": "Name",
"ErrorMessage": "Must be Unique"
}
],
"ContainsError": true
}
您可以通过定义一个继承自 `EntityApiController<TModel, TResponse>` 的 `ApiController` 来自定义 REST API。这种情况对于在 REST API 层设置授权属性非常有用,如下所示。
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
}
}
下面显示了完整的 Web API 配置。
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());
}
}
}
前端
VideoClub 的前端使用 AngularJS 实现。它只包含一个 angularjs 控制器和一个用于向 REST API 发送请求的服务。页面布局的标记如下所示。
<!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>
以及 Home Index.cshtml 页面。
@{
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")
}
主页将显示一个表格,其中包含一些电影属性,并允许对行进行搜索和排序。
在前端,操作发生在 javascript 文件中。angular 应用程序由 2 个文件组成:app.js(定义模块和服务)和 films.js(定义视图控制器)。
以下是服务和控制器定义。
(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]);
})();
在 films.js 中,我们找到了 `FilmController`,它通过调用 `filmsService.getall` 来加载电影,并提供排序和搜索操作,例如 `orderBy` 和 `onSearchValueChanged`(后者在用户在搜索框中输入时调用)。
关注点
总而言之,使用 Enterlib.NET,您可以快速构建一个支持 ODATA 的 SOLID 后端和 REST API,并采用分层架构,该架构促进了松耦合和独立的测试,有利于保证质量和灵活性。此外,通过使用 DTO 模型和从模型自动映射,性能得到了提高,因为数据库只检索必要的列。
您可以通过使用 nuget 安装以下软件包来使用 Enterlib.NET。
使用以下命令安装核心库。
PM> Install-Package Enterlib
使用以下命令安装 Entity Framework 实现。
PM> Install-Package Enterlib.EF
使用以下命令安装 WebApi 集成。
PM>Install-Package Enterlib.WebApi