分类 DotNet Core 下的文章

背景

使用Dapper查询数据时,执行到以下代码是,提示错误。

await DbConnection.QueryAsync<MyDto>(sql);

错误信息为:

Error parsing column 0 (NAME=39fb9af0-7a6f-7b2a-26d6-4fe92a2798fc - Object)
System.Data.DataException: Error parsing column 0 (NAME=39fb9af0-7a6f-7b2a-26d6-4fe92a2798fc - Object)
 ---> System.InvalidCastException: Object must implement IConvertible.

但是调试拿到了sql语句,去Navicat下执行,完全没有问题。起初,百度找到了这篇文章,但发现我的问题并非文章中的原因造成的。

原因

该查询中,只查询了两个字段,其中一个为Char(36)类型,通过Dapper查询后转换为了string,本来char转换为string再正常不过。直到后来查询资料发现,Mysql中对Char(36)默认为Guid,但也就是说,你配置了字段类型为Char(36),它代表的意义就已经不是字符串了,转换需要单独处理。

解决

知道问题原因了,就好办了。只需要在查询是将其转换为字符串即可,或者设置转换的类型为Guid。

SELECT CAST(Uid AS CHAR) Name ...

或修改MyDto中的字段类型

...
public Guid Name { get; set; }
...

步骤

引入Volo.Abp.Emailing 包

并在模块中添加对应的依赖。

    [DependsOn(
        typeof(AbpEmailingModule)
   )]
    public class XXXXModule : AbpModule
    {

添加配置

在appsettings.json中添加如下代码:

"Settings": {
  "Abp.Mailing.Smtp.Host": "127.0.0.1",
  "Abp.Mailing.Smtp.Port": "25",
  "Abp.Mailing.Smtp.UserName": "",
  "Abp.Mailing.Smtp.Password": "",
  "Abp.Mailing.Smtp.Domain": "",
  "Abp.Mailing.Smtp.EnableSsl": "false",
  "Abp.Mailing.Smtp.UseDefaultCredentials": "true",
  "Abp.Mailing.DefaultFromAddress": "noreply@abp.io",
  "Abp.Mailing.DefaultFromDisplayName": "ABP application"
}

这里需要特别注意一下,其中的password项目,不可直接填写,abp要求必须对密码进行加密处理,如果你直接填写密码。在发送邮件时会抛出如下异常:

 The input data is not a complete block.
System.Security.Cryptography.CryptographicException: The input data is not a complete block.
   at Internal.Cryptography.UniversalCryptoDecryptor.UncheckedTransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
   at Internal.Cryptography.UniversalCryptoTransform.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
   at System.Security.Cryptography.CryptoStream.ReadAsyncCore(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken, Boolean useAsync)

参考官方文档,要么,使用ISettingManager 进行配置。如果使用appsettings.json进行配置,就需要自行加密后,填写密文。

这里说下第二种,如何进行加密呢?我们可自己下一段端代码,对密码进行加密,调试方式获取加密字符串后,再把这段代码删除即可。
代码如下_settingDefinitionManager和_settingEncryptionService注入即可,用完再删除就行。

private readonly ISettingEncryptionService _settingEncryptionService;
private readonly ISettingDefinitionManager _settingDefinitionManager;
public TestAppService(
    ISettingEncryptionService settingEncryptionService,
    ISettingDefinitionManager settingDefinitionManager)
{

    _settingEncryptionService = settingEncryptionService;
    _settingDefinitionManager = settingDefinitionManager;
}

public async Task EncryptPwd()
{

    var setting = _settingDefinitionManager.Get(EmailSettingNames.Smtp.Password);
    var psd = _settingEncryptionService.Encrypt(setting,"密码");
}

这样,得到了加密后的密码,配置上就可了。

使用

使用的时候,也是非常简单,直接依赖注入方式注入IEmailSender,然后执行SendAsync接口。

private readonly IEmailSender _emailSender;
public TestAppService(
    IEmailSender emailSender)
{
    _emailSender= emailSender;
}

public async Task SendTest()
{
    await _emailSender.SendAsync("123456789@qq.com",
        "测试",
        "正文内容.....");
}

当然,实际使用中,可能还会用到模板等内容,这里不再赘述,参考官方文档吧。

背景

在使用abp vNext开发过程中,因项目使用的是ef core作为ORM工具,而Ef的批量操作性能并不能让人满意。于是便有了使用原生sql操作数据库的需求。

使用

既然说到使用了abp vNext,那肯定利用abp的功能实现更为方便。代码如下:

方式一

第一种情况,可以获取到仓储对象,Repository。无需在意哪个实体的仓储,任意实体仓储

private async Task ExecuteSql(string sql)
{
    using (var dbContext = _coursewareRepository.GetDbContext())
    {
        var aa =await dbContext.Database.ExecuteSqlRawAsync(sql);
    }
}

方式二

正常情况下,上述方式可以满足正常使用了,但是如果使用了后台任务,在后台任务中使用sql,该方式会报cannot access a disposed object的错误,提示dbContext已被释放。可通过以下方式进行使用。

private async Task ExecuteSql(string sql)
{
    using (var scope = ServiceScopeFactory.CreateScope())
    {
        var db = scope.ServiceProvider.GetService<IdeologyFrontDbContext>();
        var a = await db.Database.ExecuteSqlRawAsync(sql);
    }
}

背景

项目开发采用的是vue+abp WebApi方式,开发过程中发现有跨域问题。

解决方案

XXXHostModule类中ConfigureServices方法下添加如下代码:

var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();

ConfigureCors(context, configuration);

ConfigureCors方法如下:


private const string DefaultCorsPolicyName = "Default";
private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddCors(options =>
    {
        options.AddPolicy(DefaultCorsPolicyName, builder =>
        {
            builder
                //.AllowAnyOrigin()
                .WithOrigins(
                    configuration["App:CorsOrigins"]
                        .Split(",", StringSplitOptions.RemoveEmptyEntries)
                        .Select(o => o.RemovePostFix("/"))
                        .ToArray()
                )
                .WithAbpExposedHeaders()
                .SetIsOriginAllowedToAllowWildcardSubdomains()
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        });
    });
}

