搭建一個Web API項目(DDD)


  傳送陣:寫在最后

一、創建一個能跑的起來的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(至少我)的眼里,我已經算是堅持到最后的那個人了

  本篇也算結束了,雖然還有幾個知識點我想整理的,后面有時間再補上吧

  晚安,上海

  晚安,所有在大城市打拼的人

 

  附:源碼下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM