1. 程式人生 > 其它 >asp.net web api 2.2 基礎框架(帶例子)

asp.net web api 2.2 基礎框架(帶例子)

簡介

這個是我自己編寫的asp.net web api 2.2的基礎框架,使用了Entity Framework 6.2(beta)作為ORM。

該模板主要採用了 Unit of Work 和 Repository 模式,使用autofac進行控制反轉(ioc)。

記錄Log採用的是NLog。

結構

專案列表如下圖:

該啟動模板為多層結構,其結構如下圖:

開發流程

1. 建立model

在LegacyApplication.Models專案裡建立相應的資料夾作為子模組,然後建立model,例如Nationality.cs:

using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Infrastructure.Annotations;
using LegacyApplication.Shared.Features.Base;

namespace LegacyApplication.Models.HumanResources
{
    public class Nationality : EntityBase
    {
        public string Name { get; set; }
    }

    public class NationalityConfiguration : EntityBaseConfiguration<Nationality>
    {
        public NationalityConfiguration()
        {
            ToTable("hr.Nationality");
            Property(x => x.Name).IsRequired().HasMaxLength(50);
            Property(x => x.Name).HasMaxLength(50).HasColumnAnnotation(
                IndexAnnotation.AnnotationName,
                new IndexAnnotation(new IndexAttribute { IsUnique = true }));
        }
    }
}

所建立的model需要使用EntityBase作為基類,EntityBase有幾個業務欄位,包括CreateUser,CreateTime,UpdateUser,UpdateTime,LastAction。EntityBase程式碼如下:

using System;

namespace LegacyApplication.Shared.Features.Base
{
    public class EntityBase : IEntityBase
    {
        public EntityBase(string userName = "匿名")
        {
            CreateTime = UpdateTime = DateTime.Now;
            LastAction = "建立";
            CreateUser = UpdateUser = userName;
        }

        public int Id { get; set; }
        public DateTime CreateTime { get; set; }
        public DateTime UpdateTime { get; set; }
        public string CreateUser { get; set; }
        public string UpdateUser { get; set; }
        public string LastAction { get; set; }

        public int Order { get; set; }
    }
}

model需要使用Fluent Api來配置資料庫的對映屬性等,按約定使用Model名+Configuration作為fluent api的類的名字,並需要繼承EntityBaseConfiguration<T>這個類,這個類對EntityBase的幾個屬性進行了對映配置,其程式碼如下:

using System.Data.Entity.ModelConfiguration;

namespace LegacyApplication.Shared.Features.Base
{
    public class EntityBaseConfiguration<T> : EntityTypeConfiguration<T> where T : EntityBase
    {
        public EntityBaseConfiguration()
        {
            HasKey(e => e.Id);
            Property(x => x.CreateTime).IsRequired();
            Property(x => x.UpdateTime).IsRequired();
            Property(x => x.CreateUser).IsRequired().HasMaxLength(50);
            Property(x => x.UpdateUser).IsRequired().HasMaxLength(50);
            Property(x => x.LastAction).IsRequired().HasMaxLength(50);
        }
    }
}

1.1 自成樹形的Model

自成樹形的model是指自己和自己成主外來鍵關係的Model(表),例如選單表或者部門表的設計有時候是這樣的,下面以部門為例:

using System.Collections.Generic;
using LegacyApplication.Shared.Features.Tree;

namespace LegacyApplication.Models.HumanResources
{
    public class Department : TreeEntityBase<Department>
    {
        public string Name { get; set; }

        public ICollection<Employee> Employees { get; set; }
    }

    public class DepartmentConfiguration : TreeEntityBaseConfiguration<Department>
    {
        public DepartmentConfiguration()
        {
            ToTable("hr.Department");

            Property(x => x.Name).IsRequired().HasMaxLength(100);
        }
    }
}

與普通的Model不同的是,它需要繼承的是TreeEntityBase<T>這個基類,TreeEntityBase<T>的程式碼如下:

using System.Collections.Generic;
using LegacyApplication.Shared.Features.Base;

namespace LegacyApplication.Shared.Features.Tree
{
    public class TreeEntityBase<T>: EntityBase, ITreeEntity<T> where T: TreeEntityBase<T>
    {
        public int? ParentId { get; set; }
        public string AncestorIds { get; set; }
        public bool IsAbstract { get; set; }
        public int Level => AncestorIds?.Split('-').Length ?? 0;
        public T Parent { get; set; }
        public ICollection<T> Children { get; set; }
    }
}

其中ParentId,Parent,Children這幾個屬性是樹形關係相關的屬性,AncestorIds定義為所有祖先Id層級別連線到一起的一個字串,需要自己實現。然後Level屬性是通過AncestorIds這個屬性自動獲取該Model在樹形結構裡面的層級。

該Model的fluent api配置類需要繼承的是TreeEntityBaseConfiguration<T>這個類,程式碼如下:

using System.Collections.Generic;
using LegacyApplication.Shared.Features.Base;