XXXHostModule类中OnApplicationInitialization方法下添加如下代码:

var app = context.GetApplicationBuilder();

app.UseCors(DefaultCorsPolicyName);

appsettings.json中添加如下配置,根据需求进行添加,本示例为开发环境配置,所以加入了localhost:8080

  "App": {
    "CorsOrigins": "http://localhost:8080,http://10.10.10.144:8080,*"
  }

完成,这样就可以进行跨域访问了

背景

业务需要,通过WebApi返回视频,起初,直接在Controller读取视频信息,返回FileContentResult。大部分浏览器也能正常播放。但是会存在以下问题:

  • Mac系统下safari浏览器播放视频失败。
  • Html Video标签下,视频无法拖动进度,一些类似进度跳转,加载到最近一次播放进度的功能需求就无法满足。
  • 视频加载缓慢,wpf或其他客户端下,一些大视频可能会加载很长时间才能播放,而且播放卡顿严重。

原因

Mac 系统Safari浏览器在加载视频是要求视频必须支持分块加载,即必须支持Range请求头,而我们直接返回FileContentResult是不支持Range的。
同样,Video标签下不能拖动进度和客户端加载视频缓慢也是这个原因。

解决

找到原因,解决问题就相对简单了,google了一番,加上GitHub搜索,找到如下方法。废话不多说,直接上代码。

VideoStreamResult类

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

public class VideoStreamResult : FileStreamResult
{
    // default buffer size as defined in BufferedStream type
    private const int BufferSize = 0x1000;
    private string MultipartBoundary = "<qwe123>";

    public VideoStreamResult(Stream fileStream, string contentType)
        : base(fileStream, contentType)
    {

    }

    public VideoStreamResult(Stream fileStream, MediaTypeHeaderValue contentType)
        : base(fileStream, contentType)
    {

    }

    private bool IsMultipartRequest(RangeHeaderValue range)
    {
        return range != null && range.Ranges != null && range.Ranges.Count > 1;
    }

    private bool IsRangeRequest(RangeHeaderValue range)
    {
        return range != null && range.Ranges != null && range.Ranges.Count > 0;
    }

