八、Abp vNext 基礎篇丨標籤聚合功能

初久的私房菜發表於2021-09-13

介紹

本章節先來把上一章漏掉的上傳檔案處理下,然後實現Tag功能。

上傳檔案

上傳檔案其實不含在任何一個聚合中,它屬於一個獨立的輔助性功能,先把抽象介面定義一下,在Bcvp.Blog.Core.Application.Contracts層Blog內新建File資料夾。

一個是根據檔案name獲取檔案,一個是建立檔案,另外BlogWebConsts是對上傳檔案的約束。

    public interface IFileAppService : IApplicationService
    {
        Task<RawFileDto> GetAsync(string name);

        Task<FileUploadOutputDto> CreateAsync(FileUploadInputDto input);
    }


    public class RawFileDto
    {
        public byte[] Bytes { get; set; }

        public bool IsFileEmpty => Bytes == null || Bytes.Length == 0;

        public RawFileDto()
        {

        }

        public static RawFileDto EmptyResult()
        {
            return new RawFileDto() { Bytes = new byte[0] };
        }
    }


    public class FileUploadInputDto
    {
        [Required]
        public byte[] Bytes { get; set; }

        [Required]
        public string Name { get; set; }
    }


    public class FileUploadOutputDto
    {
        public string Name { get; set; }

        public string WebUrl { get; set; }
    }


    public class BlogWebConsts
    {
        public class FileUploading
        {
            /// <summary>
            /// Default value: 5242880
            /// </summary>
            public static int MaxFileSize { get; set; } = 5242880; //5MB

            public static int MaxFileSizeAsMegabytes => Convert.ToInt32((MaxFileSize / 1024f) / 1024f);
        }
    }

Bcvp.Blog.Core.Application層實現抽象介面

    public class FileAppService : CoreAppService, IFileAppService
    {
        // Nuget: Volo.Abp.BlobStoring   https://docs.abp.io/en/abp/latest/Blob-Storing
        protected IBlobContainer<BloggingFileContainer> BlobContainer { get; }

        public FileAppService(
            IBlobContainer<BloggingFileContainer> blobContainer)
        {
            BlobContainer = blobContainer;
        }

        public virtual async Task<RawFileDto> GetAsync(string name)
        {
            Check.NotNullOrWhiteSpace(name, nameof(name));

            return new RawFileDto
            {
                Bytes = await BlobContainer.GetAllBytesAsync(name)
            };
        }

        public virtual async Task<FileUploadOutputDto> CreateAsync(FileUploadInputDto input)
        {
            if (input.Bytes.IsNullOrEmpty())
            {
                ThrowValidationException("上傳檔案為空!", "Bytes");
            }

            if (input.Bytes.Length > BlogWebConsts.FileUploading.MaxFileSize)
            {
                throw new UserFriendlyException($"檔案大小超出上限 ({BlogWebConsts.FileUploading.MaxFileSizeAsMegabytes} MB)!");
            }

            if (!ImageFormatHelper.IsValidImage(input.Bytes, FileUploadConsts.AllowedImageUploadFormats))
            {
                throw new UserFriendlyException("無效的圖片格式!");
            }

            var uniqueFileName = GenerateUniqueFileName(Path.GetExtension(input.Name));

            await BlobContainer.SaveAsync(uniqueFileName, input.Bytes);

            return new FileUploadOutputDto
            {
                Name = uniqueFileName,
                WebUrl = "/api/blog/files/www/" + uniqueFileName
            };
        }

        private static void ThrowValidationException(string message, string memberName)
        {
            throw new AbpValidationException(message,
                new List<ValidationResult>
                {
                    new ValidationResult(message, new[] {memberName})
                });
        }

        protected virtual string GenerateUniqueFileName(string extension, string prefix = null, string postfix = null)
        {
            return prefix + GuidGenerator.Create().ToString("N") + postfix + extension;
        }
    }



    public class FileUploadConsts
    {
        public static readonly ICollection<ImageFormat> AllowedImageUploadFormats = new Collection<ImageFormat>
        {
            ImageFormat.Jpeg,
            ImageFormat.Png,
            ImageFormat.Gif,
            ImageFormat.Bmp
        };

        public static string AllowedImageFormatsJoint => string.Join(",", AllowedImageUploadFormats.Select(x => x.ToString()));
    }


    public class ImageFormatHelper
    {
        public static ImageFormat GetImageRawFormat(byte[] fileBytes)
        {
            using (var memoryStream = new MemoryStream(fileBytes))
            {
                return System.Drawing.Image.FromStream(memoryStream).RawFormat;
            }
        }

        public static bool IsValidImage(byte[] fileBytes, ICollection<ImageFormat> validFormats)
        {
            var imageFormat = GetImageRawFormat(fileBytes);
            return validFormats.Contains(imageFormat);
        }
    }

結構目錄如下

專案結構

思考

這個介面的建立檔案和返回都是用的byte這個適用於服務間呼叫,但是如果我們是前端呼叫根本沒法用,我們傳統開發的上傳檔案都是通過IFormFile來做的這裡咋辦?

ABP為我們提供Bcvp.Blog.Core.HttpApi遠端服務層,用於定義 HTTP APIs,在Controllers資料夾下建立BlogFilesController控制器,簡單點理解就是檔案建立還是由上面的FileAppService來完成,我們通過BlogFilesController擴充套件了遠端服務傳輸檔案的方式。

    [RemoteService(Name = "blog")] // 遠端服務的組名
    [Area("blog")]// Mvc裡的區域
    [Route("api/blog/files")] //Api路由
    public class BlogFilesController : AbpController, IFileAppService
    {
        private readonly IFileAppService _fileAppService;

        public BlogFilesController(IFileAppService fileAppService)
        {
            _fileAppService = fileAppService;
        }

        [HttpGet]
        [Route("{name}")]
        public Task<RawFileDto> GetAsync(string name)
        {
            return _fileAppService.GetAsync(name);
        }

        [HttpGet]
        [Route("www/{name}")]
        public async Task<FileResult> GetForWebAsync(string name) 
        {
            var file = await _fileAppService.GetAsync(name);
            return File(
                file.Bytes,
                MimeTypes.GetByExtension(Path.GetExtension(name))
            );
        }

        [HttpPost]
        public Task<FileUploadOutputDto> CreateAsync(FileUploadInputDto input)
        {
            return _fileAppService.CreateAsync(input);
        }


        [HttpPost]
        [Route("images/upload")]
        public async Task<JsonResult> UploadImage(IFormFile file)
        {
            //TODO: localize exception messages

            if (file == null)
            {
                throw new UserFriendlyException("沒找到檔案");
            }

            if (file.Length <= 0)
            {
                throw new UserFriendlyException("上傳檔案為空");
            }

            if (!file.ContentType.Contains("image"))
            {
                throw new UserFriendlyException("檔案不是圖片型別");
            }

            var output = await _fileAppService.CreateAsync(
                new FileUploadInputDto
                {
                    Bytes = file.GetAllBytes(),
                    Name = file.FileName
                }
            );

            return Json(new FileUploadResult(output.WebUrl));
        }

    }


  
    public class FileUploadResult
    {
        public string FileUrl { get; set; }

        public FileUploadResult(string fileUrl)
        {
            FileUrl = fileUrl;
        }
    }

標籤聚合

標籤應該是最簡單的了,它就一個功能,獲取當前部落格下的標籤列表,在之前我們寫文章聚合的時候已經把標籤的倉儲介面和實現都完成了,這裡補一下業務介面。

    public interface ITagAppService : IApplicationService
    {
        Task<List<TagDto>> GetPopularTags(Guid blogId, GetPopularTagsInput input);

    }
    
    public class GetPopularTagsInput
    {
        public int ResultCount { get; set; } = 10;

        public int? MinimumPostCount { get; set; }
    }


    public class TagAppService : CoreAppService, ITagAppService
    {
        private readonly ITagRepository _tagRepository;

        public TagAppService(ITagRepository tagRepository)
        {
            _tagRepository = tagRepository;
        }

        public async Task<List<TagDto>> GetPopularTags(Guid blogId, GetPopularTagsInput input)
        {
            var postTags = (await _tagRepository.GetListAsync(blogId)).OrderByDescending(t=>t.UsageCount)
                .WhereIf(input.MinimumPostCount != null, t=>t.UsageCount >= input.MinimumPostCount)
                .Take(input.ResultCount).ToList();

            return new List<TagDto>(
                ObjectMapper.Map<List<Tag>, List<TagDto>>(postTags));
        }
    }



查缺補漏

前面寫了這麼多結果忘了配置實體對映了,我們在AutoMapper的時候是需要配置Dto和實體的對映才是使用的,在Bcvp.Blog.Core.Application層有一個CoreApplicationAutoMapperProfile.cs,把漏掉的對映配置補一下。

        public CoreApplicationAutoMapperProfile()
        {
            /* You can configure your AutoMapper mapping configuration here.
             * Alternatively, you can split your mapping configurations
             * into multiple profile classes for a better organization. */

            CreateMap<BlogCore.Blogs.Blog, BlogDto>();
            CreateMap<IdentityUser, BlogUserDto>();

            CreateMap<Post, PostCacheItem>().Ignore(x => x.CommentCount).Ignore(x => x.Tags);
            CreateMap<Post, PostWithDetailsDto>().Ignore(x => x.Writer).Ignore(x => x.CommentCount).Ignore(x => x.Tags);
            CreateMap<PostCacheItem, PostWithDetailsDto>()
                .Ignore(x => x.Writer)
                .Ignore(x => x.CommentCount)
                .Ignore(x => x.Tags);

            CreateMap<Tag, TagDto>();

        }

結語

本節知識點:

  • 1.遠端服務層的使用
  • 2.標籤聚合的完成
  • 3.AutoMapper配置

聯絡作者:加群:867095512 @MrChuJiu

公眾號

相關文章