namespace LegacyApplication.Shared.Features.Tree
{
    public class TreeEntityBaseConfiguration<T> : EntityBaseConfiguration<T> where T : TreeEntityBase<T>
    {
        public TreeEntityBaseConfiguration()
        {
            Property(x => x.AncestorIds).HasMaxLength(200);
            Ignore(x => x.Level);

            HasOptional(x => x.Parent).WithMany(x => x.Children).HasForeignKey(x => x.ParentId).WillCascadeOnDelete(false);
        }
    }
}

針對樹形結構的model,我還做了幾個簡單的Extension Methods,程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LegacyApplication.Shared.Features.Tree
{
    public static class TreeExtensions
    {
        /// <summary>
        /// 把樹形結構資料的集合轉化成單一根結點的樹形結構資料
        /// </summary>
        /// <typeparam name="T">樹形結構實體</typeparam>
        /// <param name="items">樹形結構實體的集合</param>
        /// <returns>樹形結構實體的根結點</returns>
        public static TreeEntityBase<T> ToSingleRoot<T>(this IEnumerable<TreeEntityBase<T>> items) where T : TreeEntityBase<T>
        {
            var all = items.ToList();
            if (!all.Any())
            {
                return null;
            }
            var top = all.Where(x => x.ParentId == null).ToList();
            if (top.Count > 1)
            {
                throw new Exception("樹的根節點數大於1個");
            }
            if (top.Count == 0)
            {
                throw new Exception("未能找到樹的根節點");
            }
            TreeEntityBase<T> root = top.Single();

            Action<TreeEntityBase<T>> findChildren = null;
            findChildren = current =>
            {
                var children = all.Where(x => x.ParentId == current.Id).ToList();
                foreach (var child in children)
                {
                    findChildren(child);
                }
                current.Children = children as ICollection<T>;
            };

            findChildren(root);

            return root;
        }

        /// <summary>
        /// 把樹形結構資料的集合轉化成多個根結點的樹形結構資料
        /// </summary>
        /// <typeparam name="T">樹形結構實體</typeparam>
        /// <param name="items">樹形結構實體的集合</param>
        /// <returns>多個樹形結構實體根結點的集合</returns>
        public static List<TreeEntityBase<T>> ToMultipleRoots<T>(this IEnumerable<TreeEntityBase<T>> items) where T : TreeEntityBase<T>
        {
            List<TreeEntityBase<T>> roots;
            var all = items.ToList();
            if (!all.Any())
            {
                return null;
            }
            var top = all.Where(x => x.ParentId == null).ToList();
            if (top.Any())
            {
                roots = top;
            }
            else
            {
                throw new Exception("未能找到樹的根節點");
            }

            Action<TreeEntityBase<T>> findChildren = null;
            findChildren = current =>
            {
                var children = all.Where(x => x.ParentId == current.Id).ToList();
                foreach (var child in children)
                {
                    findChildren(child);
                }
                current.Children = children as ICollection<T>;
            };

            roots.ForEach(findChildren);

            return roots;
        }

        /// <summary>
        /// 作為父節點, 取得樹形結構實體的祖先ID串
        /// </summary>
        /// <typeparam name="T">樹形結構實體</typeparam>
        /// <param name="parent">父節點實體</param>
        /// <returns></returns>
        public static string GetAncestorIdsAsParent<T>(this T parent) where T : TreeEntityBase<T>
        {
            return string.IsNullOrEmpty(parent.AncestorIds) ? parent.Id.ToString() : (parent.AncestorIds + "-" + parent.Id);
        }
    }
}

2. 把Model加入到DbContext裡面

建立完Model後,需要把Model加入到Context裡面,下面是CoreContext的程式碼:

using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Diagnostics;
using System.Reflection;
using LegacyApplication.Database.Infrastructure;
using LegacyApplication.Models.Core;
using LegacyApplication.Models.HumanResources;
using LegacyApplication.Models.Work;
using LegacyApplication.Shared.Configurations;

namespace LegacyApplication.Database.Context
{
    public class CoreContext : DbContext, IUnitOfWork
    {
        public CoreContext() : base(AppSettings.DefaultConnection)
        {
            //System.Data.Entity.Database.SetInitializer<CoreContext>(null);
#if DEBUG
            Database.Log = Console.Write;
            Database.Log = message => Trace.WriteLine(message);
#endif
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); //去掉預設開啟的級聯刪除

            modelBuilder.Configurations.AddFromAssembly(Assembly.GetAssembly(typeof(UploadedFile)));
        }

        //Core
        public DbSet<UploadedFile> UploadedFiles { get; set; }

        //Work
        public DbSet<InternalMail> InternalMails { get; set; }
        public DbSet<InternalMailTo> InternalMailTos { get; set; }
        public DbSet<InternalMailAttachment> InternalMailAttachments { get; set; }
        public DbSet<Todo> Todos { get; set; }
        public DbSet<Schedule> Schedules { get; set; }

