第一天.建立Web Api
REST
Restful實作的對象包括:Http,URI,JSON,XML
建立資料庫
與作者的SQL檔有些不同處在於,我將登入者改為SQL使用者,並只給予「讀、寫」的權限
- 帳 : akhil
- 密 : 123456
- 權限列表:
- db_datareader
- db_datawriter
--
資料庫包含了三張資料表:
- Products
- Tokens
- User
Products
是用來示範CRUD用的,而Tokens
及User
則會再後續「驗證權限」時介紹。
USE [WebApiDb]
GO
/****** Object: User [akhil] Script Date: 05/07/2015 12:13:46 ******/
USE [master]
GO
CREATE LOGIN [akhil] WITH PASSWORD=N'123456', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
GO
USE [WebApiDb]
GO
CREATE USER [akhil] FOR LOGIN [akhil]
GO
USE [WebApiDb]
GO
ALTER ROLE [db_datareader] ADD MEMBER [akhil]
GO
USE [WebApiDb]
GO
ALTER ROLE [db_datawriter] ADD MEMBER [akhil]
GO
/****** Object: Table [dbo].[Products] Script Date: 05/07/2015 12:13:46 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Products](
[ProductId] [int] IDENTITY(1,1) NOT NULL,
[ProductName] [varchar](50) NOT NULL,
CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED
(
[ProductId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
SET IDENTITY_INSERT [dbo].[Products] ON
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (1, N'Laptop')
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (2, N'Mobile')
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (3, N'IPad')
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (4, N'IPhone')
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (5, N'Bag')
INSERT [dbo].[Products] ([ProductId], [ProductName]) VALUES (6, N'Watch')
SET IDENTITY_INSERT [dbo].[Products] OFF
/****** Object: Table [dbo].[User] Script Date: 05/07/2015 12:13:46 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[User](
[UserId] [int] IDENTITY(1,1) NOT NULL,
[UserName] [nvarchar](50) NOT NULL,
[Password] [nvarchar](50) NOT NULL,
[Name] [nvarchar](50) NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[User] ON
INSERT [dbo].[User] ([UserId], [UserName], [Password], [Name]) VALUES (1, N'akhil', N'akhil', N'Akhil Mittal')
INSERT [dbo].[User] ([UserId], [UserName], [Password], [Name]) VALUES (2, N'api', N'api', N'API User')
SET IDENTITY_INSERT [dbo].[User] OFF
/****** Object: Table [dbo].[Tokens] Script Date: 05/07/2015 12:13:46 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Tokens](
[TokenId] [int] IDENTITY(1,1) NOT NULL,
[UserId] [int] NOT NULL,
[AuthToken] [nvarchar](250) NOT NULL,
[IssuedOn] [datetime] NOT NULL,
[ExpiresOn] [datetime] NOT NULL,
CONSTRAINT [PK_Tokens] PRIMARY KEY CLUSTERED
(
[TokenId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object: ForeignKey [FK_Tokens_User] Script Date: 05/07/2015 12:13:46 ******/
ALTER TABLE [dbo].[Tokens] WITH CHECK ADD CONSTRAINT [FK_Tokens_User] FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([UserId])
GO
ALTER TABLE [dbo].[Tokens] CHECK CONSTRAINT [FK_Tokens_User]
GO
建立資料存取層(Data Access Layer)
隔離外部與DB間實體物件的溝通,一律透過資料存取層,不管是Asp.Net Mvc, Web Api,WCF都能夠取得,而不需要重新撰寫相同的邏輯。
DataModel
額外建立一個類別庫,名稱為:DataModel。
使用DB First的方式開發
建立edmx
- 名稱:WebApiDataModel
- 選擇Db First的方式
- 連線字串:WebApiDbEntities
- 選擇Entity Framework 6.x
- 加入所有的[資料表]
建立Repository 及 Unit Of work
Repository的用處:
- 與DB溝通
- 透過IDisposable自動釋放[連線]及[物件]
Repository的好處:
- 集中[資料邏輯]或[Web Service 存取邏輯]
- 提供單元測試的注入點
- 提高架構在演進的彈性
--
在DataModel層增加GenericRepository
GenericRepository.cs
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataModel
{
/// <summary>
/// Generic Repository class for Entity Operations
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public class GenericRepository<TEntity> where TEntity : class
{
#region Private member variables...
internal WebApiDbEntities Context;
internal DbSet<TEntity> DbSet;
#endregion
#region Public Constructor...
/// <summary>
/// Public Constructor,initializes privately declared local variables.
/// </summary>
/// <param name="context"></param>
public GenericRepository(WebApiDbEntities context)
{
this.Context = context;
this.DbSet = context.Set<TEntity>();
}
#endregion
#region Public member methods...
/// <summary>
/// generic Get method for Entities
/// </summary>
/// <returns></returns>
public virtual IEnumerable<TEntity> Get()
{
IQueryable<TEntity> query = DbSet;
return query.ToList();
}
/// <summary>
/// Generic get method on the basis of id for Entities.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public virtual TEntity GetByID(object id)
{
return DbSet.Find(id);
}
/// <summary>
/// generic Insert method for the entities
/// </summary>
/// <param name="entity"></param>
public virtual void Insert(TEntity entity)
{
DbSet.Add(entity);
}
/// <summary>
/// Generic Delete method for the entities
/// </summary>
/// <param name="id"></param>
public virtual void Delete(object id)
{
TEntity entityToDelete = DbSet.Find(id);
Delete(entityToDelete);
}
/// <summary>
/// Generic Delete method for the entities
/// </summary>
/// <param name="entityToDelete"></param>
public virtual void Delete(TEntity entityToDelete)
{
if (Context.Entry(entityToDelete).State == EntityState.Detached)
{
DbSet.Attach(entityToDelete);
}
DbSet.Remove(entityToDelete);
}
/// <summary>
/// Generic update method for the entities
/// </summary>
/// <param name="entityToUpdate"></param>
public virtual void Update(TEntity entityToUpdate)
{
DbSet.Attach(entityToUpdate);
Context.Entry(entityToUpdate).State = EntityState.Modified;
}
/// <summary>
/// generic method to get many record on the basis of a condition.
/// </summary>
/// <param name="where"></param>
/// <returns></returns>
public virtual IEnumerable<TEntity> GetMany(Func<TEntity, bool> where)
{
return DbSet.Where(where).ToList();
}
/// <summary>
/// generic method to get many record on the basis of a condition but query able.
/// </summary>
/// <param name="where"></param>
/// <returns></returns>
public virtual IQueryable<TEntity> GetManyQueryable(Func<TEntity, bool> where)
{
return DbSet.Where(where).AsQueryable();
}
/// <summary>
/// generic get method , fetches data for the entities on the basis of condition.
/// </summary>
/// <param name="where"></param>
/// <returns></returns>
public TEntity Get(Func<TEntity, Boolean> where)
{
return DbSet.Where(where).FirstOrDefault<TEntity>();
}
/// <summary>
/// generic delete method , deletes data for the entities on the basis of condition.
/// </summary>
/// <param name="where"></param>
/// <returns></returns>
public void Delete(Func<TEntity, Boolean> where)
{
IQueryable<TEntity> objects = DbSet.Where<TEntity>(where).AsQueryable();
foreach (TEntity obj in objects)
DbSet.Remove(obj);
}
/// <summary>
/// generic method to fetch all the records from db
/// </summary>
/// <returns></returns>
public virtual IEnumerable<TEntity> GetAll()
{
return DbSet.ToList();
}
/// <summary>
/// Inclue multiple
/// </summary>
/// <param name="predicate"></param>
/// <param name="include"></param>
/// <returns></returns>
public IQueryable<TEntity> GetWithInclude(
System.Linq.Expressions.Expression<Func<TEntity,
bool>> predicate, params string[] include)
{
IQueryable<TEntity> query = this.DbSet;
query = include.Aggregate(query, (current, inc) => current.Include(inc));
return query.Where(predicate);
}
/// <summary>
/// Generic method to check if entity exists
/// </summary>
/// <param name="primaryKey"></param>
/// <returns></returns>
public bool Exists(object primaryKey)
{
return DbSet.Find(primaryKey) != null;
}
/// <summary>
/// Gets a single record by the specified criteria (usually the unique identifier)
/// </summary>
/// <param name="predicate">Criteria to match on</param>
/// <returns>A single record that matches the specified criteria</returns>
public TEntity GetSingle(Func<TEntity, bool> predicate)
{
return DbSet.Single<TEntity>(predicate);
}
/// <summary>
/// The first record matching the specified criteria
/// </summary>
/// <param name="predicate">Criteria to match on</param>
/// <returns>A single record containing the first record matching the specified criteria</returns>
public TEntity GetFirst(Func<TEntity, bool> predicate)
{
return DbSet.First<TEntity>(predicate);
}
#endregion
}
}
Unit of Work的用處:
- 將DB內的資料表,都用Repository的方式存在此類別內
- 建立Save的方法
- 透過IDisposable自動釋放[連線]及[物件]
Unit of Work的好處:
- 管理Transcation
- 管理Db的「Select , Update , Insert , Delete」
- 避免重覆Update
在DataModel層增加UnitOfWork
UnitOfWork.cs
using DataModel;
using System;
using System.Collections.Generic;
using System.Data.Entity.Validation;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataModel
{
/// <summary>
/// Unit of Work class responsible for DB transactions
/// </summary>
public class UnitOfWork : IDisposable
{
#region Private member variables...
private WebApiDbEntities _context = null;
private GenericRepository<User> _userRepository;
private GenericRepository<Products> _productRepository;
private GenericRepository<Tokens> _tokenRepository;
#endregion
public UnitOfWork()
{
_context = new WebApiDbEntities();
}
#region Public Repository Creation properties...
/// <summary>
/// Get/Set Property for product repository.
/// </summary>
public GenericRepository<Products> ProductRepository
{
get
{
if (this._productRepository == null)
this._productRepository = new GenericRepository<Products>(_context);
return _productRepository;
}
}
/// <summary>
/// Get/Set Property for user repository.
/// </summary>
public GenericRepository<User> UserRepository
{
get
{
if (this._userRepository == null)
this._userRepository = new GenericRepository<User>(_context);
return _userRepository;
}
}
/// <summary>
/// Get/Set Property for token repository.
/// </summary>
public GenericRepository<Tokens> TokenRepository
{
get
{
if (this._tokenRepository == null)
this._tokenRepository = new GenericRepository<Tokens>(_context);
return _tokenRepository;
}
}
#endregion
#region Public member methods...
/// <summary>
/// Save method.
/// </summary>
public void Save()
{
try
{
_context.SaveChanges();
}
catch (DbEntityValidationException e)
{
var outputLines = new List<string>();
foreach (var eve in e.EntityValidationErrors)
{
outputLines.Add(string.Format(
"{0}: Entity of type \"{1}\" in state \"{2}\" has the following validation errors:", DateTime.Now,
eve.Entry.Entity.GetType().Name, eve.Entry.State));
foreach (var ve in eve.ValidationErrors)
{
outputLines.Add(string.Format("- Property: \"{0}\", Error: \"{1}\"", ve.PropertyName, ve.ErrorMessage));
}
}
System.IO.File.AppendAllLines(@"C:\errors.txt", outputLines);
throw e;
}
}
#endregion
#region Implementing IDiosposable...
#region private dispose variable declaration...
private bool disposed = false;
#endregion
/// <summary>
/// Protected Virtual Dispose method
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
Debug.WriteLine("UnitOfWork is being disposed");
_context.Dispose();
}
}
this.disposed = true;
}
/// <summary>
/// Dispose method
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
BusinessEntities
額外建立一個類別庫,名稱為:BusinessEntities。
定義共用的資料結構讓外部來存取DataModel的資料
ProductEntity
public class ProductEntity
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
--
TokenEntity
public class TokenEntity
{
public int TokenId { get; set; }
public int UserId { get; set; }
public string AuthToken { get; set; }
public System.DateTime IssuedOn { get; set; }
public System.DateTime ExpiresOn { get; set; }
}
--
UserEntity
public class UserEntity
{
public int UserId { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
BusinessServices
額外建立一個類別庫,名稱為:BusinessServices。
主要用途:用來存取DB(新增、刪除、修改、查詢)
IProductServices.cs
方便後續的單元測試,先建立產品的介面並定義CRUD會用到的方法。
using BusinessEntities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BusinessServices
{
/// <summary>
/// Product Service Contract
/// </summary>
public interface IProductServices
{
ProductEntity GetProductById(int productId);
IEnumerable<ProductEntity> GetAllProducts();
int CreateProduct(ProductEntity productEntity);
bool UpdateProduct(int productId, ProductEntity productEntity);
bool DeleteProduct(int productId);
}
}
實作產品的CRUD方法,並套過AutoMapper套件將資料map至ProductEntities裡。
Nuget
加入AutoMapper套件
加入組件
System.Transactions
ProductServices.cs
套件版本不一致,有修改作者原本的內容:
- AutoMapper 5.0.2 改用 Mapper.Initialize 停用 Mapper.CreateMap
using AutoMapper;
using BusinessEntities;
using DataModel;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
namespace BusinessServices
{
/// <summary>
/// Offers services for product specific CRUD operations
/// </summary>
public class ProductServices : IProductServices
{
private readonly UnitOfWork _unitOfWork;
/// <summary>
/// Public constructor.
/// </summary>
public ProductServices()
{
_unitOfWork = new UnitOfWork();
}
/// <summary>
/// Fetches product details by id
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
public BusinessEntities.ProductEntity GetProductById(int productId)
{
var product = _unitOfWork.ProductRepository.GetByID(productId);
if (product != null)
{
//AutoMapper 5.0.2 改用 Mapper.Initialize 停用 Mapper.CreateMap
//Mapper.CreateMap<Products, ProductEntity>();
Mapper.Initialize(cfg => cfg.CreateMap<Products, ProductEntity>());
var productModel = Mapper.Map<Products, ProductEntity>(product);
return productModel;
}
return null;
}
/// <summary>
/// Fetches all the products.
/// </summary>
/// <returns></returns>
public IEnumerable<BusinessEntities.ProductEntity> GetAllProducts()
{
var products = _unitOfWork.ProductRepository.GetAll().ToList();
if (products.Any())
{
//AutoMapper 5.0.2 改用 Mapper.Initialize 停用 Mapper.CreateMap
//Mapper.CreateMap<Products, ProductEntity>();
Mapper.Initialize(cfg => cfg.CreateMap<Products, ProductEntity>());
var productsModel = Mapper.Map<List<Products>, List<ProductEntity>>(products);
return productsModel;
}
return null;
}
/// <summary>
/// Creates a product
/// </summary>
/// <param name="productEntity"></param>
/// <returns></returns>
public int CreateProduct(BusinessEntities.ProductEntity productEntity)
{
using (var scope = new TransactionScope())
{
var product = new Products
{
ProductName = productEntity.ProductName
};
_unitOfWork.ProductRepository.Insert(product);
_unitOfWork.Save();
scope.Complete();
return product.ProductId;
}
}
/// <summary>
/// Updates a product
/// </summary>
/// <param name="productId"></param>
/// <param name="productEntity"></param>
/// <returns></returns>
public bool UpdateProduct(int productId, BusinessEntities.ProductEntity productEntity)
{
var success = false;
if (productEntity != null)
{
using (var scope = new TransactionScope())
{
var product = _unitOfWork.ProductRepository.GetByID(productId);
if (product != null)
{
product.ProductName = productEntity.ProductName;
_unitOfWork.ProductRepository.Update(product);
_unitOfWork.Save();
scope.Complete();
success = true;
}
}
}
return success;
}
/// <summary>
/// Deletes a particular product
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
public bool DeleteProduct(int productId)
{
var success = false;
if (productId > 0)
{
using (var scope = new TransactionScope())
{
var product = _unitOfWork.ProductRepository.GetByID(productId);
if (product != null)
{
_unitOfWork.ProductRepository.Delete(product);
_unitOfWork.Save();
scope.Complete();
success = true;
}
}
}
return success;
}
}
}
建立 Web Api Controller
產品用的Controller
ProductController.cs
將HTTP VERBS做為方法的名稱,Web Api會自己取得相對應的方法
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using BusinessEntities;
using BusinessServices;
namespace WebApi.Controllers
{
public class ProductController : ApiController
{
private readonly IProductServices _productServices;
#region Public Constructor
/// <summary>
/// Public constructor to initialize product service instance
/// </summary>
public ProductController()
{
_productServices = new ProductServices();
}
#endregion
// GET api/product
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts();
if (products != null)
{
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
{
return Request.CreateResponse(HttpStatusCode.OK, productEntities);
}
}
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
}
// GET api/product/5
public HttpResponseMessage Get(int id)
{
var product = _productServices.GetProductById(id);
if (product != null)
{
return Request.CreateResponse(HttpStatusCode.OK, product);
}
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
}
// POST api/product
public int Post([FromBody] ProductEntity productEntity)
{
return _productServices.CreateProduct(productEntity);
}
// PUT api/product/5
public bool Put(int id, [FromBody]ProductEntity productEntity)
{
if (id > 0)
{
return _productServices.UpdateProduct(id, productEntity);
}
return false;
}
// DELETE api/product/5
public bool Delete(int id)
{
if (id > 0)
{
return _productServices.DeleteProduct(id);
}
return false;
}
}
}
WebApi專案加入參考
nuget
Entity Framework
web.config
加入entity framework需要的區段
- configSections
- connectionStrings
- entityFramework
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --></configSections>
<connectionStrings>
<add name="WebApiDbEntities" connectionString="metadata=res://*/WebApiDataModel.csdl|res://*/WebApiDataModel.ssdl|res://*/WebApiDataModel.msl;provider=System.Data.SqlClient;provider connection string="data source=(localdb)\MSSQLLocalDb;initial catalog=WebApiDb;persist security info=True;user id=akhil;password=123456;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
</connectionStrings>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="mssqllocaldb" />
</parameters>
</defaultConnectionFactory>
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
</entityFramework>
<appSettings>...</appSettings>
<system.web>
...
</system.web>
<system.webServer>
...
</system.webServer>
</configuration>
驗證Web Api是否正常運作
使用PostMan
//取得單一資料
api/product/5
//取得全部資料
api/product
//新增
//修改
//刪除