189 8069 5689

ASP.NETWebAPI单元测试-WebAPI简单介绍

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

创新互联建站是一家专业提供靖边企业网站建设,专注与网站建设、成都网站制作、H5页面制作、小程序制作等业务。10年已为靖边众多企业、政府机构等服务。创新互联专业网站建设公司优惠进行中。

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

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

  2. UnitTestingWebAPI.Data:类库(包含 Repositories)

  3. UnitTestingWebAPI.Services:类库(包含 Services)

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

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

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

Domain 层

Articles.cs

public class Article
{
    public int ID { get; set; }
    public string Title { get; set; }
    public string Contents { get; set; }
    public string Author { get; set; }
    public string URL { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateEdited { get; set; }

    public int BlogID { get; set; }
    public virtual Blog Blog { get; set; }

    public Article()
    {
    }
}

Blog.cs

public class Blog
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string URL { get; set; }
    public string Owner { get; set; }
    public DateTime DateCreated { get; set; }
    public virtual ICollection
 Articles { get; set; }     public Blog()     {         Articles = new HashSet
();     } }

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

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

ASP.NET Web API 单元测试 - Web API 简单介绍

2:命令行:

install-package EntityFramework

ASP.NET Web API 单元测试 - Web API 简单介绍

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

添加下面的类:

Configurations/ArticleConfiguration.cs