        //HR
        public DbSet<Department> Departments { get; set; }
        public DbSet<Employee> Employees { get; set; }
        public DbSet<JobPostLevel> JobPostLevels { get; set; }
        public DbSet<JobPost> JobPosts { get; set; }
        public DbSet<AdministrativeLevel> AdministrativeLevels { get; set; }
        public DbSet<TitleLevel> TitleLevels { get; set; }
        public DbSet<Nationality> Nationalitys { get; set; }
        
        
    }
}

其中“modelBuilder.Configurations.AddFromAssembly(Assembly.GetAssembly(typeof(UploadedFile)));” 會把UploadFile所在的Assembly(也就是LegacyApplication.Models這個專案)裡面所有的fluent api配置類(EntityTypeConfiguration的派生類)全部載入進來。

這裡說一下CoreContext,由於它派生與DbContext,而DbContext本身就實現了Unit of Work 模式,所以我做Unit of work模式的時候,就不考慮重新建立一個新類作為Unit of work了,我從DbContext抽取了幾個方法,提煉出了IUnitofWork介面,程式碼如下:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace LegacyApplication.Database.Infrastructure
{
    public interface IUnitOfWork: IDisposable
    {
        int SaveChanges();
        Task<int> SaveChangesAsync(CancellationToken cancellationToken);
        Task<int> SaveChangesAsync();
    }
}

用的時候IUnitOfWork就是CoreContext的化身。

3.建立Repository

我理解的Repository(百貨)裡面應該具有各種小粒度的邏輯方法,以便複用,通常Repository裡面要包含各種單筆和多筆的CRUD方法。

此外,我在我的模板裡做了約定,不在Repository裡面進行任何的提交儲存等動作。

下面我們來建立一個Repository,就用Nationality為例,在LegacyApplication.Repositories裡面相應的資料夾建立NationalityRepository類:

using LegacyApplication.Database.Infrastructure;
using LegacyApplication.Models.HumanResources;
namespace LegacyApplication.Repositories.HumanResources
{
    public interface INationalityRepository : IEntityBaseRepository<Nationality>
    {
    }

    public class NationalityRepository : EntityBaseRepository<Nationality>, INationalityRepository
    {
        public NationalityRepository(IUnitOfWork unitOfWork) : base(unitOfWork)
        {
        }
    }
}

程式碼很簡單,但是它已經包含了常見的10多種CRUD方法,因為它繼承於EntityBaseRepository這個泛型類,這個類的程式碼如下:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using LegacyApplication.Database.Context;
using LegacyApplication.Shared.Features.Base;

namespace LegacyApplication.Database.Infrastructure
{
    public class EntityBaseRepository<T> : IEntityBaseRepository<T>
        where T : class, IEntityBase, new()
    {
        #region Properties
        protected CoreContext Context { get; }

        public EntityBaseRepository(IUnitOfWork unitOfWork)
        {
            Context = unitOfWork as CoreContext;
        }
        #endregion

        public virtual IQueryable<T> All => Context.Set<T>();

        public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = Context.Set<T>();
            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }
            return query;
        }

        public virtual int Count()
        {
            return Context.Set<T>().Count();
        }

        public async Task<int> CountAsync()
        {
            return await Context.Set<T>().CountAsync();
        }

        public T GetSingle(int id)
        {
            return Context.Set<T>().FirstOrDefault(x => x.Id == id);
        }

        public async Task<T> GetSingleAsync(int id)
        {
            return await Context.Set<T>().FirstOrDefaultAsync(x => x.Id == id);
        }

        public T GetSingle(Expression<Func<T, bool>> predicate)
        {
            return Context.Set<T>().FirstOrDefault(predicate);
        }

        public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate)
        {
            return await Context.Set<T>().FirstOrDefaultAsync(predicate);
        }

        public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = Context.Set<T>();
            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }
            return query.Where(predicate).FirstOrDefault();
        }

        public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = Context.Set<T>();
            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }

            return await query.Where(predicate).FirstOrDefaultAsync();
        }

        public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
        {
            return Context.Set<T>().Where(predicate);
        }

        public virtual void Add(T entity)
        {
            DbEntityEntry dbEntityEntry = Context.Entry<T>(entity);
            Context.Set<T>().Add(entity);
        }

        public virtual void Update(T entity)
        {
            DbEntityEntry<T> dbEntityEntry = Context.Entry<T>(entity);

            dbEntityEntry.Property(x => x.Id).IsModified = false;

            dbEntityEntry.State = EntityState.Modified;

            dbEntityEntry.Property(x => x.CreateUser).IsModified = false;
            dbEntityEntry.Property(x => x.CreateTime).IsModified = false;
        }

        public virtual void Delete(T entity)
        {
            DbEntityEntry dbEntityEntry = Context.Entry<T>(entity);
            dbEntityEntry.State = EntityState.Deleted;
        }

        public virtual void AddRange(IEnumerable<T> entities)
        {
            Context.Set<T>().AddRange(entities);
        }

        public virtual void DeleteRange(IEnumerable<T> entities)
        {
            foreach (var entity in entities)
            {
                DbEntityEntry dbEntityEntry = Context.Entry<T>(entity);
                dbEntityEntry.State = EntityState.Deleted;
            }
        }

        public virtual void DeleteWhere(Expression<Func<T, bool>> predicate)
        {
            IEnumerable<T> entities = Context.Set<T>().Where(predicate);

            foreach (var entity in entities)
            {
                Context.Entry<T>(entity).State = EntityState.Deleted;
            }
        }
        public void Attach(T entity)
        {
            Context.Set<T>().Attach(entity);
        }

        public void AttachRange(IEnumerable<T> entities)
        {
            foreach (var entity in entities)
            {
                Attach(entity);
            }
        }

        public void Detach(T entity)
        {
            Context.Entry<T>(entity).State = EntityState.Detached;
        }

        public void DetachRange(IEnumerable<T> entities)
        {
            foreach (var entity in entities)
            {
                Detach(entity);
            }
        }

        public void AttachAsModified(T entity)
        {
            Attach(entity);
            Update(entity);
        }
        
    }
}

