0%

webform 整合 autofac

 
vim

參考官方

Autofac

安裝 Autofac.Web

1
Install-Package Autofac.Web -Version 6.0.0

先在 web.config 加上這段 , 官方寫推薦 IIS6 & IIS7 的設定都加 , 我懶得加

1
2
3
4
5
6
7
8
9
10
11
12
13
<system.webServer>
<!-- This section is used for IIS7 -->
<modules>
<add
name="ContainerDisposal"
type="Autofac.Integration.Web.ContainerDisposalModule, Autofac.Integration.Web"
preCondition="managedHandler"/>
<add
name="PropertyInjection"
type="Autofac.Integration.Web.Forms.PropertyInjectionModule, Autofac.Integration.Web"
preCondition="managedHandler"/>
</modules>
</system.webServer>

建立類別 Box

1
2
3
4
public class Box
{
public string Name { get; set; }
}

建立 interface IRepoBoxRepo

IRepo

1
2
3
4
public interface IRepo
{
List<Box> GetAll();
}

BoxRepo

1
2
3
4
5
6
7
8
9
10
11
12
  public class BoxRepo : IRepo
{
//理論上這邊會寫 ado.net or dapper or ef 的程式碼
public List<Box> GetAll()
{
return new List<Box> {
new Box { Name = "Car 1" } ,
new Box { Name = "Car 2" } ,
new Box { Name = "Car 3" } ,
};
}
}

接著找到 Global 實現這個介面 IContainerProviderAccessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Global : HttpApplication, IContainerProviderAccessor
{
// Provider that holds the application container.
static IContainerProvider _containerProvider;

// Instance property that will be used by Autofac HttpModules
// to resolve and inject dependencies.
public IContainerProvider ContainerProvider
{
get { return _containerProvider; }
}

void Application_Start( object sender, EventArgs e )
{
// Build up your application container and register your dependencies.
var builder = new ContainerBuilder();
builder.RegisterType<BoxRepo>().As<IRepo>().InstancePerRequest();

//builder.RegisterType<BoxService>().As<IBoxService>().
// UsingConstructor(typeof(IRepo)).InstancePerRequest();
// ... continue registering dependencies...

// Once you're done registering things, set the container
// provider up with your registrations.
_containerProvider = new ContainerProvider( builder.Build() );


// Code that runs on application startup
RouteConfig.RegisterRoutes( RouteTable.Routes );
BundleConfig.RegisterBundles( BundleTable.Bundles );
}
}

然後撰寫 Page 的程式碼 , 至此 Repository 就可以直接拿出資料了 , 可是實際上應該要多加個 Service 層比較優 , 不過老案子可能會沒這些

1
2
3
4
5
6
7
8
9
10
public partial class _Default : Page
{
public IRepo BoxRepo { get; set; }
//public IBoxService BoxService { get; set; }
protected void Page_Load( object sender, EventArgs e )
{
var boxs = BoxRepo.GetAll();
boxs.ForEach( x => Console.WriteLine(x.Name));
}
}

接著建立 IBoxService

1
2
3
4
public interface IBoxService : IRepo
{
Box GetFirst();
}

然後實現他 , 這裡用建構子注入 IRepo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BoxService : IBoxService
{
private readonly IRepo _boxRepo;
public BoxService( IRepo boxRepo )
{
_boxRepo = boxRepo;
}
public List<Box> GetAll()
{
return _boxRepo.GetAll();
}

public Box GetFirst()
{
return _boxRepo.GetAll().FirstOrDefault();
}
}

一樣回到 Global 加入 DI 設定的程式碼 , 看看要不要明確設定 UsingConstructor 不這樣寫應該可以

1
2
builder.RegisterType<BoxService>().As<IBoxService>()
.UsingConstructor(typeof(IRepo)).InstancePerRequest();

接著調整 Page 就搞定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class _Default : Page
{
//public IRepo BoxRepo { get; set; }
public IBoxService BoxService { get; set; }
protected void Page_Load( object sender, EventArgs e )
{
//var boxs = BoxRepo.GetAll();
//boxs.ForEach( x => Console.WriteLine(x.Name));

var boxs = BoxService.GetAll();
var box = BoxService.GetFirst();

}
}

整合 NLog

webform

安裝 NLog

1
Install-Package NLog -Version 4.7.13

接著在 Global 註冊 NLog

1
builder.RegisterType<Logger>().As<ILogger>().InstancePerRequest();

但是會炸 error , 因為他的建構子是 protected internal

1
Autofac.Core.Activators.Reflection.NoConstructorsFoundException: 'No accessible constructors were found for the type 'NLog.Logger'.'