public class ArticleConfiguration : EntityTypeConfiguration
{     public ArticleConfiguration()     {         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

public class BlogConfiguration : EntityTypeConfiguration
{
    public BlogConfiguration()
    {
        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

    public class BloggerEntities : DbContext
    {
        public BloggerEntities()
            : base("BloggerEntities")
        {
            Configuration.ProxyCreationEnabled = false;
        }

        public DbSet Blogs { get; set; }
        public DbSet
 Articles { get; set; }         public virtual void Commit()         {             base.SaveChanges();         }         protected override void OnModelCreating(DbModelBuilder modelBuilder)         {             modelBuilder.Configurations.Add(new ArticleConfiguration());             modelBuilder.Configurations.Add(new BlogConfiguration());         }     }

Configurations/BloggerInitializer

public class BloggerInitializer : DropCreateDatabaseIfModelChanges
{
    protected override void Seed(BloggerEntities context)
    {
        GetBlogs().ForEach(b => context.Blogs.Add(b));

        context.Commit();
    }

    public static List GetBlogs()
    {
        List _blogs = new List();

        // Add two Blogs
        Blog _chsakellsBlog = new Blog()
        {
            Name = "chsakell's Blog",
            URL = "https://chsakell.com/",
            Owner = "Chris Sakellarios",
            Articles = GetChsakellsArticles()
        };

        Blog _dotNetCodeGeeks = new Blog()
        {
            Name = "DotNETCodeGeeks",
            URL = "dotnetcodegeeks",
            Owner = ".NET Code Geeks",
            Articles = GetDotNETGeeksArticles()
        };

        _blogs.Add(_chsakellsBlog);
        _blogs.Add(_dotNetCodeGeeks);

        return _blogs;
    }

    public static List
 GetChsakellsArticles()     {         List
 _articles = new List
();         Article _oData = new Article()         {             Author = "Chris S.",             Title = "ASP.NET Web API feat. OData",             URL = "https://chsakell.com/2015/04/04/asp-net-web-api-feat-odata/",             Contents = @"OData is an open standard protocol allowing the creation and consumption of queryable                          and interoperable RESTful APIs. It was initiated by Microsoft and it’s mostly known to                         .NET Developers from WCF Data Services. There are many other server platforms supporting                         OData services such as Node.js, PHP, Java and SQL Server Reporting Services. More over,                          Web API also supports OData and this post will show you how to integrate those two.."         };         Article _wcfCustomSecurity = new Article()         {             Author = "Chris S.",             Title = "Secure WCF Services with custom encrypted tokens",             URL = "https://chsakell.com/2014/12/13/secure-wcf-services-with-custom-encrypted-tokens/",             Contents = @"Windows Communication Foundation framework comes with a lot of options out of the box,                          concerning the security logic you will apply to your services. Different bindings can be                         used for certain kind and levels of security. Even the BasicHttpBinding binding supports                         some types of security. There are some times though where you cannot or don’t want to use                         WCF security available options and hence, you need to develop your own authentication logic                         accoarding to your business needs."         };         _articles.Add(_oData);         _articles.Add(_wcfCustomSecurity);         return _articles;     }     public static List
 GetDotNETGeeksArticles()     {         List
 _articles = new List
();         Article _angularFeatWebAPI = new Article()         {             Author = "Gordon Beeming",             Title = "AngularJS feat. Web API",             URL = "http://www.dotnetcodegeeks.com/2015/05/angularjs-feat-web-api.html",             Contents = @"Developing Web applications using AngularJS and Web API can be quite amuzing. You can pick                          this architecture in case you have in mind a web application with limitted page refreshes or                         post backs to the server while each application’s View is based on partial data retrieved from it."         };         _articles.Add(_angularFeatWebAPI);         return _articles;     }     public static List
 GetAllArticles()     {         List
 _articles = new List
();         _articles.AddRange(GetChsakellsArticles());         _articles.AddRange(GetDotNETGeeksArticles());         return _articles;     } }

Infrastructure/Disposable.cs

public class Disposable : IDisposable
{
    private bool isDisposed;

    ~Disposable()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    private void Dispose(bool disposing)
    {
        if (!isDisposed && disposing)
        {
            DisposeCore();
        }

        isDisposed = true;
    }

    // Ovveride this to dispose custom objects
    protected virtual void DisposeCore()
    {
    }
}

Infrastructure/IDbFactory.cs

public interface IDbFactory : IDisposable
{
    BloggerEntities Init();
}

Infrastructure/DbFactory.cs

public class DbFactory : Disposable, IDbFactory
{
    BloggerEntities dbContext;

    public BloggerEntities Init()
    {
        return dbContext ?? (dbContext = new BloggerEntities());
    }

    protected override void DisposeCore()
    {
        if (dbContext != null)
            dbContext.Dispose();
    }
}

Infrastrure/IRepository.cs

public interface IRepository where T : class
{
    // Marks an entity as new
    void Add(T entity);
    // Marks an entity as modified
    void Update(T entity);
    // Marks an entity to be removed
    void Delete(T entity);
    void Delete(Expression> where);
    // Get an entity by int id
    T GetById(int id);
    // Get an entity using delegate
    T Get(Expression> where);
    // Gets all entities of type T
    IEnumerable GetAll();
    // Gets entities using delegate
    IEnumerable GetMany(Expression> where);
}

Infrastructure/RepositoryBase.cs

public abstract class RepositoryBase where T : class
{
    #region Properties
    private BloggerEntities dataContext;
    private readonly IDbSet dbSet;

    protected IDbFactory DbFactory
    {
        get;
        private set;
    }

    protected BloggerEntities DbContext
    {
        get { return dataContext ?? (dataContext = DbFactory.Init()); }
    }
    #endregion

    protected RepositoryBase(IDbFactory dbFactory)
    {
        DbFactory = dbFactory;
        dbSet = DbContext.Set();
    }

    #region Implementation
    public virtual void Add(T entity)
    {
        dbSet.Add(entity);
    }

    public virtual void Update(T entity)
    {
        dbSet.Attach(entity);
        dataContext.Entry(entity).State = EntityState.Modified;
    }

    public virtual void Delete(T entity)
    {
        dbSet.Remove(entity);
    }

    public virtual void Delete(Expression> where)
    {
        IEnumerable objects = dbSet.Where(where).AsEnumerable();
        foreach (T obj in objects)
            dbSet.Remove(obj);
    }

    public virtual T GetById(int id)
    {
        return dbSet.Find(id);
    }

    public virtual IEnumerable GetAll()
    {
        return dbSet.ToList();
    }

    public virtual IEnumerable GetMany(Expression> where)
    {
        return dbSet.Where(where).ToList();
    }

    public T Get(Expression> where)
    {
        return dbSet.Where(where).FirstOrDefault();
    }

    #endregion

}


Infrastrure/IUnitOfWork.cs

public interface IUnitOfWork
{
    void Commit();
}


Infrastrure/UnitOfWork.cs

public class UnitOfWork : IUnitOfWork
{
    private readonly IDbFactory dbFactory;
    private BloggerEntities dbContext;

    public UnitOfWork(IDbFactory dbFactory)
    {
        this.dbFactory = dbFactory;
    }

    public BloggerEntities DbContext
    {
        get { return dbContext ?? (dbContext = dbFactory.Init()); }
    }

    public void Commit()
    {
        DbContext.Commit();
    }
}

Infrastructure/BlogRepository.cs

public class BlogRepository : RepositoryBase, IBlogRepository
{
    public BlogRepository(IDbFactory dbFactory)
        : base(dbFactory) { }

    public Blog GetBlogByName(string blogName)
    {
        var _blog = this.DbContext.Blogs.Where(b => b.Name == blogName).FirstOrDefault();

        return _blog;
    }
}

public interface IBlogRepository : IRepository
{
    Blog GetBlogByName(string blogName);
}

Repositories/ArticleRepository.cs

public class ArticleRepository : RepositoryBase
, IArticleRepository {     public ArticleRepository(IDbFactory dbFactory)         : base(dbFactory) { }     public Article GetArticleByTitle(string articleTitle)     {         var _article = this.DbContext.Articles.Where(b => b.Title == articleTitle).FirstOrDefault();         return _article;     } } public interface IArticleRepository : IRepository
{     Article GetArticleByTitle(string articleTitle); }

Service 层

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

ArticleService.cs

// operations you want to expose
public interface IArticleService
{
    IEnumerable
 GetArticles(string name = null);     Article GetArticle(int id);     Article GetArticle(string name);     void CreateArticle(Article article);     void UpdateArticle(Article article);     void DeleteArticle(Article article);     void SaveArticle(); } public class ArticleService : IArticleService {     private readonly IArticleRepository articlesRepository;     private readonly IUnitOfWork unitOfWork;     public ArticleService(IArticleRepository articlesRepository, IUnitOfWork unitOfWork)     {         this.articlesRepository = articlesRepository;         this.unitOfWork = unitOfWork;     }     #region IArticleService Members     public IEnumerable
 GetArticles(string title = null)     {         if (string.IsNullOrEmpty(title))             return articlesRepository.GetAll();         else             return articlesRepository.GetAll().Where(c => c.Title.ToLower().Contains(title.ToLower()));     }     public Article GetArticle(int id)     {         var article = articlesRepository.GetById(id);         return article;     }     public Article GetArticle(string title)     {         var article = articlesRepository.GetArticleByTitle(title);         return article;     }     public void CreateArticle(Article article)     {         articlesRepository.Add(article);     }     public void UpdateArticle(Article article)     {         articlesRepository.Update(article);     }     public void DeleteArticle(Article article)     {         articlesRepository.Delete(article);     }     public void SaveArticle()     {         unitOfWork.Commit();     }     #endregion }

BlogService.cs

// operations you want to expose
public interface IBlogService
{
    IEnumerable GetBlogs(string name = null);
    Blog GetBlog(int id);
    Blog GetBlog(string name);
    void CreateBlog(Blog blog);
    void UpdateBlog(Blog blog);
    void SaveBlog();
    void DeleteBlog(Blog blog);
}

public class BlogService : IBlogService
{
    private readonly IBlogRepository blogsRepository;
    private readonly IUnitOfWork unitOfWork;

    public BlogService(IBlogRepository blogsRepository, IUnitOfWork unitOfWork)
    {
        this.blogsRepository = blogsRepository;
        this.unitOfWork = unitOfWork;
    }

    #region IBlogService Members

    public IEnumerable GetBlogs(string name = null)
    {
        if (string.IsNullOrEmpty(name))
            return blogsRepository.GetAll();
        else
            return blogsRepository.GetAll().Where(c => c.Name == name);
    }

    public Blog GetBlog(int id)
    {
        var blog = blogsRepository.GetById(id);
        return blog;
    }

    public Blog GetBlog(string name)
    {
        var blog = blogsRepository.GetBlogByName(name);
        return blog;
    }

    public void CreateBlog(Blog blog)
    {
        blogsRepository.Add(blog);
    }

    public void UpdateBlog(Blog blog)
    {
        blogsRepository.Update(blog);
    }

    public void DeleteBlog(Blog blog)
    {
        blogsRepository.Delete(blog);
    }

    public void SaveBlog()
    {
        unitOfWork.Commit();
    }

    #endregion
}

Web API Core 组件

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

  1. Entity Framework

  2. Microsoft.AspNet.WebApi.Core

  3. Microsoft.AspNet.WebApi.Client

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

Controllers/ArticlesController.cs

public class ArticlesController : ApiController
{
    private IArticleService _articleService;

    public ArticlesController(IArticleService articleService)
    {
        _articleService = articleService;
    }

    // GET: api/Articles
    public IEnumerable
 GetArticles()     {         return _articleService.GetArticles();     }     // GET: api/Articles/5     [ResponseType(typeof(Article))]     public IHttpActionResult GetArticle(int id)     {         Article article = _articleService.GetArticle(id);         if (article == null)         {             return NotFound();         }         return Ok(article);     }     // PUT: api/Articles/5     [ResponseType(typeof(void))]     public IHttpActionResult PutArticle(int id, Article article)     {         if (!ModelState.IsValid)         {             return BadRequest(ModelState);         }         if (id != article.ID)         {             return BadRequest();         }         _articleService.UpdateArticle(article);         try         {             _articleService.SaveArticle();         }         catch (DbUpdateConcurrencyException)         {             if (!ArticleExists(id))             {                 return NotFound();             }             else             {                 throw;             }         }         return StatusCode(HttpStatusCode.NoContent);     }     // POST: api/Articles     [ResponseType(typeof(Article))]     public IHttpActionResult PostArticle(Article article)     {         if (!ModelState.IsValid)         {             return BadRequest(ModelState);         }         _articleService.CreateArticle(article);         return CreatedAtRoute("DefaultApi", new { id = article.ID }, article);     }     // DELETE: api/Articles/5     [ResponseType(typeof(Article))]     public IHttpActionResult DeleteArticle(int id)     {         Article article = _articleService.GetArticle(id);         if (article == null)         {             return NotFound();         }         _articleService.DeleteArticle(article);         return Ok(article);     }     private bool ArticleExists(int id)     {         return _articleService.GetArticle(id) != null;     } }

Controllers/BlogsController.cs

public class BlogsController : ApiController
{
    private IBlogService _blogService;

    public BlogsController(IBlogService blogService)
    {
        _blogService = blogService;
    }

    // GET: api/Blogs
    public IEnumerable GetBlogs()
    {
        return _blogService.GetBlogs();
    }

    // GET: api/Blogs/5
    [ResponseType(typeof(Blog))]
    public IHttpActionResult GetBlog(int id)
    {
        Blog blog = _blogService.GetBlog(id);
        if (blog == null)
        {
            return NotFound();
        }

        return Ok(blog);
    }

    // PUT: api/Blogs/5
    [ResponseType(typeof(void))]
    public IHttpActionResult PutBlog(int id, Blog blog)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != blog.ID)
        {
            return BadRequest();
        }

        _blogService.UpdateBlog(blog);

        try
        {
            _blogService.SaveBlog();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!BlogExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return StatusCode(HttpStatusCode.NoContent);
    }

    // POST: api/Blogs
    [ResponseType(typeof(Blog))]
    public IHttpActionResult PostBlog(Blog blog)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        _blogService.CreateBlog(blog);

        return CreatedAtRoute("DefaultApi", new { id = blog.ID }, blog);
    }

    // DELETE: api/Blogs/5
    [ResponseType(typeof(Blog))]
    public IHttpActionResult DeleteBlog(int id)
    {
        Blog blog = _blogService.GetBlog(id);
        if (blog == null)
        {
            return NotFound();
        }

        _blogService.DeleteBlog(blog);

        return Ok(blog);
    }

    private bool BlogExists(int id)
    {
        return _blogService.GetBlog(id) != null;
    }
}

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

Filters/ArticlesReversedFilter.cs

public class ArticlesReversedFilter : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        var objectContent = actionExecutedContext.Response.Content as ObjectContent;
        if (objectContent != null)
        {
            List
 _articles = objectContent.Value as List
;             if (_articles != null && _articles.Count > 0)             {                 _articles.Reverse();             }         }     } }

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

MediaTypeFormatters/ArticleFormatter.cs

public class ArticleFormatter : BufferedMediaTypeFormatter
{
    public ArticleFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/article"));
    }

    public override bool CanReadType(Type type)
    {
        return false;
    }

    public override bool CanWriteType(Type type)
    {
        //for single article object
        if (type == typeof(Article))
            return true;
        else
        {
            // for multiple article objects
            Type _type = typeof(IEnumerable
);             return _type.IsAssignableFrom(type);         }     }     public override void WriteToStream(Type type,                                         object value,                                         Stream writeStream,                                         HttpContent content)     {         using (StreamWriter writer = new StreamWriter(writeStream))         {             var articles = value as IEnumerable
;             if (articles != null)             {                 foreach (var article in articles)                 {                     writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",                                                 article.ID,                                                 article.Title,                                                 article.Author,                                                 article.URL,                                                 article.Contents));                 }             }             else             {                 var _article = value as Article;                 if (_article == null)                 {                     throw new InvalidOperationException("Cannot serialize type");                 }                 writer.Write(String.Format("[{0},\"{1}\",\"{2}\",\"{3}\",\"{4}\"]",                                                 _article.ID,                                                 _article.Title,                                                 _article.Author,                                                 _article.URL,                                                 _article.Contents));             }         }     } }

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

MessageHandler/HeaderAppenderHandler.cs

public class HeaderAppenderHandler : DelegatingHandler
{
    async protected override Task SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