我相信這個泛型類你們都應該能看明白,如果不明白可以@我。通過繼承這個類,所有的Repository都具有了常見的方法,並且寫的程式碼很少。

但是為什麼自己建立的Repository不直接繼承與EntityBaseRepository,而是中間非得插一層介面呢?因為我的Repository可能還需要其他的自定義方法,這些自定義方法需要提取到這個接口裡面以便使用。

3.1 對Repository進行註冊

在LegacyApplication.Web專案裡App_Start/MyConfigurations/AutofacWebapiConfig.cs裡面對Repository進行ioc註冊,我使用的是AutoFac

using System.Reflection;
using System.Web.Http;
using Autofac;
using Autofac.Integration.WebApi;
using LegacyApplication.Database.Context;
using LegacyApplication.Database.Infrastructure;
using LegacyApplication.Repositories.Core;
using LegacyApplication.Repositories.HumanResources;
using LegacyApplication.Repositories.Work;
using LegacyApplication.Services.Core;
using LegacyApplication.Services.Work;

namespace LegacyStandalone.Web.MyConfigurations
{
    public class AutofacWebapiConfig
    {
        public static IContainer Container;
        public static void Initialize(HttpConfiguration config)
        {
            Initialize(config, RegisterServices(new ContainerBuilder()));
        }

        public static void Initialize(HttpConfiguration config, IContainer container)
        {
            config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
        }

        private static IContainer RegisterServices(ContainerBuilder builder)
        {
            builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
            
            //builder.RegisterType<CoreContext>()
            //       .As<DbContext>()
            //       .InstancePerRequest();

            builder.RegisterType<CoreContext>().As<IUnitOfWork>().InstancePerRequest();

            //Services
            builder.RegisterType<CommonService>().As<ICommonService>().InstancePerRequest();
            builder.RegisterType<InternalMailService>().As<IInternalMailService>().InstancePerRequest();

            //Core
            builder.RegisterType<UploadedFileRepository>().As<IUploadedFileRepository>().InstancePerRequest();

            //Work
            builder.RegisterType<InternalMailRepository>().As<IInternalMailRepository>().InstancePerRequest();
            builder.RegisterType<InternalMailToRepository>().As<IInternalMailToRepository>().InstancePerRequest();
            builder.RegisterType<InternalMailAttachmentRepository>().As<IInternalMailAttachmentRepository>().InstancePerRequest();
            builder.RegisterType<TodoRepository>().As<ITodoRepository>().InstancePerRequest();
            builder.RegisterType<ScheduleRepository>().As<IScheduleRepository>().InstancePerRequest();

            //HR
            builder.RegisterType<DepartmentRepository>().As<IDepartmentRepository>().InstancePerRequest();
            builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>().InstancePerRequest();
            builder.RegisterType<JobPostLevelRepository>().As<IJobPostLevelRepository>().InstancePerRequest();
            builder.RegisterType<JobPostRepository>().As<IJobPostRepository>().InstancePerRequest();
            builder.RegisterType<AdministrativeLevelRepository>().As<IAdministrativeLevelRepository>().InstancePerRequest();
            builder.RegisterType<TitleLevelRepository>().As<ITitleLevelRepository>().InstancePerRequest();
            builder.RegisterType<NationalityRepository>().As<INationalityRepository>().InstancePerRequest();
            
            Container = builder.Build();

            return Container;
        }
    }
}

在裡面我們也可以看見我把CoreContext註冊為IUnitOfWork。

4.建立ViewModel

ViewModel是最終和前臺打交道的一層。所有的Model都是轉化成ViewModel之後再傳送到前臺,所有前臺提交過來的物件資料,大多是作為ViewModel傳進來的。

下面舉一個例子:

using System.ComponentModel.DataAnnotations;
using LegacyApplication.Shared.Features.Base;

