主頁 > .NET開發 > 開始你的api:NetApiStarter

開始你的api:NetApiStarter

2020-09-20 09:46:32 .NET開發

在此之前,寫過一篇 給新手的WebAPI實踐 ,獲得了很多新人的認可,那時還是基于.net mvc,檔案生成還是自己鬧洞大開寫出來的,經過這兩年的時間,netcore的發展已經勢不可擋,自己也在不斷的學習,公司的專案也轉向了netcore,大部分也都是前后分離的架構,后端api開發居多,從中整理了一些東西在這里分享給大家,

原始碼地址:https://gitee.com/loogn/NetApiStarter,這是一個基于netcore mvc 3.0的模板專案,如果你使用的netcore 2.x,除了參考不通用外,代碼基本是可以復用的,下面介紹一下其中的功能,

登錄驗證

這里我默認使用了jwt登錄驗證,因為它足夠簡單和輕量,在netcore mvc中使用jwt驗證非常簡單,首先在startup.cs檔案中配置服務并啟用:
ConfigureServices方法中:

            var jwtSection = Configuration.GetSection("Jwt");
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidAudience = jwtSection["Audience"],
                        ValidIssuer = jwtSection["Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["SigningKey"]))
                    };
                });

Configure方法中,在UseRouting和UseEndpoints方法之前:

app.UseAuthorization();

上面我們使用到了jwt配置塊,對應appsettings.json檔案中有這樣的配置:

{
  "Jwt": {
    "SigningKey": "1234567812345678",
    "Issuer": "NetApiStarter",
    "Audience": "NetApiStarter"
  }
}

我們再操作兩步來實作登錄驗證,
一、提供一個介面生成jwt,
二、在客戶端請求頭部加上Authorization: Bearer {jwt}
我先封裝了一個生成jwt的方法

    public static class JwtHelper
    {
        public static string WriteToken(Dictionary<string, string> claimDict, DateTime exp)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppSettings.Instance.Jwt.SigningKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                issuer: AppSettings.Instance.Jwt.Issuer,
                audience: AppSettings.Instance.Jwt.Audience,
                claims: claimDict.Select(x => new Claim(x.Key, x.Value)),
                expires: exp,
                signingCredentials: creds);
            var jwt = new JwtSecurityTokenHandler().WriteToken(token);
            return jwt;
        }
}

然后在登錄服務中呼叫

        /// <summary>
        /// 登錄,獲取jwt
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public ResultObject<LoginResponse> Login(LoginRequest request)
        {
            var user = userDao.GetUser(request.Account, request.Password);
            if (user == null)
            {
                return new ResultObject<LoginResponse>("用戶名或密碼錯誤");
            }
            var dict = new Dictionary<string, string>();
            dict.Add("userid", user.Id.ToString());
            var jwt = JwtHelper.WriteToken(dict, DateTime.Now.AddDays(7));
            var response = new LoginResponse { Jwt = jwt };
            return new ResultObject<LoginResponse>(response);
        }

在Controller和Action上添加[Authorize]和[AllowAnonymous]兩個特性就可以實作登錄驗證了,

請求回應

這里請求回應的設計依然沒有使用restful風格,一是感覺太麻煩,二是真的不太懂(實事求是),所以請求還是以POST方式投遞JSON資料,回應當然也是JSON資料這個沒啥異議的,
為啥使用POST+JSON呢,主要是簡單,大家都懂,而且規則統一、繁簡皆宜,比如什么引數都不需要,就傳{},根據ID查詢文章{articleId:23},或者復雜的查詢條件和表單提交{ name:'abc', addr:{provice:'HeNan', city:'ZhengZhou'},tags:['騎馬','射箭'] } 等等都可以優雅的傳遞,
這只是我個人的風格,netcore mvc是支持其他的方式的,選自己喜歡的就行了,
下面的內容還是按照POST+JSON來說,

首先提供請求基類:

    /// <summary>
    /// 登錄用戶請求的基類
    /// </summary>
    public class LoginedRequest
    {
        #region jwt相關用戶
        private ClaimsPrincipal _claimsPrincipal { get; set; }

        public ClaimsPrincipal GetPrincipal()
        {
            return _claimsPrincipal;
        }
        public void SetPrincipal(ClaimsPrincipal user)
        {
            _claimsPrincipal = user;
        }

        public string GetClaimValue(string name)
        {
            return _claimsPrincipal?.FindFirst(name)?.Value;
        }
        #endregion

        #region 資料庫相關用戶 (如果有必要的話)
        //不用屬性是因為swagger中會顯示出來
        private User _user;
        public User GetUser()
        {
            return _user;
        }
        public void SetUser(User user)
        {
            _user = user;
        }
        #endregion
    }