        response.Headers.Add("X-WebAPI-Header", "Web API Unit testing in chsakell's blog.");
        return response;
    }
}

HeaderAppenderHandler/EndRequestHandler.cs

public class EndRequestHandler : DelegatingHandler
{
    async protected override Task SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.RequestUri.AbsoluteUri.Contains("test"))
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new StringContent("Unit testing message handlers!")
            };

            var tsc = new TaskCompletionSource();
            tsc.SetResult(response);
            return await tsc.Task;
        }
        else
        {
            return await base.SendAsync(request, cancellationToken);
        }
    }
}

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

CustomAssembliesResolver.cs

public class CustomAssembliesResolver : DefaultAssembliesResolver
{
    public override ICollection GetAssemblies()
    {
        var baseAssemblies = base.GetAssemblies().ToList();
        var assemblies = new List(baseAssemblies) { typeof(BlogsController).Assembly };
        baseAssemblies.AddRange(assemblies);

        return baseAssemblies.Distinct().ToList();
    }
}

Asp.NET Web Application

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

  1. Entity Framework

  2. Microsoft.AspNet.WebApi.WebHost

  3. Microsoft.AspNet.WebApi.Core

  4. Microsoft.AspNet.WebApi.Client

  5. Microsoft.AspNet.WebApi.Owin

  6. Microsoft.Owin.Host.SystemWeb

  7. Microsoft.Owin

  8. Autofac.WebApi2

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