namespace LegacyApplication.ViewModels.HumanResources
{
    public class NationalityViewModel : EntityBase
    {
        [Display(Name = "名稱")]
        [Required(ErrorMessage = "{0}是必填項")]
        [StringLength(50, ErrorMessage = "{0}的長度不可超過{1}")]
        public string Name { get; set; }
    }
}

同樣,它要繼承EntityBase類。

同時,ViewModel裡面應該加上屬性驗證的註解,例如DisplayName,StringLength,Range等等等等,加上註解的屬性在ViewModel從前臺傳進來的時候會進行驗證(詳見Controller部分)。

4.1註冊ViewModel和Model之間的對映

由於ViewModel和Model之間經常需要轉化,如果手寫程式碼的話,那就太多了。所以我這裡採用了一個主流的.net庫叫AutoMapper

因為對映有兩個方法,所以每對需要註冊兩次,分別在DomainToViewModelMappingProfile.cs和ViewModelToDomainMappingProfile.cs裡面:

using System.Linq;
using AutoMapper;
using LegacyApplication.Models.Core;
using LegacyApplication.Models.HumanResources;
using LegacyApplication.Models.Work;
using LegacyApplication.ViewModels.Core;
using LegacyApplication.ViewModels.HumanResources;
using LegacyStandalone.Web.Models;
using Microsoft.AspNet.Identity.EntityFramework;
using LegacyApplication.ViewModels.Work;

namespace LegacyStandalone.Web.MyConfigurations.Mapping
{
    public class DomainToViewModelMappingProfile : Profile
    {
        public override string ProfileName => "DomainToViewModelMappings";

        public DomainToViewModelMappingProfile()
        {
            CreateMap<ApplicationUser, UserViewModel>();
            CreateMap<IdentityRole, RoleViewModel>();
            CreateMap<IdentityUserRole, RoleViewModel>();

            CreateMap<UploadedFile, UploadedFileViewModel>();

            CreateMap<InternalMail, InternalMailViewModel>();
            CreateMap<InternalMailTo, InternalMailToViewModel>();
            CreateMap<InternalMailAttachment, InternalMailAttachmentViewModel>();
            CreateMap<InternalMail, SentMailViewModel>()
                .ForMember(dest => dest.AttachmentCount, opt => opt.MapFrom(ori => ori.Attachments.Count))
                .ForMember(dest => dest.HasAttachments, opt => opt.MapFrom(ori => ori.Attachments.Any()))
                .ForMember(dest => dest.ToCount, opt => opt.MapFrom(ori => ori.Tos.Count))
                .ForMember(dest => dest.AnyoneRead, opt => opt.MapFrom(ori => ori.Tos.Any(y => y.HasRead)))
                .ForMember(dest => dest.AllRead, opt => opt.MapFrom(ori => ori.Tos.All(y => y.HasRead)));
            CreateMap<Todo, TodoViewModel>();
            CreateMap<Schedule, ScheduleViewModel>();

            CreateMap<Department, DepartmentViewModel>()
                .ForMember(dest => dest.Parent, opt => opt.Ignore())
                .ForMember(dest => dest.Children, opt => opt.Ignore());

            CreateMap<Employee, EmployeeViewModel>();
            CreateMap<JobPostLevel, JobPostLevelViewModel>();
            CreateMap<JobPost, JobPostViewModel>();
            CreateMap<AdministrativeLevel, AdministrativeLevelViewModel>();
            CreateMap<TitleLevel, TitleLevelViewModel>();
            CreateMap<Nationality, NationalityViewModel>();
            
        }
    }
}
using AutoMapper;
using LegacyApplication.Models.Core;
using LegacyApplication.Models.HumanResources;
using LegacyApplication.Models.Work;
using LegacyApplication.ViewModels.Core;
using LegacyApplication.ViewModels.HumanResources;
using LegacyStandalone.Web.Models;
using Microsoft.AspNet.Identity.EntityFramework;
using LegacyApplication.ViewModels.Work;

namespace LegacyStandalone.Web.MyConfigurations.Mapping
{
    public class ViewModelToDomainMappingProfile : Profile
    {
        public override string ProfileName => "ViewModelToDomainMappings";

        public ViewModelToDomainMappingProfile()
        {
            CreateMap<UserViewModel, ApplicationUser>();
            CreateMap<RoleViewModel, IdentityRole>();
            CreateMap<RoleViewModel, IdentityUserRole>();

            CreateMap<UploadedFileViewModel, UploadedFile>();

            CreateMap<InternalMailViewModel, InternalMail>();
            CreateMap<InternalMailToViewModel, InternalMailTo>();
            CreateMap<InternalMailAttachmentViewModel, InternalMailAttachment>();
            CreateMap<TodoViewModel, Todo>();
            CreateMap<ScheduleViewModel, Schedule>();

            CreateMap<DepartmentViewModel, Department>()
                .ForMember(dest => dest.Parent, opt => opt.Ignore())
                .ForMember(dest => dest.Children, opt => opt.Ignore());
            CreateMap<EmployeeViewModel, Employee>();
            CreateMap<JobPostLevelViewModel, JobPostLevel>();
            CreateMap<JobPostViewModel, JobPost>();
            CreateMap<AdministrativeLevelViewModel, AdministrativeLevel>();
            CreateMap<TitleLevelViewModel, TitleLevel>();
            CreateMap<NationalityViewModel, Nationality>();
            
        }
    }
}

