一、創建一個能跑的起來的Web API項目
1、建一個空的 ASP.NET Web應用
(為什么不直接添加一個Web API項目呢,那樣會有些多余的內容(如js、css、Areas等),項目首先就需要清理一次。這樣一步步來也更易於理解API項目)

2、用NuGet引入Web API

這時我的packages
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.AspNet.Cors" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.Cors" version="5.2.3" targetFramework="net45" /> <package id="Microsoft.AspNet.WebApi.WebHost" version="5.2.3" targetFramework="net45" /> <package id="Newtonsoft.Json" version="6.0.8" targetFramework="net45" /> </packages>
3、App_Start下創建一個WebApiConfig.cs類,作為api啟動配置類
代碼如下
using System.Web.Http; using System.Web.Http.Cors; namespace Frozen.API { public class WebApiConfig { public static void Register(HttpConfiguration config) { //啟用跨域 var cors = new EnableCorsAttribute("*", "*", "*"); config.EnableCors(cors); // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
4、添加“全局應用程序類” Global.asax
Application_Start方法如下
protected void Application_Start(object sender, EventArgs e) { GlobalConfiguration.Configure(WebApiConfig.Register); }
5、添加一個Web API控制器類,比如”StudentController“
初始代碼如下(常用的還有個Patch方法)
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace Frozen.API.Controllers { public class StudentController : ApiController { // GET api/<controller> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/<controller>/5 public string Get(int id) { return "value"; } // POST api/<controller> public void Post([FromBody]string value) { } // PUT api/<controller>/5 public void Put(int id, [FromBody]string value) { } // DELETE api/<controller>/5 public void Delete(int id) { } } }
6、綁定下測試域名
還得在hosts做下指向
7、感覺可以跑起來了。啟動程序,直接報了500,錯誤提示是”由於權限不足而無法讀取配置文件“
權限問題,解決方案文件夾,右鍵屬性,安全選項卡,添加‘Everyone’,將‘修改’權限打開,解決問題
8、提前在‘public string Get(int id)’處加好斷點,在瀏覽器輸入‘http://api.frozen.com/api/student/1’
命中斷點,說明這已經是一個可以跑起來的API項目了

二、搭建框架
1、按ABP,被我搭建成了這樣。這張圖后續會根據項目實際情況,或個人現階段的理論誤區,持續更新
(其實還應該有張架構圖,但由於線條交錯,看起來效果不怎么樣,所以沒貼出來)

2、注冊Autofac
從NuGet安裝Autofac,關鍵代碼如下:
public static void SetAutofacContainer() { var builder = new ContainerBuilder(); builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); builder.RegisterType<InMemoryCache>().As<ICache>().InstancePerLifetimeScope(); builder.RegisterAssemblyTypes(typeof(StuEducationRepo).Assembly) .Where(t => t.Name.EndsWith("Repo")) .AsImplementedInterfaces().InstancePerLifetimeScope(); builder.RegisterAssemblyTypes(typeof(StudentRegisterDmnService).Assembly) .Where(t => t.Name.EndsWith("DmnService")) .AsImplementedInterfaces().InstancePerLifetimeScope(); builder.RegisterAssemblyTypes(typeof(StuEducationAppService).Assembly) .Where(t => t.Name.EndsWith("AppService")) .AsImplementedInterfaces().InstancePerLifetimeScope(); builder.RegisterWebApiFilterProvider(GlobalConfiguration.Configuration); IContainer container = builder.Build(); var resolver = new AutofacWebApiDependencyResolver(container); // Configure Web API with the dependency resolver. GlobalConfiguration.Configuration.DependencyResolver = resolver; }
3、注冊AutoMapper
從NuGet安裝AutoMapper,關鍵代碼如下:
Mapper.Initialize(x => { x.AddProfile<DomainToDtoProfile>(); x.AddProfile<DtoToDomainProfile>(); });
public class DomainToDtoProfile : Profile { public override string ProfileName { get { return "DomainToDtoMappings"; } } protected override void Configure() { Mapper.CreateMap<TB_Stu_Education, StuEducationDto>() .ForMember(dest => dest.DegreeName, opt => opt.ResolveUsing<DegreeNameResolver>().FromMember(s => s.DegreeId)) ; } }
4、注冊log4net
從NuGet安裝log4net,關鍵代碼如下,添加配置文件“\Config\log4net.config”
log4net.Config.XmlConfigurator.Configure( new System.IO.FileInfo(AppDomain.CurrentDomain.BaseDirectory + "\\Config\\log4net.config") );
public class Log4NetLogger : ILogger { private static readonly ILog loginfo; private static readonly ILog logerror; private static readonly ILog logmonitor; static Log4NetLogger() { //不同類型的日志 存放在 不同 的 目錄中 loginfo = LogManager.GetLogger("loginfo"); logerror = LogManager.GetLogger("logerror"); logmonitor = LogManager.GetLogger("logmonitor"); } public void Info(string message) { if (loginfo.IsInfoEnabled) loginfo.Info(message); } public void InfoFormat(string format, params object[] args) { if (loginfo.IsInfoEnabled) loginfo.InfoFormat(format, args); } public void Warn(string message) { if (loginfo.IsWarnEnabled) loginfo.Warn(message); } public void Error(string message, Exception ex = null) { if (logerror.IsErrorEnabled) { if (ex != null) { logerror.Error(message, ex); } else { logerror.Error(message); } } } public void Monitor(string message) { if (logmonitor.IsInfoEnabled) logmonitor.Info(message); } }
三、調試API接口(Fiddler)
1、GET 獲取
http://api.frozen.com/api/StuEducation/1
返回:{"DegreeName":"本科","Id":1,"StuId":1,"DegreeId":2,"SchoolName":"安大","MajorName":"計算機科學與技術","StartDate":"2008-09-01 00:00:00","EndDate":"2012-06-01 00:00:00","CreateTime":"2015-01-01 00:00:00","LastModifyTime":null}
代碼:
public StuEducationDto Get(int id) { var dto = _stuEducationAppService.GetDTOById(id); return dto; }
2、POST 新增

返回
HTTP/1.1 201 Created
代碼:
public HttpResponseMessage Post([FromBody]StuEducationDto dto) { int result = _stuEducationAppService.CreateByDTO(dto); return result > 0 ? Request.CreateResponse(HttpStatusCode.Created) : Request.CreateResponse(HttpStatusCode.InternalServerError); }
3、PUT 新增/修改
代碼:
public HttpResponseMessage Put(int id, [FromBody]StuEducationDto dto) { var result = _stuEducationAppService.CreateOrUpdateByDTO(id, dto); return result > 0 ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.InternalServerError); }
4、Patch 局部更新
代碼(使用了dynamic參數):
public HttpResponseMessage Patch(int id, dynamic dtoUpdate) { var dto = _stuEducationAppService.GetDTOById(id); if (dto == null) { return Request.CreateResponse(HttpStatusCode.PaymentRequired); } foreach (JProperty prop in dtoUpdate) { switch (prop.Name.ToLower()) { case "degreeid": dto.DegreeId = prop.Value.ToObject<int>(); break; case "schoolname": dto.SchoolName = prop.Value.ToObject<string>(); break; case "majormame": dto.SchoolName = prop.Value.ToObject<string>(); break; case "startdate": dto.StartDate = prop.Value.ToObject<DateTime>(); break; case "enddate": dto.EndDate = prop.Value.ToObject<DateTime>(); break; default: break; } } var result = _stuEducationAppService.UpdateByDTO(id, dto); return result > 0 ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.NotFound); }
5、Delete 刪
代碼:
public HttpResponseMessage Delete(int id) { var result = _stuEducationAppService.DeleteById(id); return result > 0 ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.NotFound); }
四、數據倉儲
由於不打算使用EF,但數據倉儲又是DDD一個不可繞開的話題,所以單獨寫了一個DDD EF Repository的Demo
http://www.cnblogs.com/frozenzhang/p/5390551.html
五、MongoDB數據倉儲
已單獨開篇,
http://www.cnblogs.com/frozenzhang/p/5442314.html
六、領域事件DomainEvents
感謝倉儲大叔的分享,這里只貼出大叔沒給出的源碼:ActionDelegatedEventHandler<TEvent>類
public class ActionDelegatedEventHandler<TEvent> : IEventHandler<TEvent> where TEvent : IEvent { private Action<TEvent> func; public ActionDelegatedEventHandler(Action<TEvent> func) { this.func = func; } public void Handle(TEvent evt) { func(evt); } }
調用示例:
static void Main(string[] args) { EventBus.Instance.Subscribe(new DeleteStudentHandler_SendEmailToStudent()); EventBus.Instance.Subscribe(new DeleteStudentHandler_SendEmailToStudent()); EventBus.Instance.Subscribe(new ActionDelegatedEventHandler<DeleteStudentEvent>(o => { Thread.Sleep(100); Console.WriteLine("學生Id為{0}", o.StuId); })); EventBus.Instance.Subscribe(new ActionDelegatedEventHandler<DeleteStudentEvent>(o => { Thread.Sleep(100); Console.WriteLine("學生Id為{0}", o.StuId); })); var entity = new DeleteStudentEvent { StuId = 1 }; Console.WriteLine("事件:刪除一個學生,學生Id為{0}", entity.StuId); EventBus.Instance.Publish(entity); Console.WriteLine("over"); Console.ReadKey(); }
結果:

七、領域Command
關於Event和Command的解釋,http://www.zhihu.com/question/29129068
完整代碼請移步
注冊
builder.RegisterType<DefaultCommandBus>().As<ICommandBus>().InstancePerLifetimeScope(); var domainCommands = Assembly.Load("Frozen.DomainCommands"); builder.RegisterAssemblyTypes(domainCommands) .AsClosedTypesOf(typeof(ICommandHandler<>)).InstancePerLifetimeScope(); builder.RegisterAssemblyTypes(domainCommands) .AsClosedTypesOf(typeof(IValidationHandler<>)).InstancePerLifetimeScope();
Command
/// <summary> /// Command 刪除學生 /// </summary> public class DeleteStudentCommand : ICommand { /// <summary> /// 學生Id /// </summary> public int StuId { get; set; } }
Handler
public class DeleteStudentHandler : ICommandHandler<DeleteStudentCommand> { private readonly IStuEducationRepo _iStuEducationRepo; public DeleteStudentHandler(IStuEducationRepo iStuEducationRepo) { this._iStuEducationRepo = iStuEducationRepo; } public ICommandResult Execute(DeleteStudentCommand command) { return new CommandResult(true); } }
調用
var command = new DeleteStudentCommand() {
StuId = 1 }; var result = _commandBus.Submit(command);
結果:

八、Solr搜索引擎
1、搭建Solr環境(Windows),見另一篇分享http://www.cnblogs.com/frozenzhang/p/5333746.html
2、在browser的Solr管理后台添加Core “Student”
<!-- general --> <field name="StuId" type="int" indexed="true" stored="true" multiValued="false" required="true"/> <field name="Name" type="string" indexed="true" stored="true"/> <field name="DegreeIdArr" type="int" indexed="true" stored="true" multiValued="true" /> <field name="SchoolNameArr" type="string" indexed="true" stored="true" multiValued="true" /> <field name="MajorCodeArr" type="string" indexed="true" stored="true" multiValued="true" /> <uniqueKey>StuId</uniqueKey>

3、從NuGet安裝SolrNet
4、項目中新建索引類‘StudentSolrIndex’
public class StudentSolrIndex { [SolrUniqueKey("StuId")] public int StuId { get; set; } [SolrField("Name")] public string Name { get; set; } [SolrField("DegreeId")] public int DegreeId { get; set; } public string SchoolNamesStr { get; set; } [SolrField("SchoolNameArr")] public ICollection<string> SchoolNameArr { get { if (string.IsNullOrEmpty(SchoolNamesStr)) { return new string[] { }; } return SchoolNamesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct().ToList(); } } public string MajorCodesStr { get; set; } [SolrField("MajorCodeArr")] public ICollection<string> MajorCodeArr { get { if (string.IsNullOrEmpty(MajorCodesStr)) { return new string[] { }; } return MajorCodesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct().ToList(); } } }
5、.config配置
<appSettings>
<add key="StudentSolrServiceUrl" value="http://localhost:8080/solr/Student" />
</appSettings>
6、同步數據
SolrNet.Startup.Init<StudentSolrIndex>(ConfigurationManager.AppSettings.Get("StudentSolrServiceUrl")); var solrOper = ServiceLocator.Current.GetInstance<ISolrOperations<StudentSolrIndex>>(); solrOper.Add(new StudentSolrIndex() { StuId = 1, Name = "張冬林", DegreeId = 3, SchoolNamesStr = "安大,上大", MajorCodesStr = "080901,080902" }); solrOper.Commit();
7、這時在solr的admin界面,查詢,看見數據,說明數據同步成功了

8、查詢數據
SolrNet.Startup.Init<StudentSolrResult>(ConfigurationManager.AppSettings.Get("StudentSolrServiceUrl")); var solrQuery = ServiceLocator.Current.GetInstance<ISolrOperations<StudentSolrResult>>(); ISolrQuery mainQuery = SolrQuery.All; QueryOptions options = new QueryOptions() { FilterQueries = new List<ISolrQuery>().ToArray(), OrderBy = new SortOrder[] { SortOrder.Parse("score desc") }, Start = 0, Rows = 20, }; var results = solrQuery.Query(mainQuery, options);
結果截圖:

九、Redis
1、Windows下Redis的環境安裝,感謝園子里一位博友的分享
2、配置主從服務器(從服務器作為只讀)
Redis的默認服務端口是6379,
所以這里只修改從服務器的redis.config里的配置
port 6380
bind 127.0.0.1
slaveof 127.0.0.1 6379
6379是主服務器,6380作為從服務器

3、Redis作為緩存服務器
已單獨開了一篇,http://www.cnblogs.com/frozenzhang/p/5439940.html
十、SignalR(+Redis)
1、SignalR在線聊天室
已單獨開了一篇,http://www.cnblogs.com/frozenzhang/p/5406773.html
十一、Memcached
1 <configSections> 2 <sectionGroup name="enyim.com"> 3 <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" /> 4 </sectionGroup> 5 </configSections> 6 <enyim.com> 7 <memcached> 8 <servers> 9 <!-- put your own server(s) here--> 10 <add address="127.0.0.1" port="11211" /> 11 </servers> 12 <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" /> 13 </memcached> 14 </enyim.com>
1 using Enyim.Caching; 2 using Enyim.Caching.Memcached; 3 using Frozen.Framework.Cache; 4 using System; 5 using System.Collections.Generic; 6 7 namespace Froen.Memcached.Cached 8 { 9 public class MemcachedCache : ICache 10 { 11 private const string REGION_NAME = "$#MemcachedCache#$"; 12 private const int _DefaultCacheTime = 30; 13 private readonly static object s_lock = new object(); 14 15 private static readonly MemcachedClient client = new MemcachedClient(); 16 17 public IEnumerable<KeyValuePair<string, object>> Entries 18 { 19 get { throw new NotImplementedException(); } 20 } 21 22 public T Get<T>(string key, Func<T> baseMethod) 23 { 24 return Get(key, baseMethod, _DefaultCacheTime); 25 } 26 27 public T Get<T>(string key, Func<T> baseMethod, int cacheTime) 28 { 29 key = BuildKey(key); 30 31 if (client.Get(key) != null) 32 { 33 return client.Get<T>(key); 34 } 35 else 36 { 37 lock (s_lock) 38 { 39 if (client.Get(key) == null) 40 { 41 var value = baseMethod(); 42 if (value != null) //請區別null與String.Empty 43 { 44 client.Store(StoreMode.Set, key, value, TimeSpan.FromMinutes(cacheTime)); 45 } 46 return value; 47 } 48 return client.Get<T>(key); 49 } 50 } 51 } 52 53 public bool Contains(string key) 54 { 55 return client.Get(BuildKey(key)) != null; 56 } 57 58 public void Remove(string key) 59 { 60 client.Remove(BuildKey(key)); 61 } 62 63 private string BuildKey(string key) 64 { 65 return string.IsNullOrEmpty(key) ? null : REGION_NAME + key; 66 } 67 68 } 69 }
十二、為API自動生成幫助文檔
1、安裝Microsoft.AspNet.WebApi.HelpPage
2、修改Areas/HelpPag/App_Start/HelpPageConfig的Register方法
配置xml文件路徑:"~/App_Data"
1 // Uncomment the following to use the documentation from XML documentation file. 2 config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data")));
這時訪問/Help應該會報異常

3、修改Areas/HelpPag/XmlDocumentationProvider的構造函數
1 public XmlDocumentationProvider(string documentPath) 2 { 3 //if (documentPath == null) 4 //{ 5 // throw new ArgumentNullException("documentPath"); 6 //} 7 //XPathDocument xpath = new XPathDocument(documentPath); 8 //_documentNavigator = xpath.CreateNavigator(); 9 10 XDocument finalDoc = null; 11 foreach (string file in Directory.GetFiles(documentPath, "Frozen.*.xml")) 12 { 13 using (var fileStream = File.OpenRead(file)) 14 { 15 if (finalDoc == null) 16 { 17 finalDoc = XDocument.Load(fileStream); 18 } 19 else 20 { 21 XDocument xdocAdditional = XDocument.Load(fileStream); 22 23 finalDoc.Root.XPathSelectElement("/doc/members") 24 .Add(xdocAdditional.Root.XPathSelectElement("/doc/members").Elements()); 25 } 26 } 27 } 28 29 // Supply the navigator that rest of the XmlDocumentationProvider code looks for 30 _documentNavigator = finalDoc.CreateNavigator(); 31 }
4、通過Web頁查看接口

強烈推薦閱讀園友的分享:如何使 WebAPI 自動生成漂亮又實用在線API文檔-Swashbuckle。界面不僅顏值高,還可以代替Fiddler來調試api接口
寫在最后
此篇隨筆,如果你粗閱了一下,會發現沒有任何理論闡述,這主要是因為博主對相關知識點的理解尚不深刻,至今也沒有閱讀過源碼,不想誤導大家。錯誤的地方也歡迎指正。內容基本是從當前的項目中整理出來的,或來自園子里。博主雖有四年工作經驗,會的技能不多也不少,但平時缺乏總結,缺乏review,整理這篇隨筆的主要原因一方面是想梳理下自己掌握的技能,另一方面也希望能明確自己在專業技能方面的不足之處
兩年前我剛來魔都,其實那時候(對於當時的我來說,理想的)工作挺難找的,房總招了我,給的待遇對於那時候的我來說,已經非常滿足了。無奈項目后來轉java了,dotNET團隊解散。不管怎么說,房哥對我有着一份知遇之恩,所以以后如果有機會,希望能再次拜入房哥門下
接着就是面試,基本都會被問的一個問題‘在315che做的都是維護工作么’,好吧,現在才知道人家其實是在問‘你是不是團隊里最菜的那個’。面試了幾家,有人要就入職了,平級跳,平級跳,沒錯,就是平級跳
近期要換雇主了,已拿到一個比較滿意的offer,所以沒有什么如臨大敵的感覺。但一切又好像太平靜了
這篇文章發的其實有點倉促,因為臨時打算花時間做個面試准備(這段話寫於2016/05/10凌晨)
一年半前,面試了滬江和攜程,都被刷了,比較難回答的問題:‘性能’‘設計模式’‘算法’
攜程被刷是因為當時資歷的確沒達到
滬江當時面試后感覺還好,但無奈還是沒過。現在想想,當時可能因為是,沒有MVC項目的工作經驗,‘性能’意識不強,還有就是自己面試時的應變能力、語言組織能力有點差
這半個月其實我也做了下思想斗爭,該不該再投一次滬江和攜程呢。現在還是決定再投一次,說實話 面試時,這些理論方面的描述,我其實並不擅長。所以打算一個星期左右的時間做下面試准備,准備差不多后再投
最后也說下為什么選擇離開現在的東家。其實只有一句話:情非得已 實屬無奈,我已經兌現了當初面試時我說的一句話,‘我不會輕易跳槽’,在我這樣Programmer(至少我)的眼里,我已經算是堅持到最后的那個人了
本篇也算結束了,雖然還有幾個知識點我想整理的,后面有時間再補上吧
晚安,上海
晚安,所有在大城市打拼的人
附:源碼下載
