• asp.net web api 2.2 基础框架(带例子)


    链接:https://github.com/solenovex/asp.net-web-api-2.2-starter-template

    简介

    这个是我自己编写的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 }));
            }
        }
    }
    View Code

    所建立的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; }
        }
    }
    View Code

    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);
            }
        }
    }
    View Code

    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);
            }
        }
    }
    View Code

     

    与普通的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; }
        }
    }
    View Code

    其中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);
            }
        }
    }
    View Code

    针对树形结构的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);
            }
        }
    }
    View Code

    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; }
            
            
        }
    }
    View Code

    其中“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();
        }
    }
    View Code

    用的时候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)
            {
            }
        }
    }
    View Code

    代码很简单,但是它已经包含了常见的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);
            }
            
        }
    }
    View Code

    我相信这个泛型类你们都应该能看明白,如果不明白可以@我。通过继承这个类,所有的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;
            }
        }
    }
    View Code

    在里面我们也可以看见我把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; }
        }
    }
    View Code

    同样,它要继承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>();
                
            }
        }
    }
    View Code
    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>();
                
            }
        }
    }
    View Code

    高级功能还是要参考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();
            }
        }
    }
    View Code

    这是比较标准的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
    }
    View Code

    这个基类里面可以有很多东西,目前,它可以获取当前用户名,当前时间,当前用户(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);
            }
        }
    }
    View Code

    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;
            }
        }
    }
    View Code

    因为我每个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());
    View Code

    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。

  • 相关阅读:
    physicslectureGriavity
    electromagnetic
    dp
    physicsmechanic wave
    C# 2.0 Specification(迭代器)(二)
    C#类、接口、虚方法和抽象方法接口与抽象类的区别实例
    web.config connectionStrings 数据库连接字符串的解释(转载)
    onpropertychange事件
    C#中ParameterizedThreadStart和ThreadStart区别
    C# 文件操作全收录
  • 原文地址:https://www.cnblogs.com/cgzl/p/aspnet-web-api-starter-template.html
Copyright © 2020-2023  润新知