• 搭建一个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,关键代码如下,添加配置文件“Configlog4net.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(至少我)的眼里,我已经算是坚持到最后的那个人了

      本篇也算结束了,虽然还有几个知识点我想整理的,后面有时间再补上吧

      晚安,上海

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

      附:源码下载

  • 相关阅读:
    多线程,死锁,DeadLock
    多线程,Socket,上传文件
    MyBatis自动创建代码
    oracle 11g 监听启动成功后立马自动关闭
    echart 报表图片不展示
    quick easyui ftp 启动报错,bind port faild,maybe another……
    jquery.easyui.min.js:1 Uncaught TypeError: $.fn.validatebox.methods[_43e] is not a function(…)
    无效的列类型
    当eclipse发送报文乱码时,在java代码中发送和接收的地方都改成utf-8编码即可
    ie js new date
  • 原文地址:https://www.cnblogs.com/frozenzhang/p/5315944.html
Copyright © 2020-2023  润新知