最無腦爆力法直接繼承 Logger 然後送他一個 public 的建構子即可

1
2
3
4
public class MyLogger : Logger
{
public MyLogger(){}
}

Global 註冊改這樣

1
builder.RegisterType<MyLogger>().As<ILogger>().InstancePerRequest();

另外如果想要在自己的物件也用屬性注入的話修改這樣 , 因為老 code 很容易有建構子地獄發生 , 所以插 Log 改用屬性注入 , 不然寫到哭

1
builder.RegisterType<BoxService>().As<IBoxService>().PropertiesAutowired().InstancePerRequest();

最後 Default Page 修改這樣就搞定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial class _Default : Page
{
public IBoxService BoxService { get; set; }

public ILogger Logger { get; set; }
protected void Page_Load( object sender, EventArgs e )
{
Logger.Info( "test log default page" );
//var boxs = BoxRepo.GetAll();
//boxs.ForEach( x => Console.WriteLine(x.Name));

var box = BoxService.GetFirst();
//Logger.Info(box.Name);
var boxs = BoxService.GetAll();
}
}

不然就是參考老外多包一個代理用的中間層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface ILoggerService<T>
{
void Info(string message);

void Debug(string message);
}

public class LoggerService<T> : ILoggerService<T>
{
public ILogger logger { get; set; }

public LoggerService()
{
logger = LogManager.GetLogger(typeof(T).FullName);
}

public void Debug(string message)
{
logger.Debug(message);
}

public void Info(string message)
{
logger.Info(message);
}
}

最後註冊進去就搞定了

1
builder.RegisterGeneric(typeof(LoggerService<>)).As(typeof(ILoggerService<>)).InstancePerRequest();

webapi

想要在 GlobalApplication_Error 讓 Log 有作用 , 生命週期要選 InstancePerLifetimeScope 才生效

1
builder.RegisterType<MyLogger>().As<ILogger>().InstancePerLifetimeScope();

測試 Application_Error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void Application_Error(object sender, EventArgs e)
{
var rawUrl = Request.RawUrl;
var ex = Server.GetLastError();

var logger = _containerProvider.ApplicationContainer.Resolve<ILogger>();
logger.Error(ex);


Debug.WriteLine("RawUrl: {0}", rawUrl);
Debug.WriteLine("Ex: {0}", ex.Message);
Debug.WriteLine("StackTrace: {0}", ex.StackTrace);

}

可是 web api 不會直接走 Application_Error , 要加上 filter , 但因為跟 autofac 進行整合 , 所以要加上 IAutofacExceptionFilter 參考官網

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoggingActionFilter : IAutofacExceptionFilter
{
readonly ILogger _logger;

public LoggingActionFilter(ILogger logger)
{
_logger = logger;
}

public Task OnExceptionAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
_logger.Error(actionExecutedContext.Exception);
return Task.FromResult(0);
}
}

接著設定 DI 屬性注入

1
2
3
builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger>()))
.AsWebApiExceptionFilterForAllControllers()
.InstancePerRequest();

注意還要有設定這段才會正確生效

1
2
var config = GlobalConfiguration.Configuration;
builder.RegisterWebApiFilterProvider(config);

如果不想用 Global 的方式的話 , 其實也可以直接繼承 ExceptionFilterAttribute 然後 override OnException
特別注意到 namespace System.Web.Http.Filters

1
2
3
4
5
6
7
8
9
public class LogExceptionFilterAttribute : ExceptionFilterAttribute
{
public ILogger Logger { get; set; }

public override void OnException(HttpActionExecutedContext context)
{
Logger.Error(context.Exception);
}
}

注意一樣要設定 builder.RegisterWebApiFilterProvider(config); , 接著掛載 AttributeController 上即可

1
2
3
4
5
6
7
8
9
10
11
[LogExceptionFilter]
public class HaController : ApiController
{
[HttpGet]
[Route("ha")]
public string Ha()
{
throw new Exception("Ha");
return "HA";
}
}

改用 Microsoft.Extensions.Logging

首先安裝 NLog.Config NLog.Extensions.Logging NLog.Web
參考大神NLog官方
舊版 web api 忘了怎麼寫可以參考老外 or 這篇

注意引用命名空間 using Microsoft.Extensions.Logging;

接著設定 Autofac 整合 , 修改 Application_Start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

//把 NLog 的 Provider 新增進去
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(new NLogLoggerProvider());

var builder = new ContainerBuilder();