高階功能還是要參考AutoMapper的文件。

5.建立Controller

先上個例子:

using System.Collections.Generic;
using System.Data.Entity;
using System.Threading.Tasks;
using System.Web.Http;
using AutoMapper;
using LegacyApplication.Database.Infrastructure;
using LegacyApplication.Models.HumanResources;
using LegacyApplication.Repositories.HumanResources;
using LegacyApplication.ViewModels.HumanResources;
using LegacyStandalone.Web.Controllers.Bases;
using LegacyApplication.Services.Core;

namespace LegacyStandalone.Web.Controllers.HumanResources
{
    [RoutePrefix("api/Nationality")]
    public class NationalityController : ApiControllerBase
    {
        private readonly INationalityRepository _nationalityRepository;
        public NationalityController(
            INationalityRepository nationalityRepository,
            ICommonService commonService,
            IUnitOfWork unitOfWork) : base(commonService, unitOfWork)
        {
            _nationalityRepository = nationalityRepository;
        }

        public async Task<IEnumerable<NationalityViewModel>> Get()
        {
            var models = await _nationalityRepository.All.ToListAsync();
            var viewModels = Mapper.Map<IEnumerable<Nationality>, IEnumerable<NationalityViewModel>>(models);
            return viewModels;
        }

        public async Task<IHttpActionResult> GetOne(int id)
        {
            var model = await _nationalityRepository.GetSingleAsync(id);
            if (model != null)
            {
                var viewModel = Mapper.Map<Nationality, NationalityViewModel>(model);
                return Ok(viewModel);
            }
            return NotFound();
        }

        public async Task<IHttpActionResult> Post([FromBody]NationalityViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            var newModel = Mapper.Map<NationalityViewModel, Nationality>(viewModel);
            newModel.CreateUser = newModel.UpdateUser = User.Identity.Name;
            _nationalityRepository.Add(newModel);
            await UnitOfWork.SaveChangesAsync();

            return RedirectToRoute("", new { controller = "Nationality", id = newModel.Id });
        }

        public async Task<IHttpActionResult> Put(int id, [FromBody]NationalityViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            viewModel.UpdateUser = User.Identity.Name;
            viewModel.UpdateTime = Now;
            viewModel.LastAction = "更新";
            var model = Mapper.Map<NationalityViewModel, Nationality>(viewModel);

            _nationalityRepository.AttachAsModified(model);

            await UnitOfWork.SaveChangesAsync();

            return Ok(viewModel);
        }

        public async Task<IHttpActionResult> Delete(int id)
        {
            var model = await _nationalityRepository.GetSingleAsync(id);
            if (model == null)
            {
                return NotFound();
            }
            _nationalityRepository.Delete(model);
            await UnitOfWork.SaveChangesAsync();
            return Ok();
        }
    }
}

這是比較標準的Controller,裡面包含一個多筆查詢,一個單筆查詢和CUD方法。

所有的Repository,Service等都是通過依賴注入弄進來的。

所有的Controller需要繼承ApiControllerBase,所有Controller公用的方法、屬性(property)等都應該放在ApiControllerBase裡面,其程式碼如下:

namespace LegacyStandalone.Web.Controllers.Bases
{
    public abstract class ApiControllerBase : ApiController
    {
        protected readonly ICommonService CommonService;
        protected readonly IUnitOfWork UnitOfWork;
        protected readonly IDepartmentRepository DepartmentRepository;
        protected readonly IUploadedFileRepository UploadedFileRepository;

        protected ApiControllerBase(
            ICommonService commonService,
            IUnitOfWork untOfWork)
        {
            CommonService = commonService;
            UnitOfWork = untOfWork;
            DepartmentRepository = commonService.DepartmentRepository;
            UploadedFileRepository = commonService.UploadedFileRepository;
        }

        #region Current Information

        protected DateTime Now => DateTime.Now;
        protected string UserName => User.Identity.Name;

        protected ApplicationUserManager UserManager => Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

        [NonAction]
        protected async Task<ApplicationUser> GetMeAsync()
        {
            var me = await UserManager.FindByNameAsync(UserName);
            return me;
        }

        [NonAction]
        protected async Task<Department> GetMyDepartmentEvenNull()
        {
            var department = await DepartmentRepository.GetSingleAsync(x => x.Employees.Any(y => y.No == UserName));
            return department;
        }

        [NonAction]
        protected async Task<Department> GetMyDepartmentNotNull()
        {
            var department = await GetMyDepartmentEvenNull();
            if (department == null)
            {
                throw new Exception("您不屬於任何單位/部門");
            }
            return department;
        }

        #endregion

        #region Upload

