如果你对Web API已比较了解则可以跳过本篇直接看单元测试部分。


创建一个叫 UnitTesingWebAPI的空白的解决方案,并包含下列项目:

UnitTestingWebAPI.Domain:类库(包含Entity Models)

UnitTestingWebAPI.Data:类库(包含Repositories)

UnitTestingWebAPI.Services:类库(包含Services)

UnitTestingWebAPI.API.Core:类库(包含WebAPI组件,例如:Controllers, Filters, Massage Handlers)

UnitTestingWebAPI.API:空的ASP.NET Web Application(Web程序去管控(host) WebAPI

UnitTestingWebAPI.Tests:类库(包含单元测试)


Domain 层:

Articles.cs

publicclassArticle{publicintID{get;set;}publicstringTitle{get;set;}publicstringContents{get;set;}publicstringAuthor{get;set;}publicstringURL{get;set;}publicDateTimeDateCreated{get;set;}publicDateTimeDateEdited{get;set;}publicintBlogID{get;set;}publicvirtualBlogBlog{get;set;}publicArticle(){}}

Blog.cs

publicclassBlog{publicintID{get;set;}publicstringName{get;set;}publicstringURL{get;set;}publicstringOwner{get;set;}publicDateTimeDateCreated{get;set;}publicvirtualICollection<Article>Articles{get;set;}publicBlog(){Articles=newHashSet<Article>();}}


Respository 层:
为UnitTestingWebAPI.Data安装Entity Framework,有两种方式:

1:在UnitTestingWebAPI.Data上右键,点击管理Nuget包,选择对话窗口的左边选择在线包,找到EF进行安装

2:命令行:

install-packageEntityFramework

注:一定要注意我用红色圈住的地方。


添加下面的类:

Configurations/ArticleConfiguration.cs

publicclassArticleConfiguration:EntityTypeConfiguration<Article>{publicArticleConfiguration(){ToTable("Article");Property(a=>a.Title).IsRequired().HasMaxLength(100);Property(a=>a.Contents).IsRequired();Property(a=>a.Author).IsRequired().HasMaxLength(50);Property(a=>a.URL).IsRequired().HasMaxLength(200);Property(a=>a.DateCreated).HasColumnType("datetime2");Property(a=>a.DateEdited).HasColumnType("datetime2");}}

Configurations/BlogConfiguration.cs

publicclassBlogConfiguration:EntityTypeConfiguration<Blog>{publicBlogConfiguration(){ToTable("Blog");Property(b=>b.Name).IsRequired().HasMaxLength(100);Property(b=>b.URL).IsRequired().HasMaxLength(200);Property(b=>b.Owner).IsRequired().HasMaxLength(50);Property(b=>b.DateCreated).HasColumnType("datetime2");}}

Configurations/BloggerEntities

publicclassBloggerEntities:DbContext{publicBloggerEntities():base("BloggerEntities"){Configuration.ProxyCreationEnabled=false;}publicDbSet<Blog>Blogs{get;set;}publicDbSet<Article>Articles{get;set;}publicvirtualvoidCommit(){base.SaveChanges();}protectedoverridevoidOnModelCreating(DbModelBuildermodelBuilder){modelBuilder.Configurations.Add(newArticleConfiguration());modelBuilder.Configurations.Add(newBlogConfiguration());}}

Configurations/BloggerInitializer

publicclassBloggerInitializer:DropCreateDatabaseIfModelChanges<BloggerEntities>{protectedoverridevoidSeed(BloggerEntitiescontext){GetBlogs().ForEach(b=>context.Blogs.Add(b));context.Commit();}publicstaticList<Blog>GetBlogs(){List<Blog>_blogs=newList<Blog>();//AddtwoBlogsBlog_chsakellsBlog=newBlog(){Name="chsakell'sBlog",URL="https://chsakell.com/",Owner="ChrisSakellarios",Articles=GetChsakellsArticles()};Blog_dotNetCodeGeeks=newBlog(){Name="DotNETCodeGeeks",URL="dotnetcodegeeks",Owner=".NETCodeGeeks",Articles=GetDotNETGeeksArticles()};_blogs.Add(_chsakellsBlog);_blogs.Add(_dotNetCodeGeeks);return_blogs;}publicstaticList<Article>GetChsakellsArticles(){List<Article>_articles=newList<Article>();Article_oData=newArticle(){Author="ChrisS.",Title="ASP.NETWebAPIfeat.OData",URL="https://chsakell.com/2015/04/04/asp-net-web-api-feat-odata/",Contents=@"ODataisanopenstandardprotocolallowingthecreationandconsumptionofqueryableandinteroperableRESTfulAPIs.ItwasinitiatedbyMicrosoftandit’smostlyknownto.NETDevelopersfromWCFDataServices.TherearemanyotherserverplatformssupportingODataservicessuchasNode.js,PHP,JavaandSQLServerReportingServices.Moreover,WebAPIalsosupportsODataandthispostwillshowyouhowtointegratethosetwo.."};Article_wcfCustomSecurity=newArticle(){Author="ChrisS.",Title="SecureWCFServiceswithcustomencryptedtokens",URL="https://chsakell.com/2014/12/13/secure-wcf-services-with-custom-encrypted-tokens/",Contents=@"WindowsCommunicationFoundationframeworkcomeswithalotofoptionsoutofthebox,concerningthesecuritylogicyouwillapplytoyourservices.Differentbindingscanbeusedforcertainkindandlevelsofsecurity.EventheBasicHttpBindingbindingsupportssometypesofsecurity.Therearesometimesthoughwhereyoucannotordon’twanttouseWCFsecurityavailableoptionsandhence,youneedtodevelopyourownauthenticationlogicaccoardingtoyourbusinessneeds."};_articles.Add(_oData);_articles.Add(_wcfCustomSecurity);return_articles;}publicstaticList<Article>GetDotNETGeeksArticles(){List<Article>_articles=newList<Article>();Article_angularFeatWebAPI=newArticle(){Author="GordonBeeming",Title="AngularJSfeat.WebAPI",URL="http://www.dotnetcodegeeks.com/2015/05/angularjs-feat-web-api.html",Contents=@"DevelopingWebapplicationsusingAngularJSandWebAPIcanbequiteamuzing.Youcanpickthisarchitectureincaseyouhaveinmindawebapplicationwithlimittedpagerefreshesorpostbackstotheserverwhileeachapplication’sViewisbasedonpartialdataretrievedfromit."};_articles.Add(_angularFeatWebAPI);return_articles;}publicstaticList<Article>GetAllArticles(){List<Article>_articles=newList<Article>();_articles.AddRange(GetChsakellsArticles());_articles.AddRange(GetDotNETGeeksArticles());return_articles;}}


Infrastructure/Disposable.cs

publicclassDisposable:IDisposable{privateboolisDisposed;~Disposable(){Dispose(false);}publicvoidDispose(){Dispose(true);GC.SuppressFinalize(this);}privatevoidDispose(booldisposing){if(!isDisposed&&disposing){DisposeCore();}isDisposed=true;}//OvveridethistodisposecustomobjectsprotectedvirtualvoidDisposeCore(){}}

Infrastructure/IDbFactory.cs

publicinterfaceIDbFactory:IDisposable{BloggerEntitiesInit();}

Infrastructure/DbFactory.cs

publicclassDbFactory:Disposable,IDbFactory{BloggerEntitiesdbContext;publicBloggerEntitiesInit(){returndbContext??(dbContext=newBloggerEntities());}protectedoverridevoidDisposeCore(){if(dbContext!=null)dbContext.Dispose();}}

Infrastrure/IRepository.cs

publicinterfaceIRepository<T>whereT:class{//MarksanentityasnewvoidAdd(Tentity);//MarksanentityasmodifiedvoidUpdate(Tentity);//MarksanentitytoberemovedvoidDelete(Tentity);voidDelete(Expression<Func<T,bool>>where);//GetanentitybyintidTGetById(intid);//GetanentityusingdelegateTGet(Expression<Func<T,bool>>where);//GetsallentitiesoftypeTIEnumerable<T>GetAll();//GetsentitiesusingdelegateIEnumerable<T>GetMany(Expression<Func<T,bool>>where);}

Infrastructure/RepositoryBase.cs

publicabstractclassRepositoryBase<T>whereT:class{#regionPropertiesprivateBloggerEntitiesdataContext;privatereadonlyIDbSet<T>dbSet;protectedIDbFactoryDbFactory{get;privateset;}protectedBloggerEntitiesDbContext{get{returndataContext??(dataContext=DbFactory.Init());}}#endregionprotectedRepositoryBase(IDbFactorydbFactory){DbFactory=dbFactory;dbSet=DbContext.Set<T>();}#regionImplementationpublicvirtualvoidAdd(Tentity){dbSet.Add(entity);}publicvirtualvoidUpdate(Tentity){dbSet.Attach(entity);dataContext.Entry(entity).State=EntityState.Modified;}publicvirtualvoidDelete(Tentity){dbSet.Remove(entity);}publicvirtualvoidDelete(Expression<Func<T,bool>>where){IEnumerable<T>objects=dbSet.Where<T>(where).AsEnumerable();foreach(Tobjinobjects)dbSet.Remove(obj);}publicvirtualTGetById(intid){returndbSet.Find(id);}publicvirtualIEnumerable<T>GetAll(){returndbSet.ToList();}publicvirtualIEnumerable<T>GetMany(Expression<Func<T,bool>>where){returndbSet.Where(where).ToList();}publicTGet(Expression<Func<T,bool>>where){returndbSet.Where(where).FirstOrDefault<T>();}#endregion}


Infrastrure/IUnitOfWork.cs

publicinterfaceIUnitOfWork{voidCommit();}


Infrastrure/UnitOfWork.cs

publicclassUnitOfWork:IUnitOfWork{privatereadonlyIDbFactorydbFactory;privateBloggerEntitiesdbContext;publicUnitOfWork(IDbFactorydbFactory){this.dbFactory=dbFactory;}publicBloggerEntitiesDbContext{get{returndbContext??(dbContext=dbFactory.Init());}}publicvoidCommit(){DbContext.Commit();}}

Infrastructure/BlogRepository.cs

publicclassBlogRepository:RepositoryBase<Blog>,IBlogRepository{publicBlogRepository(IDbFactorydbFactory):base(dbFactory){}publicBlogGetBlogByName(stringblogName){var_blog=this.DbContext.Blogs.Where(b=>b.Name==blogName).FirstOrDefault();return_blog;}}publicinterfaceIBlogRepository:IRepository<Blog>{BlogGetBlogByName(stringblogName);}


Repositories/ArticleRepository.cs

publicclassArticleRepository:RepositoryBase<Article>,IArticleRepository{publicArticleRepository(IDbFactorydbFactory):base(dbFactory){}publicArticleGetArticleByTitle(stringarticleTitle){var_article=this.DbContext.Articles.Where(b=>b.Title==articleTitle).FirstOrDefault();return_article;}}publicinterfaceIArticleRepository:IRepository<Article>{ArticleGetArticleByTitle(stringarticleTitle);}


Service 层

到UnitTestingWebAPI.Service项目上,添加对UnitTestingWebAPI.Domain, UnitTestingWebAPI.Data的引用,并添加下列文件:

ArticleService.cs

//operationsyouwanttoexposepublicinterfaceIArticleService{IEnumerable<Article>GetArticles(stringname=null);ArticleGetArticle(intid);ArticleGetArticle(stringname);voidCreateArticle(Articlearticle);voidUpdateArticle(Articlearticle);voidDeleteArticle(Articlearticle);voidSaveArticle();}publicclassArticleService:IArticleService{privatereadonlyIArticleRepositoryarticlesRepository;privatereadonlyIUnitOfWorkunitOfWork;publicArticleService(IArticleRepositoryarticlesRepository,IUnitOfWorkunitOfWork){this.articlesRepository=articlesRepository;this.unitOfWork=unitOfWork;}#regionIArticleServiceMemberspublicIEnumerable<Article>GetArticles(stringtitle=null){if(string.IsNullOrEmpty(title))returnarticlesRepository.GetAll();elsereturnarticlesRepository.GetAll().Where(c=>c.Title.ToLower().Contains(title.ToLower()));}publicArticleGetArticle(intid){vararticle=articlesRepository.GetById(id);returnarticle;}publicArticleGetArticle(stringtitle){vararticle=articlesRepository.GetArticleByTitle(title);returnarticle;}publicvoidCreateArticle(Articlearticle){articlesRepository.Add(article);}publicvoidUpdateArticle(Articlearticle){articlesRepository.Update(article);}publicvoidDeleteArticle(Articlearticle){articlesRepository.Delete(article);}publicvoidSaveArticle(){unitOfWork.Commit();}#endregion}

BlogService.cs

//operationsyouwanttoexposepublicinterfaceIBlogService{IEnumerable<Blog>GetBlogs(stringname=null);BlogGetBlog(intid);BlogGetBlog(stringname);voidCreateBlog(Blogblog);voidUpdateBlog(Blogblog);voidSaveBlog();voidDeleteBlog(Blogblog);}publicclassBlogService:IBlogService{privatereadonlyIBlogRepositoryblogsRepository;privatereadonlyIUnitOfWorkunitOfWork;publicBlogService(IBlogRepositoryblogsRepository,IUnitOfWorkunitOfWork){this.blogsRepository=blogsRepository;this.unitOfWork=unitOfWork;}#regionIBlogServiceMemberspublicIEnumerable<Blog>GetBlogs(stringname=null){if(string.IsNullOrEmpty(name))returnblogsRepository.GetAll();elsereturnblogsRepository.GetAll().Where(c=>c.Name==name);}publicBlogGetBlog(intid){varblog=blogsRepository.GetById(id);returnblog;}publicBlogGetBlog(stringname){varblog=blogsRepository.GetBlogByName(name);returnblog;}publicvoidCreateBlog(Blogblog){blogsRepository.Add(blog);}publicvoidUpdateBlog(Blogblog){blogsRepository.Update(blog);}publicvoidDeleteBlog(Blogblog){blogsRepository.Delete(blog);}publicvoidSaveBlog(){unitOfWork.Commit();}#endregion}


Web API Core 组件

在UnitTestingWebAPI.API.Core 上添加 UnitTestingWebAPI.API.Domain 和UnitTestingWebAPI.Service 项目,并安装下面的包(方法和前面Resporities层一样):

Entity Framework

Microsoft.AspNet.WebApi.Core

Microsoft.AspNet.WebApi.Client


添加下面的Web API Controller到 Controller 文件夹中:

Controllers/ArticlesController.cs

publicclassArticlesController:ApiController{privateIArticleService_articleService;publicArticlesController(IArticleServicearticleService){_articleService=articleService;}//GET:api/ArticlespublicIEnumerable<Article>GetArticles(){return_articleService.GetArticles();}//GET:api/Articles/5[ResponseType(typeof(Article))]publicIHttpActionResultGetArticle(intid){Articlearticle=_articleService.GetArticle(id);if(article==null){returnNotFound();}returnOk(article);}//PUT:api/Articles/5[ResponseType(typeof(void))]publicIHttpActionResultPutArticle(intid,Articlearticle){if(!ModelState.IsValid){returnBadRequest(ModelState);}if(id!=article.ID){returnBadRequest();}_articleService.UpdateArticle(article);try{_articleService.SaveArticle();}catch(DbUpdateConcurrencyException){if(!ArticleExists(id)){returnNotFound();}else{throw;}}returnStatusCode(HttpStatusCode.NoContent);}//POST:api/Articles[ResponseType(typeof(Article))]publicIHttpActionResultPostArticle(Articlearticle){if(!ModelState.IsValid){returnBadRequest(ModelState);}_articleService.CreateArticle(article);returnCreatedAtRoute("DefaultApi",new{id=article.ID},article);}//DELETE:api/Articles/5[ResponseType(typeof(Article))]publicIHttpActionResultDeleteArticle(intid){Articlearticle=_articleService.GetArticle(id);if(article==null){returnNotFound();}_articleService.DeleteArticle(article);returnOk(article);}privateboolArticleExists(intid){return_articleService.GetArticle(id)!=null;}}

Controllers/BlogsController.cs

publicclassBlogsController:ApiController{privateIBlogService_blogService;publicBlogsController(IBlogServiceblogService){_blogService=blogService;}//GET:api/BlogspublicIEnumerable<Blog>GetBlogs(){return_blogService.GetBlogs();}//GET:api/Blogs/5[ResponseType(typeof(Blog))]publicIHttpActionResultGetBlog(intid){Blogblog=_blogService.GetBlog(id);if(blog==null){returnNotFound();}returnOk(blog);}//PUT:api/Blogs/5[ResponseType(typeof(void))]publicIHttpActionResultPutBlog(intid,Blogblog){if(!ModelState.IsValid){returnBadRequest(ModelState);}if(id!=blog.ID){returnBadRequest();}_blogService.UpdateBlog(blog);try{_blogService.SaveBlog();}catch(DbUpdateConcurrencyException){if(!BlogExists(id)){returnNotFound();}else{throw;}}returnStatusCode(HttpStatusCode.NoContent);}//POST:api/Blogs[ResponseType(typeof(Blog))]publicIHttpActionResultPostBlog(Blogblog){if(!ModelState.IsValid){returnBadRequest(ModelState);}_blogService.CreateBlog(blog);returnCreatedAtRoute("DefaultApi",new{id=blog.ID},blog);}//DELETE:api/Blogs/5[ResponseType(typeof(Blog))]publicIHttpActionResultDeleteBlog(intid){Blogblog=_blogService.GetBlog(id);if(blog==null){returnNotFound();}_blogService.DeleteBlog(blog);returnOk(blog);}privateboolBlogExists(intid){return_blogService.GetBlog(id)!=null;}}


在有需要时添加下面的过滤器,它会反转Articles list的顺序:

Filters/ArticlesReversedFilter.cs

publicclassArticlesReversedFilter:ActionFilterAttribute{publicoverridevoidOnActionExecuted(HttpActionExecutedContextactionExecutedContext){varobjectContent=actionExecutedContext.Response.ContentasObjectContent;if(objectContent!=null){List<Article>_articles=objectContent.ValueasList<Article>;if(_articles!=null&&_articles.Count>0){_articles.Reverse();}}}}


当添加下面的媒体类型格式化器,可以返回一个用逗号分割来展示的文章列表:

MediaTypeFormatters/ArticleFormatter.cs

publicclassArticleFormatter:BufferedMediaTypeFormatter{publicArticleFormatter(){SupportedMediaTypes.Add(newMediaTypeHeaderValue("application/article"));}publicoverrideboolCanReadType(Typetype){returnfalse;}publicoverrideboolCanWriteType(Typetype){//forsinglearticleobjectif(type==typeof(Article))returntrue;else{//formultiplearticleobjectsType_type=typeof(IEnumerable<Article>);return_type.IsAssignableFrom(type);}}publicoverridevoidWriteToStream(Typetype,objectvalue,StreamwriteStream,HttpContentcontent){using(StreamWriterwriter=newStreamWriter(writeStream)){vararticles=valueasIEnumerable<Article>;if(articles!=null){foreach(vararticleinarticles){writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",article.ID,article.Title,article.Author,article.URL,article.Contents));}}else{var_article=valueasArticle;if(_article==null){thrownewInvalidOperationException("Cannotserializetype");}writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",_article.ID,_article.Title,_article.Author,_article.URL,_article.Contents));}}}}


添加下面两个 消息 处理器,第一个负责Response中添加定制 header,第二个可以决定这个请求是否被接受:

MessageHandler/HeaderAppenderHandler.cs

publicclassHeaderAppenderHandler:DelegatingHandler{asyncprotectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){HttpResponseMessageresponse=awaitbase.SendAsync(request,cancellationToken);response.Headers.Add("X-WebAPI-Header","WebAPIUnittestinginchsakell'sblog.");returnresponse;}}


HeaderAppenderHandler/EndRequestHandler.cs

publicclassEndRequestHandler:DelegatingHandler{asyncprotectedoverrideTask<HttpResponseMessage>SendAsync(HttpRequestMessagerequest,CancellationTokencancellationToken){if(request.RequestUri.AbsoluteUri.Contains("test")){varresponse=newHttpResponseMessage(HttpStatusCode.OK){Content=newStringContent("Unittestingmessagehandlers!")};vartsc=newTaskCompletionSource<HttpResponseMessage>();tsc.SetResult(response);returnawaittsc.Task;}else{returnawaitbase.SendAsync(request,cancellationToken);}}}


添加下面被用于从Web 应用程序中注册Controller 的 DefaultAssembliesResolver

CustomAssembliesResolver.cs

publicclassCustomAssembliesResolver:DefaultAssembliesResolver{publicoverrideICollection<Assembly>GetAssemblies(){varbaseAssemblies=base.GetAssemblies().ToList();varassemblies=newList<Assembly>(baseAssemblies){typeof(BlogsController).Assembly};baseAssemblies.AddRange(assemblies);returnbaseAssemblies.Distinct().ToList();}}


Asp.NET Web Application

添加 UnitTestingWebAPI.API Web应用项目,并且添加引用 UnitTestingWebAPI.Core, UnitTestingWebAPI.Data 和 UnitTestingWebAPI.Service,同样需要安装下列组件包:

Entity Framework

Microsoft.AspNet.WebApi.WebHost

Microsoft.AspNet.WebApi.Core

Microsoft.AspNet.WebApi.Client

Microsoft.AspNet.WebApi.Owin

Microsoft.Owin.Host.SystemWeb

Microsoft.Owin

Autofac.WebApi2


在Global配置文件(如果没有就新增一个)中配置初始化数据库配置

Global.asax

protectedvoidApplication_Start(){GlobalConfiguration.Configure(WebApiConfig.Register);//InitdatabaseSystem.Data.Entity.Database.SetInitializer(newBloggerInitializer());}

同样要记得添加一个相关的Connection String在Web.config文件中.

<connectionStrings><addname="BloggerDbConnStr"connectionString="DataSource=(localdb)\v11.0;InitialCatalog=BloggerDB;IntegratedSecurity=True"providerName="System.Data.SqlClient"/></connectionStrings>

注册外部Controller

在Web Application的根目录创建一个 Owin Startup.cs 文件并且粘贴下面的代码,在(autofac configration)需要的时候,这部分代码会确保 UnitTestingWebAPI.API.Core(CustomAssembliesResolver)项目正确使用WebApi Controller和注入合适的仓库以及服务.


Startup.cs

publicclassStartup{publicvoidConfiguration(IAppBuilderappBuilder){varconfig=newHttpConfiguration();config.Services.Replace(typeof(IAssembliesResolver),newCustomAssembliesResolver());config.Formatters.Add(newArticleFormatter());config.Routes.MapHttpRoute(name:"DefaultApi",routeTemplate:"api/{controller}/{id}",defaults:new{id=RouteParameter.Optional});//Autofacconfigurationvarbuilder=newContainerBuilder();builder.RegisterApiControllers(typeof(BlogsController).Assembly);builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerRequest();//Repositoriesbuilder.RegisterAssemblyTypes(typeof(BlogRepository).Assembly).Where(t=>t.Name.EndsWith("Repository")).AsImplementedInterfaces().InstancePerRequest();//Servicesbuilder.RegisterAssemblyTypes(typeof(ArticleService).Assembly).Where(t=>t.Name.EndsWith("Service")).AsImplementedInterfaces().InstancePerRequest();IContainercontainer=builder.Build();config.DependencyResolver=newAutofacWebApiDependencyResolver(container);appBuilder.UseWebApi(config);}}

在这个时候,你应该可以启动Web应用程序并且使用下面的请求来获取article或blogs(这里的端口可能不一致):

http://localhost:57414/api/Articles

http://localhost:57414/api/Blogs


同样附上原文:chsakell's Blog


文章中的源码:http://down.51cto.com/data/2243634


到此为止,WebAPI部分介绍的差不多了,有问题请留言.