//這行是必須的
builder.RegisterInstance(loggerFactory).As<ILoggerFactory>().SingleInstance();
//用泛型註冊
builder.RegisterGeneric(typeof(Logger<>)).As(typeof(ILogger<>));


builder.RegisterType<TestAttribute>().PropertiesAutowired();
builder.RegisterType<LogExceptionFilterAttribute>().PropertiesAutowired();


builder.Register(c => new LoggingActionFilter(c.Resolve<ILogger<LoggingActionFilter>>()))
.AsWebApiExceptionFilterForAllControllers()
.InstancePerRequest();

builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
var config = GlobalConfiguration.Configuration;
builder.RegisterWebApiFilterProvider(config);


var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);

//清除 web api xml
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();

}

接著把要用 Log 的類別改成這樣即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoggingActionFilter : IAutofacExceptionFilter
{
readonly ILogger<LoggingActionFilter> _logger;

public LoggingActionFilter(ILogger<LoggingActionFilter> logger)
{
_logger = logger;
}

public Task OnExceptionAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
_logger.LogError(actionExecutedContext.Exception.Message.ToString());
return Task.FromResult(0);
}
}

然後 Controller 大概改這樣 , action 裡面的實作就自行調整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[RoutePrefix("api/ha")]
public class HaController : ApiController
{
readonly ILogger<HaController> _logger;
public HaController(ILogger<HaController> logger)
{
_logger = logger;
}

[HttpGet]
[Route("ha")]
public IHttpActionResult Ha()
{
_logger.LogError("測試 controller & action");

try
{
throw new Exception("test");
List<string> list = new List<string>();
list.Add("haha");
return Ok(list);
}
catch (Exception ex)
{
_logger.LogError(ex.ToString());
return StatusCode(HttpStatusCode.InternalServerError);
}
}
}

後來遇到一個雷用 Json 當 Layout 時出現中文亂碼 , 加上 escapeUnicode="false" 還有設定 encoding="utf-8"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">

<extensions>
<add assembly="NLog.Web"/>
</extensions>


<!-- optional, add some variables
https://github.com/nlog/NLog/wiki/Configuration-file#variables
-->
<variable name="myvar" value="myvalue"/>

<!--
See https://github.com/nlog/nlog/wiki/Configuration-file
for information on customizing logging rules and outputs.
-->
<targets>

<!--
add your targets here
See https://github.com/nlog/NLog/wiki/Targets for possible targets.
See https://github.com/nlog/NLog/wiki/Layout-Renderers for the possible layout renderers.
-->

<!--
Write events to a file with the date in the filename.
<target xsi:type="File" name="f" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
-->
<target name="file" xsi:type="File"
layout="${longdate} ${logger} ${message}${exception:format=ToString}"
fileName="${basedir}/logs/${shortdate}.log"
encoding="utf-8" />

<target name="jsonfile" xsi:type="File" fileName="${basedir}/logs/${shortdate}.json" encoding="utf-8">
<layout xsi:type="JsonLayout">
<attribute name="time" layout="${date:format=O}" />
<attribute name="message" layout="${message}" escapeUnicode="false"/>
<attribute name="logger" layout="${logger}"/>
<attribute name="level" layout="${level}"/>

<attribute name="${aspnet-appbasepath}" layout="${aspnet-appbasepath}"/>
<attribute name="${aspnet-application}" layout="${aspnet-application}"/>
<attribute name="${aspnet-item}" layout="${aspnet-item}"/>
<attribute name="${aspnet-mvc-action}" layout="${aspnet-mvc-action}"/>
<attribute name="${aspnet-mvc-controller}" layout="${aspnet-mvc-controller}"/>
<attribute name="${aspnet-request}" layout="${aspnet-request}"/>
<attribute name="${aspnet-request-cookie}" layout="${aspnet-request-cookie}"/>
<attribute name="${aspnet-request-form}" layout="${aspnet-request-form}"/>
<attribute name="${aspnet-request-headers}" layout="${aspnet-request-headers}"/>
<attribute name="${aspnet-request-host}" layout="${aspnet-request-host}"/>
<attribute name="${aspnet-request-ip}" layout="${aspnet-request-ip}"/>
<attribute name="${aspnet-request-method}" layout="${aspnet-request-method}"/>
<attribute name="${aspnet-request-posted-body}" layout="${aspnet-request-posted-body}"/>
<attribute name="${aspnet-request-querystring}" layout="${aspnet-request-querystring}"/>
<attribute name="${aspnet-request-referrer}" layout="${aspnet-request-referrer}"/>
<attribute name="${aspnet-request-routeparameters}" layout="${aspnet-request-routeparameters}"/>
<attribute name="${aspnet-request-url}" layout="${aspnet-request-url}"/>
<attribute name="${aspnet-request-useragent}" layout="${aspnet-request-useragent}"/>
<attribute name="${aspnet-response-statuscode}" layout="${aspnet-response-statuscode}"/>
<attribute name="${aspnet-session}" layout="${aspnet-session}"/>
<attribute name="${aspnet-sessionid}" layout="${aspnet-sessionid}"/>
<attribute name="${aspnet-traceidentifier}" layout="${aspnet-traceidentifier}"/>
<attribute name="${aspnet-user-authtype}" layout="${aspnet-user-authtype}"/>
<attribute name="${aspnet-user-identity}" layout="${aspnet-user-identity}"/>
<attribute name="${aspnet-user-isauthenticated}" layout="${aspnet-user-isauthenticated}"/>
<attribute name="${aspnet-webrootpath}" layout="${aspnet-webrootpath}"/>
<attribute name="${iis-site-name}" layout="${iis-site-name}"/>
</layout>
</target>

