对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(2)
chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目。
源码: https://github.com/chsakell/spa-webapi-angularjs
文章:http://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/
这里记录下对此项目的理解。分为如下几篇:
● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(1)--领域、Repository、Service
● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(2)--依赖倒置、Bundling、视图模型验证、视图模型和领域模型映射、自定义handler
● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(3)--主页面布局
● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)--Movie增改查以及上传图片
依赖倒置
我们注意到经常是把接口注入到构造函数中,然后调用接口方法,如何最终让接口的实现类执行相应的方法呢?这时候就应该请出Autofac了。通过NuGet安装:Autofac ASP.NET Web API 2.2 Integration
在HomeCinema.Web中创建有关Autofac的配置类。
namespace HomeCinema.Web.App_Start { public class AutofacWebapiConfig { public static IContainer Container; public static void Initialize(HttpConfiguration config) { Initialize(config, RegisterServices(new ContainerBuilder())); } public static void Initialize(HttpConfiguration config, IContainer container) { config.DependencyResolver = new AutofacWebApiDependencyResolver(container); } private static IContainer RegisterServices(ContainerBuilder builder) { builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); // EF HomeCinemaContext builder.RegisterType<HomeCinemaContext>() .As<DbContext>() .InstancePerRequest(); builder.RegisterType<DbFactory>() .As<IDbFactory>() .InstancePerRequest(); builder.RegisterType<UnitOfWork>() .As<IUnitOfWork>() .InstancePerRequest(); builder.RegisterGeneric(typeof(EntityBaseRepository<>)) .As(typeof(IEntityBaseRepository<>)) .InstancePerRequest(); ... Container = builder.Build(); return Container; } } }
再创建一个用来初始化Autofac。
namespace HomeCinema.Web.App_Start { public class Bootstrapper { public static void Run() { // Configure Autofac AutofacWebapiConfig.Initialize(GlobalConfiguration.Configuration); ... } } }
还需要在全局运行以上的静态方法Run.
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { var config = GlobalConfiguration.Configuration; ... Bootstrapper.Run(); ... GlobalConfiguration.Configuration.EnsureInitialized(); ... } }
配置Bundling
在ASP.NET MVC中提供了一种管理js,css文件的方法叫做Bunling,首先提供一个静态方法为BundleCollection集合添加元素。如下:
namespace HomeCinema.Web.App_Start { public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/Vendors/modernizr.js")); bundles.Add(new ScriptBundle("~/bundles/vendors").Include( "~/Scripts/Vendors/jquery.js", "~/Scripts/Vendors/bootstrap.js", "~/Scripts/Vendors/toastr.js", "~/Scripts/Vendors/jquery.raty.js", "~/Scripts/Vendors/respond.src.js", "~/Scripts/Vendors/angular.js", "~/Scripts/Vendors/angular-route.js", "~/Scripts/Vendors/angular-cookies.js", "~/Scripts/Vendors/angular-validator.js", "~/Scripts/Vendors/angular-base64.js", "~/Scripts/Vendors/angular-file-upload.js", "~/Scripts/Vendors/angucomplete-alt.min.js", "~/Scripts/Vendors/ui-bootstrap-tpls-0.13.1.js", "~/Scripts/Vendors/underscore.js", "~/Scripts/Vendors/raphael.js", "~/Scripts/Vendors/morris.js", "~/Scripts/Vendors/jquery.fancybox.js", "~/Scripts/Vendors/jquery.fancybox-media.js", "~/Scripts/Vendors/loading-bar.js" )); bundles.Add(new ScriptBundle("~/bundles/spa").Include( "~/Scripts/spa/modules/common.core.js", "~/Scripts/spa/modules/common.ui.js", "~/Scripts/spa/app.js", "~/Scripts/spa/services/apiService.js", "~/Scripts/spa/services/notificationService.js", "~/Scripts/spa/services/membershipService.js", "~/Scripts/spa/services/fileUploadService.js", "~/Scripts/spa/layout/topBar.directive.js", "~/Scripts/spa/layout/sideBar.directive.js", "~/Scripts/spa/layout/customPager.directive.js", "~/Scripts/spa/directives/rating.directive.js", "~/Scripts/spa/directives/availableMovie.directive.js", "~/Scripts/spa/account/loginCtrl.js", "~/Scripts/spa/account/registerCtrl.js", "~/Scripts/spa/home/rootCtrl.js", "~/Scripts/spa/home/indexCtrl.js", "~/Scripts/spa/customers/customersCtrl.js", "~/Scripts/spa/customers/customersRegCtrl.js", "~/Scripts/spa/customers/customerEditCtrl.js", "~/Scripts/spa/movies/moviesCtrl.js", "~/Scripts/spa/movies/movieAddCtrl.js", "~/Scripts/spa/movies/movieDetailsCtrl.js", "~/Scripts/spa/movies/movieEditCtrl.js", "~/Scripts/spa/controllers/rentalCtrl.js", "~/Scripts/spa/rental/rentMovieCtrl.js", "~/Scripts/spa/rental/rentStatsCtrl.js" )); bundles.Add(new StyleBundle("~/Content/css").Include( "~/content/css/site.css", "~/content/css/bootstrap.css", "~/content/css/bootstrap-theme.css", "~/content/css/font-awesome.css", "~/content/css/morris.css", "~/content/css/toastr.css", "~/content/css/jquery.fancybox.css", "~/content/css/loading-bar.css")); BundleTable.EnableOptimizations = false; } } }
在全局中启用Bundling。
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { ... BundleConfig.RegisterBundles(BundleTable.Bundles); } }
在ASP.NET MVC的视图页按如下调用Bundle中的css或js文件。
@Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") @Scripts.Render("~/bundles/vendors") @Scripts.Render("~/bundles/spa")
Styles.Render方法或Scripts.Render位于"Microsoft Asp.Net Web Optimization"组件的"System.Web.Optimization"命名空间内,先通过NuGet安装:Microsoft Asp.Net Web Optimization
然后在Views文件夹的web.config中把"System.Web.Optimization"命名空间配置进去。
<pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="HomeCinema.Web" /> <add namespace="System.Web.Optimization" /> </namespaces> </pages>
视图模型的验证
首先,通过NuGet安装:FluentValidation
拿Customer的视图模型来说:
namespace HomeCinema.Web.Models { [Bind(Exclude = "UniqueKey")] public class CustomerViewModel : IValidatableObject { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string IdentityCard { get; set; } public Guid UniqueKey { get; set; } public DateTime DateOfBirth { get; set; } public string Mobile { get; set; } public DateTime RegistrationDate { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var validator = new CustomerViewModelValidator(); var result = validator.Validate(this); return result.Errors.Select(item => new ValidationResult(item.ErrorMessage, new[] { item.PropertyName })); } } }
通过IValidatableObject的接口方法Validate,我们为CustomerViewModel定义了一个验证类CustomerViewModelValidator:
namespace HomeCinema.Web.Infrastructure.Validators { public class CustomerViewModelValidator : AbstractValidator<CustomerViewModel> { public CustomerViewModelValidator() { RuleFor(customer => customer.FirstName).NotEmpty() .Length(1, 100).WithMessage("First Name must be between 1 - 100 characters"); RuleFor(customer => customer.LastName).NotEmpty() .Length(1, 100).WithMessage("Last Name must be between 1 - 100 characters"); RuleFor(customer => customer.IdentityCard).NotEmpty() .Length(1, 100).WithMessage("Identity Card must be between 1 - 50 characters"); RuleFor(customer => customer.DateOfBirth).NotNull() .LessThan(DateTime.Now.AddYears(-16)) .WithMessage("Customer must be at least 16 years old."); RuleFor(customer => customer.Mobile).NotEmpty().Matches(@"^\d{10}$") .Length(10).WithMessage("Mobile phone must have 10 digits"); RuleFor(customer => customer.Email).NotEmpty().EmailAddress() .WithMessage("Enter a valid Email address"); } } }
以上的RuleFor方法等就是FluentValidation组件的Fluent API。
视图模型和领域模型的映射
首先,通过NuGet安装:Automapper
继承AutoMapper的Profile类,用来把领域模型映射到视图模型。
namespace HomeCinema.Web.Mappings { public class DomainToViewModelMappingProfile : Profile { public override string ProfileName { get { return "DomainToViewModelMappings"; } } protected override void Configure() { Mapper.CreateMap<Movie, MovieViewModel>() .ForMember(vm => vm.Genre, map => map.MapFrom(m => m.Genre.Name)) .ForMember(vm => vm.GenreId, map => map.MapFrom(m => m.Genre.ID)) .ForMember(vm => vm.IsAvailable, map => map.MapFrom(m => m.Stocks.Any(s => s.IsAvailable))) .ForMember(vm => vm.NumberOfStocks, map => map.MapFrom(m => m.Stocks.Count)) .ForMember(vm => vm.Image, map => map.MapFrom(m => string.IsNullOrEmpty(m.Image) == true ? "unknown.jpg" : m.Image)); Mapper.CreateMap<Genre, GenreViewModel>() .ForMember(vm => vm.NumberOfMovies, map => map.MapFrom(g => g.Movies.Count())); // code omitted Mapper.CreateMap<Customer, CustomerViewModel>(); Mapper.CreateMap<Stock, StockViewModel>(); Mapper.CreateMap<Rental, RentalViewModel>(); } } }
再写一个继承AutoMapper的Profile类,用来把视图模型映射到领域模型。
namespace HomeCinema.Web.Mappings { public class ViewModelToDomainMappingProfile : Profile { public override string ProfileName { get { return "ViewModelToDomainMappings"; } } protected override void Configure() { Mapper.CreateMap<MovieViewModel, Movie>() //.ForMember(m => m.Image, map => map.Ignore()) .ForMember(m => m.Genre, map => map.Ignore()); } } }
接着定义一个有关AutoMapper的配置类:
namespace HomeCinema.Web.Mappings { public class AutoMapperConfiguration { public static void Configure() { Mapper.Initialize(x => { x.AddProfile<DomainToViewModelMappingProfile>(); }); } } }
封装一个类调用AutoMapper的配置:
namespace HomeCinema.Web.App_Start { public class Bootstrapper { public static void Run() { // Configure Autofac AutofacWebapiConfig.Initialize(GlobalConfiguration.Configuration); //Configure AutoMapper AutoMapperConfiguration.Configure(); } } }
最后,在全局文件中运行Run静态方法,略去。
自定义HttpMessageHandler
在System.Net.Http命名空间中定义了一个抽象类自定义HttpMessageHandler,其中定义了一个SendAsync方法,用来接收请求,返回响应,以异步的方式:
protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
HttpMessageHandler还有一个继承类DelegatingHandler,这里,就来继承DelegatingHandler,实现自定义handler。
namespace HomeCinema.Web.MessageHandlers { public class HomeCinemaAuthHandler : DelegatingHandler { IEnumerable<string> authHeaderValues = null; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { try { request.Headers.TryGetValues("Authorization",out authHeaderValues); if(authHeaderValues == null) return base.SendAsync(request, cancellationToken); // cross fingers var tokens = authHeaderValues.FirstOrDefault(); tokens = tokens.Replace("Basic","").Trim(); if (!string.IsNullOrEmpty(tokens)) { byte[] data = Convert.FromBase64String(tokens); string decodedString = Encoding.UTF8.GetString(data); string[] tokensValues = decodedString.Split(':'); //扩展方法GetMembershipService var membershipService = request.GetMembershipService(); var membershipCtx = membershipService.ValidateUser(tokensValues[0], tokensValues[1]); if (membershipCtx.User != null) { IPrincipal principal = membershipCtx.Principal; Thread.CurrentPrincipal = principal; HttpContext.Current.User = principal; } else // Unauthorized access - wrong crededentials { var response = new HttpResponseMessage(HttpStatusCode.Unauthorized); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } } else { var response = new HttpResponseMessage(HttpStatusCode.Forbidden); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } return base.SendAsync(request, cancellationToken); } catch { var response = new HttpResponseMessage(HttpStatusCode.Forbidden); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } } } }
以上,request.GetMembershipService()方法使基于HttpRequestMessage的扩展方法,用来从依赖倒置中获取某个接口。
namespace HomeCinema.Web.Infrastructure.Extensions { public static class RequestMessageExtensions { internal static IMembershipService GetMembershipService(this HttpRequestMessage request) { return request.GetService<IMembershipService>(); } internal static IEntityBaseRepository<T> GetDataRepository<T>(this HttpRequestMessage request) where T : class, IEntityBase, new() { return request.GetService<IEntityBaseRepository<T>>(); } private static TService GetService<TService>(this HttpRequestMessage request) { IDependencyScope dependencyScope = request.GetDependencyScope(); TService service = (TService)dependencyScope.GetService(typeof(TService)); return service; } } }
最后,在WebApi.config中配置。
namespace HomeCinema.Web { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services config.MessageHandlers.Add(new HomeCinemaAuthHandler()); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
待续~