        [NonAction]
        public virtual async Task<IHttpActionResult> Upload()
        {
            var root = GetUploadDirectory(DateTime.Now.ToString("yyyyMM"));
            var result = await UploadFiles(root);
            return Ok(result);
        }

        [NonAction]
        public virtual async Task<IHttpActionResult> GetFileAsync(int fileId)
        {
            var model = await UploadedFileRepository.GetSingleAsync(x => x.Id == fileId);
            if (model != null)
            {
                return new FileActionResult(model);
            }
            return null;
        }

        [NonAction]
        public virtual IHttpActionResult GetFileByPath(string path)
        {
            return new FileActionResult(path);
        }

        [NonAction]
        protected string GetUploadDirectory(params string[] subDirectories)
        {
#if DEBUG
            var root = HttpContext.Current.Server.MapPath("~/App_Data/Upload");
#else
            var root = AppSettings.UploadDirectory;
#endif
            if (subDirectories != null && subDirectories.Length > 0)
            {
                foreach (var t in subDirectories)
                {
                    root = Path.Combine(root, t);
                }
            }
            if (!Directory.Exists(root))
            {
                Directory.CreateDirectory(root);
            }
            return root;
        }

        [NonAction]
        protected async Task<List<UploadedFile>> UploadFiles(string root)
        {
            var list = await UploadFilesAsync(root);
            var models = Mapper.Map<List<UploadedFileViewModel>, List<UploadedFile>>(list).ToList();
            foreach (var model in models)
            {
                UploadedFileRepository.Add(model);
            }
            await UnitOfWork.SaveChangesAsync();
            return models;
        }

        [NonAction]
        private async Task<List<UploadedFileViewModel>> UploadFilesAsync(string root)
        {
            if (!Request.Content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
            var provider = new MultipartFormDataStreamProvider(root);
            var count = HttpContext.Current.Request.Files.Count;
            var files = new List<HttpPostedFile>(count);
            for (var i = 0; i < count; i++)
            {
                files.Add(HttpContext.Current.Request.Files[i]);
            }
            await Request.Content.ReadAsMultipartAsync(provider);
            var list = new List<UploadedFileViewModel>();
            var now = DateTime.Now;
            foreach (var file in provider.FileData)
            {
                var temp = file.Headers.ContentDisposition.FileName;
                var length = temp.Length;
                var lastSlashIndex = temp.LastIndexOf(@"", StringComparison.Ordinal);
                var fileName = temp.Substring(lastSlashIndex + 2, length - lastSlashIndex - 3);
                var fileInfo = files.SingleOrDefault(x => x.FileName == fileName);
                long size = 0;
                if (fileInfo != null)
                {
                    size = fileInfo.ContentLength;
                }
                var newFile = new UploadedFileViewModel
                {
                    FileName = fileName,
                    Path = file.LocalFileName,
                    Size = size,
                    Deleted = false
                };
                var userName = string.IsNullOrEmpty(User.Identity?.Name)
                    ? "anonymous"
                    : User.Identity.Name;
                newFile.CreateUser = newFile.UpdateUser = userName;
                newFile.CreateTime = newFile.UpdateTime = now;
                newFile.LastAction = "上傳";
                list.Add(newFile);
            }
            return list;
        }

        #endregion

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            UserManager?.Dispose();
            UnitOfWork?.Dispose();
        }
    }

    #region Upload Model

    internal class FileActionResult : IHttpActionResult
    {
        private readonly bool _isInline = false;
        private readonly string _contentType;
        public FileActionResult(UploadedFile fileModel, string contentType, bool isInline = false)
        {
            UploadedFile = fileModel;
            _contentType = contentType;
            _isInline = isInline;
        }

        public FileActionResult(UploadedFile fileModel)
        {
            UploadedFile = fileModel;
        }

        public FileActionResult(string path)
        {
            UploadedFile = new UploadedFile
            {
                Path = path
            };
        }