</targets>

<rules>
<!-- add your logging rules here -->

<!--
Write all events with minimal level of Debug (So Debug, Info, Warn, Error and Fatal, but not Trace) to "f"
<logger name="*" minlevel="Debug" writeTo="f" />
-->
<logger name="*" minlevel="Debug" writeTo="file" />
<logger name="*" minlevel="Debug" writeTo="jsonfile" />
</rules>
</nlog>

為了讓 Error 統一輸出自己想要的格式參考這篇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class GlobalExceptionHandler : ExceptionHandler
{
//要順便 Log 的話加上去
//public ILogger<GlobalExceptionHandler> Logger { get; set; }
public override void Handle(ExceptionHandlerContext context)
{
//Logger.LogError(context.Exception.ToString());

var info = new ErrorInformation
{
Message = "伺服器內部發生錯誤" ,
ErrorDate = DateTime.UtcNow
};
var result = context.Request.CreateResponse( HttpStatusCode.InternalServerError, info,
System.Net.Http.Formatting.JsonMediaTypeFormatter.DefaultMediaType);
context.Result = new ResponseMessageResult(result);
}
}

看看要不要順便也把 Log 插裡面 , 要的話需要用 di container 去拿

1
2
builder.RegisterType<GlobalExceptionHandler>().PropertiesAutowired();
config.Services.Replace(typeof(IExceptionHandler), container.Resolve<GlobalExceptionHandler>());

如果不用 DI 拿的話 config 記得要多加上這樣

1
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());

其他地雷

如果有遇到噴這個 error 就檢查看看型別是否設定有錯誤

1
2
None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'WebApplication3.BoxService' can be invoked with the available services and parameters:
Cannot resolve parameter 'WebApplication3.BoxRepo boxRepo' of constructor 'Void .ctor(WebApplication3.BoxRepo)'.

建構子有問題

1
Cannot choose between multiple constructors with equal length 2 on type 'WebApplication3.BoxService'. Select the constructor explicitly, with the UsingConstructor() configuration method, when the component is registered.

這兩個順序會讓 autofac 搞不懂要找誰 , 老 code 特別容易發生這種問題

1
2
3
4
5
6
7
8
9
10
11
public BoxService( IRepo boxRepo, ILogger logger )
{
_boxRepo = boxRepo;
_logger = logger;
}

public BoxService( ILogger logger ,IRepo boxRepo )
{
_boxRepo = boxRepo;
_logger = logger;
}

使用 UsingConstructor 告訴他建構子用哪個即可搞定

1
2
3
builder.RegisterType<BoxService>().As<IBoxService>()
.UsingConstructor(typeof(IRepo) , typeof(ILogger))
.PropertiesAutowired().InstancePerRequest();

SignalR

官方文件好像沒更新 , 應該是要安裝 Autofac.SignalR2 以及 Autofac.Owin

1
Install-Package Autofac.SignalR2 -Version 6.0.0

這裡特別要注意到 , 如果整合 asp.net webform 的話 , DI 的設定是直接寫在 Global 裡面 , 而 SignalR 則是寫在 Startup 類別裡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Startup
{
public void Configuration( IAppBuilder app )
{
var builder = new ContainerBuilder();

builder.RegisterHubs( Assembly.GetExecutingAssembly() );

//註冊繼承自 NLog 把建構子改為 public 的自訂類別
builder.RegisterType<MyLogger>().As<ILogger>();

//註冊 repository
builder.RegisterType<BoxRepo>().As<IRepo>();

//設定 services
builder.RegisterType<BoxService>().As<IBoxService>()
.UsingConstructor( typeof( IRepo ), typeof( ILogger ) );
//.InstancePerLifetimeScope();

var container = builder.Build();
var resolver = new AutofacDependencyResolver( container );

// Any connection or hub wire up and configuration should go here
app.UseAutofacMiddleware(container);

app.Map( "/signalr", map =>
{
map.UseCors( CorsOptions.AllowAll );
var config = new HubConfiguration { Resolver = resolver };
map.RunSignalR(config);
} );

}
}