Global.asax

protected void Application_Start()
{
    GlobalConfiguration.Configure(WebApiConfig.Register);

    // Init database
    System.Data.Entity.Database.SetInitializer(new BloggerInitializer());
}

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


  

注册外部Controller

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

Startup.cs

public class Startup
{
    public void Configuration(IAppBuilder appBuilder)
    {
        var config = new HttpConfiguration();
        config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());
        config.Formatters.Add(new ArticleFormatter());

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );


        // Autofac configuration
        var builder = new ContainerBuilder();
        builder.RegisterApiControllers(typeof(BlogsController).Assembly);
        builder.RegisterType().As().InstancePerRequest();
        builder.RegisterType().As().InstancePerRequest();

        //Repositories
        builder.RegisterAssemblyTypes(typeof(BlogRepository).Assembly)
            .Where(t => t.Name.EndsWith("Repository"))
            .AsImplementedInterfaces().InstancePerRequest();
        // Services
        builder.RegisterAssemblyTypes(typeof(ArticleService).Assembly)
            .Where(t => t.Name.EndsWith("Service"))
            .AsImplementedInterfaces().InstancePerRequest();

        IContainer container = builder.Build();
        config.DependencyResolver = new AutofacWebApiDependencyResolver(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部分介绍的差不多了,有问题请留言.


当前名称:ASP.NETWebAPI单元测试-WebAPI简单介绍
当前链接:http://cdxtjz.cn/article/jspjih.html

其他资讯