這個類中說白了就是兩個手寫屬性,一個ClaimsPrincipal用來保存從jwt決議出來的用戶,一個User用來保存資料庫中完整的用戶資訊,為啥不直接使用屬性呢,上面注釋也提到了,不想在api檔案中顯示出來,這個用戶資訊是在服務層使用的,而且User不是必須的,比如jwt中的資訊夠服務層使用,不定義User也是可以的,總之這里的資訊是為服務層邏輯服務的,
我們還可以定義其他的基類,比如經常用的分頁基類:

    public class PagedRequest : LoginedRequest
    {
        public int PageIndex { get; set; }
        public int PageSize { get; set; }
    }

根據專案的實際情況還可以定義更多的基類來方便開發,

回應類使用統一的格式,這里直接提供json方便查看:

{
  "result": {
    "jwt": "string"
  },
  "success": true,
  "code": 0,
  "msg": "錯誤資訊"
}

result是具體的回應物件,如果success為false的話,result一般是null,

ActionFilter

mvc本身是一個擴展性極強的框架,層層有攔截,ActionFilter就是其中之一,IActionFilter介面有兩個方法,一個是OnActionExecuted,一個是OnActionExecuting,從命名也能看出,就是在Action的前后分別執行的方法,我們這里主要重寫OnActionExecuting方法來做兩件事:
一、將登陸資訊賦值給請求物件
二、驗證請求物件
這里說的請求物件,其型別就是LoginedRequest或者LoginedRequest的子類,看代碼:

    [AppService]
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public class MyActionFilterAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// 是否驗證引數有效性
        /// </summary>
        public bool ValidParams { get; set; } = true;

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            //由于Filters是套娃模式,使用以下邏輯保證作用域的覆寫 Action > Controller > Global
            if (context.Filters.OfType<MyActionFilterAttribute>().Last() != this)
            {
                return;
            }

            //默認只有一個引數
            var firstParam = context.ActionArguments.FirstOrDefault().Value;
            if (firstParam != null && firstParam.GetType().IsClass)
            {
                //驗證引數合法性
                if (ValidParams)
                {
                    var validationResults = new List<ValidationResult>();
                    var validationFlag = Validator.TryValidateObject(firstParam, new ValidationContext(firstParam), validationResults, false);
                    if (!validationFlag)
                    {
                        var ro = new ResultObject(validationResults.First().ErrorMessage);
                        context.Result = new JsonResult(ro);
                        return;
                    }
                }
            }

            var requestParams = firstParam as LoginedRequest;
            if (requestParams != null)
            {
                //設定jwt用戶
                requestParams.SetPrincipal(context.HttpContext.User);
                var userid = requestParams.GetClaimValue("userid");
                //如果有必要,可以每次都獲取資料庫中的用戶
                if (!string.IsNullOrEmpty(userid))
                {
                    var user = ((UserService)context.HttpContext.RequestServices.GetService(typeof(UserService))).SingleById(long.Parse(userid));
                    requestParams.SetUser(user);
                }
            }

            base.OnActionExecuting(context);
        }
    }

模型驗證這塊使用的是系統自帶的,從上面代碼也可以看出,如果請求物件定義為LoginedRequest及其子類,每次請求會填充ClaimsPrincipal,如果有必要,可以從資料庫中讀取User資訊填充,
請求經過ActionFilter時,模型驗證不通過的,直接回傳了驗證錯誤資訊,通過之后到達Action和Service時,用戶資訊已經可以直接使用了,

api檔案和日志

api檔案首選swagger了,aspnetcore 官方檔案也是使用的這個,我這里用的是Swashbuckle,首先安裝參考

Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc4
定義一個擴展類,方便把swagger注入容器中:

public static class SwaggerServiceExtensions
    {
        public static IServiceCollection AddSwagger(this IServiceCollection services)
        {
            //https://github.com/domaindrivendev/Swashbuckle.AspNetCore
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo
                {
                    Title = "My Api",
                    Version = "v1"
                });
                c.IgnoreObsoleteActions();
                c.IgnoreObsoleteProperties();
                c.DocumentFilter<SwaggerDocumentFilter>();
                //自定義型別映射
                c.MapType<byte>(() => new OpenApiSchema { Type = "byte", Example = new OpenApiByte(0) });
                c.MapType<long>(() => new OpenApiSchema { Type = "long", Example = new OpenApiLong(0L) });
                c.MapType<int>(() => new OpenApiSchema { Type = "integer", Example = new OpenApiInteger(0) });
                c.MapType<DateTime>(() => new OpenApiSchema { Type = "DateTime", Example = new OpenApiDateTime(DateTimeOffset.Now) });

                //xml注釋
                foreach (var file in Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
                {
                    c.IncludeXmlComments(file);
                }

                //Authorization的設定
                c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    In = ParameterLocation.Header,
                    Description = "請輸入驗證的jwt,示例:Bearer {jwt}",
                    Name = "Authorization",

                    Type = SecuritySchemeType.ApiKey,
                });
            });
            return services;
        }

        /// <summary>
        /// Swagger控制器描述文字
        /// </summary>
        class SwaggerDocumentFilter : IDocumentFilter
        {
            public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
            {
                swaggerDoc.Tags = new List<OpenApiTag>
                {
                    new OpenApiTag{ Name="User", Description="用戶相關"},
                    new OpenApiTag{ Name="Common", Description="公共功能"},
                };
            }
        }
    }

主要是驗證部分,加上去之后就可以在檔案中使用jwt測驗了
然后在startup.cs的ConfigureServices方法中
services.AddSwagger();
Configure方法中:

            if (env.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI(options =>
                {
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
                    options.DocExpansion(DocExpansion.None);
                });
            }

這里限制了只有在開發環境才顯示api檔案,如果是需要外部呼叫的話,可以不做這個限制,

日志組件使用Serilog,
首先也是安裝參考
Install-Package Serilog
Install-Package Serilog.AspNetCore
Install-Package Serilog.Settings.Configuration
Install-Package Serilog.Sinks.RollingFile

然后在appsettings.json中添加配置

{
  "Serilog": {
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "RollingFile",
        "Args": { "pathFormat": "logs/{Date}.log" }
      }
    ],
    "Enrich": [ "FromLogContext" ],
    "MinimumLevel": {
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    }
  },
}

更多配置請查看https://github.com/serilog/serilog-settings-configuration

上述配置會在應用程式根目錄的logs檔案夾下,每天生成一個命名類似20191129.log的日志檔案

最后要修改一下Program.cs,代替默認的日志組件

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseConfiguration(new ConfigurationBuilder().SetBasePath(Environment.CurrentDirectory).AddJsonFile("appsettings.json").Build());

                    webBuilder.UseStartup<Startup>();

                    webBuilder.UseSerilog((whbContext, configureLogger) =>
                    {
configureLogger.ReadFrom.Configuration(whbContext.Configuration);
                    });
                });

檔案分塊上傳

檔案上傳就像登錄驗證一樣常用,哪個應用還不上傳個頭像啥的,所以我也打算整合到模板專案中,如果是單純的上傳也就沒必要說了,這里主要說的是一種大檔案上傳的解決方法: 分塊上傳,

分塊上傳是需要客戶端配合的,客戶端把一個大檔案分好塊,一小塊一小塊的上傳,上傳完成之后服務端按照順序合并到一起就是整個檔案了,

所以我們先定義分塊上傳的引數:

string identifier : 檔案標識,一個檔案的唯一標識,
int chunkNumber :當前塊所以,我是從1開始的
int chunkSize :每塊大小,客戶端設定的固定值,單位為byte,一般2M左右就可以了
long totalSize:檔案總大小,單位為byte
int totalChunks:總塊數

這些引數都好理解,在服務端驗證和合并檔案時需要,

開始的時候我是這樣處理的,客戶端每上傳一塊,我會把這塊的內容寫到一個臨時檔案中,使用identifier和chunkNumber來命名,這樣就知道是哪個檔案的哪一塊了,當上傳完最后一塊之后,也就是chunkNumber==totalChunks的時候,我將所有的分塊小檔案合并到目標檔案,然后回傳url,

這個邏輯是沒什么問題,只需要一個機制保證合并檔案的時候所有塊都已上傳就可以了,為什么要這樣一個機制呢,主要是因為客戶端的上傳可能是多執行緒的,而且也不能完全保證http的回應順序和請求順序是一樣的,所以雖然上傳完最后一塊才會合并,但是還是需要一個機制判斷一下是否所有塊都上傳完畢,沒有上傳完還要等待一下(想一想怎么實作!),

后來在實際上傳程序中發現最后一塊回應會比較慢,特別是檔案很大的時候,這個也好理解,因為最后一塊上傳會合并檔案,所以需要優化一下,