接著注入物件到 hub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TestHub : Hub
{
private readonly ILifetimeScope _hubLifetimeScope;
private readonly ILogger _logger;
private readonly IBoxService _boxService;

//可以這樣寫
//public TestHub( ILifetimeScope lifetimeScope )
//{
// // Create a lifetime scope for the hub.
// _hubLifetimeScope = lifetimeScope.BeginLifetimeScope();

// // Resolve dependencies from the hub lifetime scope.
// _logger = _hubLifetimeScope.Resolve<ILogger>();

// _boxService = _hubLifetimeScope.Resolve<IBoxService>();
//}

//這樣寫也可以
public TestHub(IBoxService boxService , ILogger logger)
{
_boxService = boxService;
_logger = logger;
}


//給 client 呼叫 server 上的方法
public void LaSai( string message )
{
_logger.Info( "test log" );
var result = _boxService.GetAll();
//server 丟一些 result 回去給 client
Clients.All.hello( result );
}
}

最後是前端呼叫 laSai 這個方法時 , 因為在 server 內有這句 Clients.All.hello( result ); 所以前端會把得到的 json 物件印在畫面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="Scripts/jquery-3.4.1.js"></script>
<script src="Scripts/jquery.signalR-2.2.2.js"></script>
<script src="/signalr/hubs"></script>

<!--使用靜態生出來的-->
<!--<script src="Scripts/server.js"></script>-->
</head>
<body>
<script>

//如果是其他站台要用的話要加上這句
//$.connection.hub.url = 'http://localhost:23007/signalr';
var chat = $.connection.testHub;

$.connection.hub.start().done(function () {
$(document).on('click' , '#btn', function () {
chat.server.laSai('la di sai');
})
})

chat.client.hello = function (resp) {
console.log(resp);
resp.forEach(x => {
$('#display').append(`<p>${x.Name}</p>`);
});
}
</script>
<div id="display"></div>
<button id="btn">go</button>
</body>
</html>

SignalR 後續

常常有個疑惑 , 不曉得怎樣定時去撈 DB 然後用 signalr 傳給前端 , 於是看到了這篇文章
主要就是實現了 IRegisteredObject 介面 , 然後就可以在 server 裡面定時去執行某些任務
所以特別注意到 HostingEnvironment.RegisterObject 當插入這個以後 server side (IIS) 就會固定每隔 N 秒執行撈 sql 的動作

另外有個雷 , 因為我有用 autofac 導致執行的時候雖然會觸發 timer 內的事件 , 結果 signalr server side 裡面的 function 卻不 work
所以要參考這篇去調整設定 , 不然是吃不到 DI 的

Startup 內的 Configuration 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var builder = new ContainerBuilder();

//註冊 signalr 的 hub
builder.RegisterHubs(Assembly.GetExecutingAssembly());

//把 NLog 的 Provider 新增進去
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(new NLogLoggerProvider());

//註冊 AutoMapper
builder.RegisterAutoMapper(Assembly.GetExecutingAssembly());
builder.RegisterType(typeof(Mapper)).AsSelf();

//這行是必須的
builder.RegisterInstance(loggerFactory).As<ILoggerFactory>().SingleInstance();

//用泛型註冊
builder.RegisterGeneric(typeof(Logger<>)).As(typeof(ILogger<>));


//DI 的相關屬性設定
//註冊 connectionFactory
builder.RegisterType<ConnectionFactory>()
.As<IConnectionFactory>()
.AsSelf();

//註冊 repository
builder.RegisterType<YourRepository>()
.As<IYourRepository>()
.As<IRepository<YourClass>>()
.AsSelf();

var container = builder.Build();
var resolver = new AutofacDependencyResolver(container);

//todo 注意這個地方很雷 , 如果沒有替換 DependencyResolver 會無法正常 trigger 10 秒更新軌跡的動作
//https://stackoverflow.com/questions/21126624/signalr-autofac-owin-why-doesnt-globalhost-connectionmanager-gethubcontext
GlobalHost.DependencyResolver = resolver;