    protected async Task WriteVideoAsync(HttpResponse response)
    {
        var bufferingFeature = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
        bufferingFeature?.DisableBuffering();

        var length = FileStream.Length;

        var range = response.HttpContext.GetRanges(length);

        if (IsMultipartRequest(range))
        {
            response.ContentType = $"multipart/byteranges; boundary={MultipartBoundary}";
        }
        else
        {
            response.ContentType = ContentType.ToString();
        }

        response.Headers.Add("Accept-Ranges", "bytes");

        if (IsRangeRequest(range))
        {
            response.StatusCode = (int)HttpStatusCode.PartialContent;

            if (!IsMultipartRequest(range))
            {
                response.Headers.Add("Content-Range", $"bytes {range.Ranges.First().From}-{range.Ranges.First().To}/{length}");
            }

            foreach (var rangeValue in range.Ranges)
            {
                if (IsMultipartRequest(range)) // dunno if multipart works
                {
                    await response.WriteAsync($"--{MultipartBoundary}");
                    await response.WriteAsync(Environment.NewLine);
                    await response.WriteAsync($"Content-type: {ContentType}");
                    await response.WriteAsync(Environment.NewLine);
                    await response.WriteAsync($"Content-Range: bytes {range.Ranges.First().From}-{range.Ranges.First().To}/{length}");
                    await response.WriteAsync(Environment.NewLine);
                }

                await WriteDataToResponseBody(rangeValue, response);

                if (IsMultipartRequest(range))
                {
                    await response.WriteAsync(Environment.NewLine);
                }
            }

            if (IsMultipartRequest(range))
            {
                await response.WriteAsync($"--{MultipartBoundary}--");
                await response.WriteAsync(Environment.NewLine);
            }
        }
        else
        {
            await FileStream.CopyToAsync(response.Body);
        }
    }

    private async Task WriteDataToResponseBody(RangeItemHeaderValue rangeValue, HttpResponse response)
    {
        var startIndex = rangeValue.From ?? 0;
        var endIndex = rangeValue.To ?? 0;

        byte[] buffer = new byte[BufferSize];
        long totalToSend = endIndex - startIndex;
        int count = 0;

        long bytesRemaining = totalToSend + 1;
        response.ContentLength = bytesRemaining;

        FileStream.Seek(startIndex, SeekOrigin.Begin);

        while (bytesRemaining > 0)
        {
            try
            {
                if (bytesRemaining <= buffer.Length)
                    count = FileStream.Read(buffer, 0, (int)bytesRemaining);
                else
                    count = FileStream.Read(buffer, 0, buffer.Length);

                if (count == 0)
                    return;

                await response.Body.WriteAsync(buffer, 0, count);

                bytesRemaining -= count;
            }
            catch (IndexOutOfRangeException)
            {
                await response.Body.FlushAsync();
                return;
            }
            finally
            {
                await response.Body.FlushAsync();
            }
        }
    }

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        await WriteVideoAsync(context.HttpContext.Response);
    }
}

这里使用了一个扩展方法:

public static RangeHeaderValue GetRanges(this HttpContext context, long contentSize)
{
    RangeHeaderValue rangesResult = null;

    string rangeHeader = context.Request.Headers["Range"];

    if (!string.IsNullOrEmpty(rangeHeader))
    {
        string[] ranges = rangeHeader.Replace("bytes=", string.Empty).Split(",".ToCharArray());

        rangesResult = new RangeHeaderValue();

        for (int i = 0; i < ranges.Length; i++)
        {
            const int START = 0, END = 1;

            long endByte, startByte;

            long parsedValue;

            string[] currentRange = ranges[i].Split("-".ToCharArray());

            if (long.TryParse(currentRange[END], out parsedValue))
                endByte = parsedValue;
            else
                endByte = contentSize - 1;


            if (long.TryParse(currentRange[START], out parsedValue))
                startByte = parsedValue;
            else
            {
                startByte = contentSize - endByte;
                endByte = contentSize - 1;
            }

            rangesResult.Ranges.Add(new RangeItemHeaderValue(startByte, endByte));
        }
    }

    return rangesResult;
}

在Controller中使用:

public async Task<ActionResult> DownloadById(string filepath)
{
    ///根据业务,获取文件Stream
    var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
    return new VideoStreamResult(stream, mimeType);
}