這里就使用到了佇列的概念了,我們可以把每次上傳的內容都放在佇列中,然后使用另一個執行緒從佇列中讀取并寫入目標檔案,在這個場景中BlockingCollection是最合適不過的了,
我們定義一個物體類,用于保存入列的資料:

    public class UploadChunkItem
    {
        public byte[] Data { get; set; }
        public int ChunkNumber { get; set; }
        public int ChunkSize { get; set; }
        public string FilePath { get; set; }
    }

然后定義一個佇列寫入器

public class UploadChunkWriter
    {
        public static UploadChunkWriter Instance = new UploadChunkWriter();
        private BlockingCollection<UploadChunkItem> _queue;
        private int _writeWorkerCount = 3;
        private Thread _writeThread;
        public UploadChunkWriter()
        {
            _queue = new BlockingCollection<UploadChunkItem>(500);
            _writeThread = new Thread(this.Write);
        }

        public void Write()
        {
            while (true)
            {
                //單執行緒寫入
                //var item = _queue.Take();
                //using (var fileStream = File.Open(item.FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite))
                //{
                //    fileStream.Position = (item.ChunkNumber - 1) * item.ChunkSize;
                //    fileStream.Write(item.Data, 0, item.Data.Length);
                //    item.Data = https://www.cnblogs.com/loogn/p/null;
                //}

                //多執行緒寫入
                Task[] tasks = new Task[_writeWorkerCount];
                for (int i = 0; i < _writeWorkerCount; i++)
                {
                    var item = _queue.Take();
                    tasks[i] = Task.Run(() =>
                     {
                         using (var fileStream = File.Open(item.FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite))
                         {
                             fileStream.Position = (item.ChunkNumber - 1) * item.ChunkSize;
                             fileStream.Write(item.Data, 0, item.Data.Length);
                             item.Data = null;
                         }
                     });
                }
                Task.WaitAll(tasks);
            }
        }

        public void Add(UploadChunkItem item)
        {
            _queue.Add(item);
        }

        public void Start()
        {
            _writeThread.Start();
        }
    }

主要是Write方法的邏輯,呼叫_queue.Take()方法從佇列中獲取一項,如果佇列中沒有資料,這個方法會堵塞當前執行緒,這也是我們所期望的,獲取到資料之后,打開目標檔案(在上傳第一塊的時候會創建),根據ChunkNumber 和ChunkSize找到開始寫入的位置,然后把本塊資料寫入,
打開目標檔案的時候使用了FileShare.ReadWrite,表示這個檔案可以同時被多個執行緒讀取和寫入,