var yourService = resolver.Resolve<YourService>();

// Any connection or hub wire up and configuration should go here
app.UseAutofacMiddleware(container);

//設定 signalr
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);

//這裡直接插 config 會失效
//var config = new HubConfiguration { Resolver = resolver };

//要選用這個方法
//map.MapHubs(config);

//如果直接用這句的話不曉得為啥會不給過 , 可能是 autofac 跟 signalr 整合的 bug
//map.RunSignalR(config);

//同上面的問題 GlobalHost.DependencyResolver , 上面三句在此處其實都不起作用 , 直接呼叫 RunSignalR 即可
map.RunSignalR();
});

//註冊背景物件 , 這個會去定時每隔 10 秒呼叫一次 hub
//撈 sql 把資料給送給前端使用
HostingEnvironment.RegisterObject(new BackgroundServerTimer(yourService));

web api

autofac

Autofac.WebApi2

1
Install-Package Autofac.WebApi2 -Version 6.1.0

一樣在 Global 設定 , 這次不小心踩個雷 , 把 builder.RegisterApiControllers( Assembly.GetExecutingAssembly() ).InstancePerRequest(); 寫在 var container = builder.Build(); 之後
特別注意這個順序很重要 , 寫錯了就起不來 , 讓你 debug 到想死 , 可憐 ~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void Application_Start( object sender, EventArgs e )
{
//設定 DI
var builder = new ContainerBuilder();

//DI 的相關屬性設定
builder.RegisterType<MyLogger>().As<ILogger>().InstancePerRequest();
builder.RegisterType<BoxRepo>().As<IRepo>().InstancePerRequest();
builder.RegisterType<BoxService>().As<IBoxService>()
.UsingConstructor( typeof( IRepo ), typeof( ILogger ) )
.PropertiesAutowired().InstancePerRequest();

//設定 web api controller 註冊 DI
builder.RegisterApiControllers( Assembly.GetExecutingAssembly() ).InstancePerRequest();

//建立 DI container 特別注意順序很重要 , build 完以後才寫上面的屬性註冊會失效
var container = builder.Build();

//註冊給 asp.net webform 使用
_containerProvider = new ContainerProvider( container );

//設定 web api 的 di
var config = GlobalConfiguration.Configuration;
config.DependencyResolver = new AutofacWebApiDependencyResolver( container );

//預設的 application startup 設定流程
GlobalConfiguration.Configure( WebApiConfig.Register );
RouteConfig.RegisterRoutes( RouteTable.Routes );
BundleConfig.RegisterBundles( BundleTable.Bundles );

//清空 xml formatter
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Clear();
}

WebApiProxy

在 nuget 搜尋 WebApiProxy 並且安裝
找到 WebApiConfig 加上這句 config.RegisterProxyRoutes( );
參考自這篇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void AddRoutes(HttpConfiguration config)
{
//https://dotblogs.com.tw/yc421206/2018/09/28/webapi_generate_proxy_class_code
//WebApiProxy
config.RegisterProxyRoutes( );

//這串要在 MapHttpRoute 前面
config.MapHttpAttributeRoutes( );

config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

他會幫你自動產生 proxy 像這樣 http://localhost:23007/api/proxies
接著前端 code 會長這樣 , 注意需要有 jQuery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="Scripts/jquery-3.4.1.js"></script>

<!--自動產生的 javascript api proxies-->
<script src="/api/proxies"></script>

</head>
<body>
<script>
//測試 proxy 產出的 javascript api
$.proxies.gps.get('test2').done(function (resp) {
console.log(resp);
});

var test1 = {
"id": 'test1',
"x": 12,
"y": 24,
};
$.proxies.gps.add(test1).done(function (resp) {
console.log(resp);
});

</script>
</body>
</html>

Swashbuckle

在 nuget 搜尋 Swashbuckle 並且安裝
Build => Output path => bin\ => 打勾 XML document file => bin\WebApplicationWebFormWithWebApi.xml
找到 SwaggerConfig.cs 底下的 c.IncludeXmlComments 這串 , 修改為這樣
參考1 參考2 參考3

1
2
3
4
5
var files = Directory.GetFiles(
AppDomain.CurrentDomain.BaseDirectory, $"WebApplicationWebFormWithWebApi*.xml",
SearchOption.AllDirectories
);
foreach (var name in files) c.IncludeXmlComments( name );

生出 typescript 用的 前端 api

這個是因為用純 js 太通靈 , 後來才發現的 typescript 方法 , 需要先有 swagger 的 api 才可以動!
安裝 , 注意要切到你專案的 root 底下 , 可以參考這個官方說明

1
npm i swagger-typescript-api

建立資料夾讓他長得像是下面這樣

1
2
3
4
├───Scripts
│ └───TypeScripts
│ ├───dist
│ └───src

接著在 TypeScripts/src 底下新增 tsconfig.json , 這個選項 compileOnSave 可以讓存檔時生出 js 檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES2020",
"outDir": "./dist",
"rootDir": "./src",
"module": "ES2020"
},
"exclude": [
"node_modules",
"wwwroot"
],
"compileOnSave": true
}