        private UploadedFile UploadedFile { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            FileStream file;
            try
            {
                file = File.OpenRead(UploadedFile.Path);
            }
            catch (DirectoryNotFoundException)
            {
                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
            }
            catch (FileNotFoundException)
            {
                return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
            }

            var response = new HttpResponseMessage
            {
                Content = new StreamContent(file)
            };
            var name = UploadedFile.FileName ?? file.Name;
            var last = name.LastIndexOf("\", StringComparison.Ordinal);
            if (last > -1)
            {
                var length = name.Length - last - 1;
                name = name.Substring(last + 1, length);
            }
            if (!string.IsNullOrEmpty(_contentType))
            {
                response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(_contentType);
            }
            response.Content.Headers.ContentDisposition =
                new ContentDispositionHeaderValue(_isInline ? DispositionTypeNames.Inline : DispositionTypeNames.Attachment)
                {
                    FileName = HttpUtility.UrlEncode(name, Encoding.UTF8)
                };

            return Task.FromResult(response);
        }
    }
    #endregion
}

這個基類裡面可以有很多東西,目前,它可以獲取當前使用者名稱,當前時間,當前使用者(ApplicationUser),當前登陸人的部門,檔案上傳下載等。

這個基類保證的通用方法的可擴充套件性和複用性,其他例如EntityBase,EntityBaseRepository等等也都是這個道理。

注意,前面在Repository裡面講過,我們不在Repository裡面做提交動作。

所以所有的提交動作都在Controller裡面進行,通常所有掛起的更改只需要一次提交即可,畢竟Unit of Work模式。

5.1獲取列舉的Controller

所有的列舉都應該放在LegacyApplication.Shared/ByModule/xxx模組/Enums下。

然後前臺通過訪問"api/Shared"(SharedController.cs)獲取該模組下(或者整個專案)所有的列舉。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;

namespace LegacyStandalone.Web.Controllers.Bases
{
    [RoutePrefix("api/Shared")]
    public class SharedController : ApiController
    {
        [HttpGet]
        [Route("Enums/{moduleName?}")]
        public IHttpActionResult GetEnums(string moduleName = null)
        {
            var exp = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(t => t.GetTypes())
                .Where(t => t.IsEnum);
            if (!string.IsNullOrEmpty(moduleName))
            {
                exp = exp.Where(x => x.Namespace == $"LegacyApplication.Shared.ByModule.{moduleName}.Enums");
            }
            var enumTypes = exp;
            var result = new Dictionary<string, Dictionary<string, int>>();
            foreach (var enumType in enumTypes)
            {
                result[enumType.Name] = Enum.GetValues(enumType).Cast<int>().ToDictionary(e => Enum.GetName(enumType, e), e => e);
            }
            return Ok(result);
        }

        [HttpGet]
        [Route("EnumsList/{moduleName?}")]
        public IHttpActionResult GetEnumsList(string moduleName = null)
        {
            var exp = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(t => t.GetTypes())
                .Where(t => t.IsEnum);
            if (!string.IsNullOrEmpty(moduleName))
            {
                exp = exp.Where(x => x.Namespace == $"LegacyApplication.Shared.ByModule.{moduleName}.Enums");
            }
            var enumTypes = exp;
            var result = new Dictionary<string, List<KeyValuePair<string, int>>>();
            foreach (var e in enumTypes)
            {
                var names = Enum.GetNames(e);
                var values = Enum.GetValues(e).Cast<int>().ToArray();
                var count = names.Count();
                var list = new List<KeyValuePair<string, int>>(count);
                for (var i = 0; i < count; i++)
                {
                    list.Add(new KeyValuePair<string, int> (names[i], values[i]));
                }
                result.Add(e.Name, list);
            }
            return Ok(result);
        }
    }
}

6.建立Services

注意Controller裡面的CommonService就處在Service層。並不是所有的Model/Repository都有相應的Service層。

通常我在如下情況會建立Service:

a.需要寫與資料庫操作無關的可複用邏輯方法。

b.需要寫多個Repository參與的可複用的邏輯方法或引用。

我的CommonService就是b這個型別,其程式碼如下:

using LegacyApplication.Repositories.Core;
using LegacyApplication.Repositories.HumanResources;
using System;
using System.Collections.Generic;
using System.Text;

namespace LegacyApplication.Services.Core
{
    public interface ICommonService
    {
        IUploadedFileRepository UploadedFileRepository { get; }
        IDepartmentRepository DepartmentRepository { get; }
    }

    public class CommonService : ICommonService
    {
        public IUploadedFileRepository UploadedFileRepository { get; }
        public IDepartmentRepository DepartmentRepository { get; }

        public CommonService(
            IUploadedFileRepository uploadedFileRepository,
            IDepartmentRepository departmentRepository)
        {
            UploadedFileRepository = uploadedFileRepository;
        }
    }
}

因為我每個Controller都需要注入這幾個Repository,所以如果不寫service的話,每個Controller的Constructor都需要多幾行程式碼,所以我把他們封裝進了一個Service,然後注入這個Service就行。

Service也需要進行IOC註冊。

7.其他

a.使用自行實現的異常處理和異常記錄類:

 GlobalConfiguration.Configuration.Services.Add(typeof(IExceptionLogger), new MyExceptionLogger());
            GlobalConfiguration.Configuration.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());

b.啟用了Cors

c.所有的Controller預設是需要驗證的

d.採用Token Bearer驗證方式

e.預設建立一個使用者,在DatabaseInitializer.cs裡面可以看見使用者名稱密碼。

f.EF採用Code First,需要手動進行遷移。(我認為這樣最好)

g.內建把漢字轉為拼音首字母的工具,PinyinTools

h.所有上傳檔案的Model需要實現IFileEntity介面,參考程式碼中的例子。

i.所有後臺翻頁返回的結果應該是使用PaginatedItemsViewModel。

裡面有很多例子,請參考。

注意:專案啟動後顯示錯誤頁,因為我把Home頁去掉了。請訪問/Help頁檢視API列表。

過些日子可以考慮加入Swagger。