檔案上傳方法也簡單:

        /// <summary>
        /// 分片上傳
        /// </summary>
        /// <param name="formFile"></param>
        /// <param name="chunkNumber"></param>
        /// <param name="chunkSize"></param>
        /// <param name="totalSize"></param>
        /// <param name="identifier"></param>
        /// <param name="totalChunks"></param>
        /// <returns></returns>
        public ResultObject<UploadFileResponse> ChunkUploadfile(IFormFile formFile, int chunkNumber, int chunkSize, long totalSize,
            string identifier, int totalChunks)
        {
            var appSetting = AppSettings.Instance;
            #region 驗證
            if (formFile == null && formFile.Length == 0)
            {
                return new ResultObject<UploadFileResponse>("檔案不能為空");
            }
            if (formFile.Length > appSetting.Upload.LimitSize)
            {
                return new ResultObject<UploadFileResponse>("檔案超過了最大限制");
            }
            var ext = Path.GetExtension(formFile.FileName).ToLower();
            if (!appSetting.Upload.AllowExts.Contains(ext))
            {
                return new ResultObject<UploadFileResponse>("檔案型別不允許");
            }
            if (chunkNumber == 0 || chunkSize == 0 || totalSize == 0 || identifier.Length == 0 || totalChunks == 0)
            {
                return new ResultObject<UploadFileResponse>("引數錯誤0");
            }
            if (chunkNumber > totalChunks)
            {
                return new ResultObject<UploadFileResponse>("引數錯誤1");
            }
            if (totalSize > appSetting.Upload.TotalLimitSize)
            {
                return new ResultObject<UploadFileResponse>("引數錯誤2");
            }
            if (chunkNumber < totalChunks && formFile.Length != chunkSize)
            {
                return new ResultObject<UploadFileResponse>("引數錯誤3");
            }
            if (totalChunks == 1 && formFile.Length != totalSize)
            {
                return new ResultObject<UploadFileResponse>("引數錯誤4");
            }
            #endregion

            //寫入邏輯
            var now = DateTime.Now;
            var yy = now.ToString("yyyy");
            var mm = now.ToString("MM");
            var dd = now.ToString("dd");

            var fileName = EncryptHelper.MD5Encrypt(identifier) + ext;
            var folder = Path.Combine(appSetting.Upload.UploadPath, yy, mm, dd);
            var filePath = Path.Combine(folder, fileName);

            //執行緒安全的創建檔案
            if (!File.Exists(filePath))
            {
                lock (lockObj)
                {
                    if (!File.Exists(filePath))
                    {
                        if (!Directory.Exists(folder))
                        {
                            Directory.CreateDirectory(folder);
                        }
                        File.Create(filePath).Dispose();
                    }
                }
            }

            var data = https://www.cnblogs.com/loogn/p/new byte[formFile.Length];
            formFile.OpenReadStream().Read(data, 0, data.Length);

            UploadChunkWriter.Instance.Add(new UploadChunkItem
            {
                ChunkNumber = chunkNumber,
                ChunkSize = chunkSize,
                Data = data,
                FilePath = filePath
            });
            if (chunkNumber == totalChunks)
            {
                //等等寫入完成
                int i = 0;
                while (true)
                {
                    if (i >= 20)
                    {
                        return new ResultObject
                        {
                            Success = false,
                            Msg = $"上傳失敗,總大小:{totalSize},實際大小:{new FileInfo(filePath).Length}",
                            Result = new UploadFileResponse { Url = "" }
                        };
                    }
                    if (new FileInfo(filePath).Length != totalSize)
                    {
                        Thread.Sleep(TimeSpan.FromMilliseconds(1000));
                        i++;
                    }
                    else
                    {
                        break;
                    }
                }
                var fileUrl = $"{appSetting.RootUrl}{appSetting.Upload.RequestPath}/{yy}/{mm}/{dd}/{fileName}";
                var response = new UploadFileResponse { Url = fileUrl };
                return new ResultObject(response);
            }
            else
            {
                return new ResultObject
                {
                    Success = true,
                    Msg = "uploading...",
                    Result = new UploadFileResponse { Url = "" }
                };
            }
        }

撇開上面的引數驗證,主要邏輯也就是三個,一是創建目標檔案,二是分塊資料加入佇列,三是最后一塊的時候要驗證檔案的完整性(也就是所有的塊都上傳了,并都寫入到了目標檔案)

創建目標檔案需要保證執行緒安全,這里使用了雙重檢查加鎖機制,雙重檢查的優點是避免了不必要的加鎖情況,

完整性我只是驗證了檔案的大小,這只是一種簡單的機制,一般是夠用了,別忘了我們的介面都是受jwt保護的,包括這里的上傳檔案,如果要求更高的話,可以讓客戶端傳參整個檔案的md5值,然后服務端驗證合并之后檔案的md5是否和客戶端給的一致,

最后要開啟寫入執行緒,可以在Startup.cs的Configure方法中開啟:

UploadChunkWriter.Instance.Start();

經過這樣的整改,上傳速度溜溜的,最后一塊也不用長時間等待啦!

(專案中當然也包含了不分塊上傳)

其他功能

自從netcore提供了依賴注入,我也習慣了這種寫法,不過在建構式中寫一堆注入實在是難看,而且既要宣告欄位接收,又要寫引數賦值,挺麻煩的,于是乎自己寫了個小組件,已經用于手頭所有的專案,當然也包含在了NetApiStarter中,不僅解決了屬性和欄位注入,同時也解決了實作多介面注入的問題,以及一個介面多個實作精準注入的問題,詳細說明可查看專案檔案Autowired.Core,

如果你聽過MediatR,那么這個功能不需要介紹了,專案中包含一個應用程式級別的事件發布和訂閱的功能,具體使用可查看檔案AppEventService,

如果你聽過AutoMapper,那么這個功能也不需要介紹了,專案中包含一個SimpleMapper,代碼不多功能還行,支持嵌套類、陣列、IList<>、IDictionary<,>物體映射在多層資料傳輸的時候可謂是必不可少的功能,用法嘛就不說了,只有一個Map方法太簡單了

重中之重

如果你感覺這個專案對你、或者其他人(You or others,沒毛病)有稍許幫助,請給個Star好嗎!

NetApiStarter倉庫地址:https://gitee.com/loogn/NetApiStarter

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/87849.html

標籤:.NET Core

上一篇:記錄一次Mac VSCode運行Grpc模板專案

下一篇:Aso.Net Core 的配置系統Configuration

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more