最後啟動你的 api 網站 , 讓他能抓得到 swagger 產生的 json , 並且執行以下命令

1
npx swagger-typescript-api -p http://localhost:50348/swagger/docs/v1 -o ./YourProject/Scripts/TypeScripts/src -n myApi.ts

接著 cd 到 TypeScripts 目錄 , 執行 tsc 這個命令讓 js 可以生出來 , 注意一定要在 TypeScripts 這個資料夾才正常

1
tsc

Solution Exploer 上面點 Show All Files 可以看到生出 myApi.ts

1
tsc myApi.ts

萬一跳這個 error 執行這串即可 npm install -g typescript

1
tsc : 無法辨識 'tsc' 詞彙是否為 Cmdlet、函數、指令檔或可執行程式的名稱。請檢查名稱拼字是否正確,如果包含路徑的話,請確認路徑是否正確,然後再試一次。

src 加入這個檔案 test.ts

1
2
3
4
5
6
7
8
9
10
import { Api } from "./myApi.js";

const root = new Api({
baseUrl: "http://localhost:50348"
});


var test = root.api.valuesGet();

root.api.valuesGet().then(x => console.log(x.data));

最後新增個網頁來測 test.html , 正常的話在 console 上面會印出 api 撈出的數值 , 這樣可以用強行別減輕不少開發壓力

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script src="Scripts/TypeScripts/src/test.js" type="module"></script>
</body>
</html>

整合 AutoMaper

首先安裝 AutoMapperAutoMapper.Contrib.Autofac.DependencyInjection
可以參考他的官網

修正 Global 如下就可以正常註冊了 , 他的 github 上面 example 好像沒更新
他是用這句 containerBuilder.AddAutoMapper(typeof(Program).Assembly); 實際使用時看他標示已經棄用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 public class Global : HttpApplication, IContainerProviderAccessor
{
static IContainerProvider _containerProvider;
public IContainerProvider ContainerProvider
{
get { return _containerProvider; }
}
void Application_Start( object sender, EventArgs e )
{
var builder = new ContainerBuilder();

//註冊 AutoMapper
builder.RegisterAutoMapper( Assembly.GetExecutingAssembly() );
//我測 webform 沒加這句的話也會噴 null
builder.RegisterType(typeof(Mapper)).AsSelf();

builder.RegisterType<MyLogger>().As<ILogger>().InstancePerRequest();
builder.RegisterApiControllers( Assembly.GetExecutingAssembly() ).InstancePerRequest();
var container = builder.Build();
_containerProvider = new ContainerProvider( container );
var config = GlobalConfiguration.Configuration;
config.DependencyResolver = new AutofacWebApiDependencyResolver( container );

//從 Autofac 裡面拿出 AutoMapper 的 config
var mapperConfiguration = container.Resolve<MapperConfiguration>();

//這句用來驗證 AutoMapper 的 Profile 是否正確
mapperConfiguration.AssertConfigurationIsValid( );

// Code that runs on application startup
GlobalConfiguration.Configure( WebApiConfig.Register );
RouteConfig.RegisterRoutes( RouteTable.Routes );
BundleConfig.RegisterBundles( BundleTable.Bundles );
}
}

建立 Customer

1
2
3
4
5
6
7
8
9
10
11
public class Customer
{
public Guid Id { get; }
public string Name { get; }

public Customer( Guid id, string name )
{
Id = id;
Name = name;
}
}

建立 CustomerDto

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CustomerDto
{
public Guid Id { get; }
public string Name { get; }

public string FullName { get; set; }

public CustomerDto( Guid id, string name )
{
Id = id;
Name = name;
}
}

建立 CustomerProfile
這裡是重點 , 最好在專案建立資料夾 Profiles 進行分類 , 不然會有一堆 mapping
注意他的規則是擺在後面的 Type 為 return 的物件 , 所以此處為設定 CustomerDto 為 return 的結果
如果屬性相同的話可以不用一個一個設定 , 他會自己去 mapping
最後可以考慮加上 ReverseMap(); 這樣可以 two way mapping

1
2
3
4
5
6
7
8
9
10
11
public class CustomerProfile : Profile
{
public CustomerProfile()
{
//注意他的規則是擺在後面的 Type 為 return 的物件
//從 Customer 的 Name +Id Mapping 至 DTO 的 FullName
CreateMap<Customer, CustomerDto>()
.ForMember( x => x.FullName, y => y.MapFrom( z => z.Id + z.Name ) )
.ReverseMap();
}
}

最後弄個 api controller 測試看看 , 要手動注入 IMapper 就搞定了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[RoutePrefix("api/values")]
public class ValuesController : ApiController
{
private readonly IComponentContext _componentContext;
private readonly IMapper _mapper;
public ValuesController( IComponentContext componentContext, IMapper mapper )
{
_componentContext = componentContext;
_mapper = mapper;
}

[HttpGet]
[Route("test")]
public string Get()
{
Debug.WriteLine("Hello World");
var x = _componentContext.Resolve<ILogger>(new TypedParameter(typeof(string) , "test test test test"));
x.Info( "Ha Ha" );

return "test";
}

[HttpGet]
[Route("CustomerToDto")]
public CustomerDto CustomerToDto()
{
var customer = new Customer( Guid.NewGuid(), "HaHa" );
var result = _mapper.Map<Customer, CustomerDto>(customer);
return result;

}

[HttpGet]
[Route("DtoToCustomer")]
public Customer DtoToCustomer()
{
var customerDto = new CustomerDto( Guid.NewGuid(), "HaHa" );
var result = _mapper.Map<CustomerDto, Customer>(customerDto);
return result;
}
}

最後是 signalr 整合 automapper , 跟前面一樣大同小異在 Startup
加入關鍵這三行即可
builder.RegisterAutoMapper( Assembly.GetExecutingAssembly() );
var mapperConfiguration = container.Resolve<MapperConfiguration>();
mapperConfiguration.AssertConfigurationIsValid();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Startup
{
public void Configuration( IAppBuilder app )
{
var builder = new ContainerBuilder();

builder.RegisterAutoMapper( Assembly.GetExecutingAssembly() );

builder.RegisterHubs( Assembly.GetExecutingAssembly() );
builder.RegisterType<MyLogger>().As<ILogger>();

builder.RegisterType<BoxRepo>().As<IRepo>();

builder.RegisterType<BoxService>().As<IBoxService>()
.UsingConstructor( typeof( IRepo ), typeof( ILogger ) );
//.InstancePerLifetimeScope();

var container = builder.Build();
var resolver = new AutofacDependencyResolver( container );

//從 Autofac 裡面拿出 AutoMapper 的 config
var mapperConfiguration = container.Resolve<MapperConfiguration>();

//這句用來驗證 AutoMapper 的 Profile 是否正確
mapperConfiguration.AssertConfigurationIsValid();


// Any connection or hub wire up and configuration should go here
app.UseAutofacMiddleware(container);

app.Map( "/signalr", map =>
{
map.UseCors( CorsOptions.AllowAll );
var config = new HubConfiguration { Resolver = resolver };
map.RunSignalR(config);
} );

}
}

接著建立 CustomerHub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CustomerHub : Hub
{
private readonly IMapper _mapper;

public CustomerHub( IMapper mapper )
{
_mapper = mapper;
}

public CustomerDto CustomerToDto()
{
var customer = new Customer( Guid.NewGuid(), "HaHa" );
var result = _mapper.Map<Customer, CustomerDto>( customer );
Clients.All.getCustomerDto( result );
return result;

}

public Customer DtoToCustomer()
{
var customerDto = new CustomerDto( Guid.NewGuid(), "HaHa" );
var result = _mapper.Map<CustomerDto, Customer>( customerDto );
Clients.All.getCustomer( result );
return result;
}
}

最後調整前端 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="Scripts/jquery-3.4.1.js"></script>
<script src="Scripts/jquery.signalR-2.2.2.js"></script>
<script src="/signalr/hubs"></script>

<!--使用靜態生出來的-->
<!--<script src="Scripts/server.js"></script>-->
</head>
<body>
<script>
var chat = $.connection.customerHub;
$.connection.hub.start().done(function () {
$(document).on('click' , '#btn', function () {
chat.server.customerToDto();
})
})
chat.client.getCustomerDto = function (resp) {
console.log(resp);
$('#display').append(`<p>${resp.Id} , ${resp.Name}</p>`);
}
</script>
<div id="display"></div>
<button id="btn">go</button>
</body>
</html>
關閉