雜物儲存室 Coding Blog

抓取 Active Directory 使用者資訊

using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;

var service = new DirectoryService();

service.GetUserByPrincipal("user").Dump();

#pragma warning disable CA1416
public class DirectoryService
{
	private readonly ContextType _contextType = ContextType.Domain;

	public User? GetUserByPrincipal(string userName)
	{
		using var context = new PrincipalContext(_contextType);

		return Parse(context, userName);
	}

	public User? GetUserByPrincipal(string userName, string password)
	{
		using var context = new PrincipalContext(_contextType, null, null, userName, password);

		try
		{
			if (string.IsNullOrWhiteSpace(context.ConnectedServer))
			{
				throw new Exception("ConnectedServer is null or empty.");
			}
		}
		catch (DirectoryServicesCOMException)
		{
			// Invalid password
			return null;
		}

		return Parse(context, userName);
	}

	private static User? Parse(PrincipalContext context, string userName)
	{
		using var userPrincipal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, userName);

		if (userPrincipal?.Enabled ?? false)
		{
			return new User(userPrincipal);
		}

		return null;
	}

	public IEnumerable<(string Key, object Value)>? GetUserByDirectoryEntry(string userName)
	{
		using var dcEntry = new DirectoryEntry();

		return Parse(dcEntry, userName);
	}

	public IEnumerable<(string Key, object Value)>? GetUserByDirectoryEntry(string userName, string password)
	{
		using var dcEntry = new DirectoryEntry();

		using var userEntry = new DirectoryEntry(dcEntry.Path, userName, password);

		return Parse(userEntry, userName);
	}

	private static IEnumerable<(string Key, object Value)>? Parse(DirectoryEntry dcEntry, string userName)
	{
		var search = new DirectorySearcher(dcEntry)
		{
			Filter = $"(&(objectClass=user)(sAMAccountName={userName}))"
		};

		SearchResult? searchResult;

		try
		{
			searchResult = search.FindOne();
		}
		catch (DirectoryServicesCOMException)
		{
			// Invalid password
			return null;
		}

		if (searchResult == null)
		{
			return null;
		}

		var user = new List<(string Key, object Value)>();

		foreach (var p in searchResult.Properties)
		{
			if (p is not System.Collections.DictionaryEntry { Key: string key, Value: ResultPropertyValueCollection propertyValue })
			{
				continue;
			}

			var value = GetResultPropertyValue(propertyValue);

			user.Add((key, value));
		}

		return user;
	}

	private static object GetResultPropertyValue(ResultPropertyValueCollection propertyValue)
	{
		var value = propertyValue[0];

		if (value is byte[] b)
		{
			value = ByteArrayToGuid(b);
		}

		return value;

		static object ByteArrayToGuid(byte[] binaryData)
		{
			var strHex = BitConverter.ToString(binaryData);

			if (Guid.TryParse(strHex.Replace("-", string.Empty), out var guid))
			{
				return guid;
			}

			return strHex;
		}
	}

	public class User(UserPrincipal principal)
	{
		public Guid? Id { get; } = principal.Guid;
		public string? EmployeeId { get; } = principal.EmployeeId;
		public string? Account { get; } = principal.SamAccountName;
		public string? UserName { get; } = principal.Name;
		public string? Surname { get; } = principal.Surname;
		public string? GivenName { get; } = principal.GivenName;
		public string? EmailAddress { get; } = principal.EmailAddress;
		public Group[]? Groups { get; } = GetGroups(principal);

		private static Group[]? GetGroups(Principal user)
		{
			var groups = user.GetGroups().ToArray().Where(x => x is GroupPrincipal).Select(x => new Group((GroupPrincipal)x)).ToArray();

			return groups;
		}
	}

	public class Group(GroupPrincipal principal)
	{
		public Guid? Id { get; } = principal.Guid;
		public string? Account { get; } = principal.SamAccountName;
		public string? Name { get; } = principal.Name;
		public bool? IsSecurityGroup { get; } = principal.IsSecurityGroup;
	}
}
#pragma warning restore CA1416

WSL2 安裝筆記

至從升級到 Windows 10 20H1 後 WSL2 一直都沒辦法用 vscode 連上, 今天弄了一個乾淨的環境重頭開始也順利安裝完成.

安裝

  1. 首先確認 Windows 10 是否為 Windows 10 20H1 以後的版本.

  2. 安裝 vscode 並安裝套件 Remote - WSL

  3. 使用 PowerShell 安裝 WSL.

     dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
    
  4. 安裝VirtualMachinePlatform 並 重新啟動 Windows

     dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    
  5. 安裝 WSL 2 Linux kernel.

  6. 將 WSL 2 設定為預設

     wsl --set-default-version 2
    
  7. 安裝 Ubuntu 並設定帳號密碼.

  8. 輸入 vscode . 後會安裝 WSL Server 並啟動 vscode

ISSUE

如果啟動 Ubuntu 後出現以下錯誤代碼 0xc03a001a

Installing, this may take a few minutes...
WslRegisterDistribution failed with error: 0xc03a001a
Error: 0xc03a001a The requested operation could not be completed due to a virtual disk system limitation.  Virtual hard disk files must be uncompressed and unencrypted and must not be sparse.

Press any key to continue...

請在資料夾 %LOCALAPPDATA%/packages/ 找到 Ubuntu 的資料夾, 例如 CanonicalGroupLimited.UbuntuonWindows_79rhkp1fndgsc

並在進階裡把 壓縮內容以節省磁碟空間 取消勾選即可

參考資料

Windows Subsystem for Linux Installation Guide for Windows 10


NSwag 自訂物件範例

直接上 code

public void ConfigureServices(IServiceCollection services)
{
    services.AddOpenApiDocument(config =>
    {
        config.TypeMappers.Add(new PrimitiveTypeMapper(typeof(Geometry), schema =>
        {
            schema.Title = "GeoJson";
            schema.Properties.Add("type", new JsonSchemaProperty
            {
                IsRequired = true,
                Title = "GeoJson type",
                Type = JsonObjectType.String
            });
            schema.Properties.Add("coordinates", new JsonSchemaProperty
            {
                IsRequired = true,
                Title = "Coordinates"
            });
            schema.Example = new
            {
                type = "Point",
                coordinates = new[] { 121.517283, 25.047732 }
            }; //在這邊寫輸入範例
        }));
    });
}

在 Console Application 使用 DI

直接上 code

dotnet add package Microsoft.Extensions.DependencyInjection --version 3.1.4
// 取得config
var config = new ConfigurationBuilder()
                // ...
                .Build();

var services = new ServiceCollection();

// 注入 ILogger<T> 元件 (Microsoft.Extensions.Logging)
// 如果有安裝 Microsoft.Extensions.Logging.Console 就可以像 ASP.NET Core 的 console 顯示訊息
services.AddLogging(configure => LoggingHelper.SetLoggingBuilder(configure, config));

// 跟 ASP.NET Core 一樣註冊 Service
services.AddTransient<Service>();

var provider = services.BuildServiceProvider();

var service = provider.GetService<Service>(); // 使用 provider 取得所需要的 service

建立自訂的 ILogger 元件

加入參考

dotnet add package Microsoft.Extensions.Logging.Configuration

建立 LoggerConfiguration

LoggerConfiguration 的作用在於傳入自訂的 Log 元件設定檔, 比如說 LogPathLog 記錄等級.

public class CustomLoggerConfiguration
{
    public string LogPath { get; set; }

    private readonly IList<LogLevel> _logLevels;

    public CustomLoggerConfiguration()
    {
        _logLevels = new List<LogLevel>
        {
            LogLevel.Error,
            LogLevel.Critical
        };
    }

    public void AddLogLevel(params LogLevel[] levels)
    {
        foreach (var level in levels)
        {
            if (IsLogLevelEnable(level))
            {
                return;
            }
            _logLevels.Add(level);
        }
    }

    public bool IsLogLevelEnable(LogLevel level)
    {
        return _logLevels.Contains(level);
    }
}

建立 CustomLogger

繼承介面 ILogger 建立我們自訂的 Logger 元件, 將訊息寫入原本就建立好的 MyLogger 元件.

public class CustomLogger : ILogger
{
    private readonly string _name;
    private readonly Func<CustomLoggerConfiguration> _getCurrentConfig;
    private readonly MyLogger _loggerService;
    public CustomLogger(string name, Func<CustomLoggerConfiguration> getCurrentConfig)
    {
        _name = name;
        _getCurrentConfig = getCurrentConfig;
        _loggerService = new MyLogger(_getCurrentConfig().LogPath);
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        try
        {
            var logLevel = MapLevel(logLevel);
            var message = formatter(state, exception);

            _loggerService.Log(_name, logLevel, message, exception);
        }
        catch (Exception e)
        {
            Debug.WriteLine(e);
        }
    }
    private static MyLogLevel MapLevel(LogLevel level)
    {
        return level switch
        {
            LogLevel.Trace => MyLogLevel.Trace,
            LogLevel.Debug => MyLogLevel.Debug,
            LogLevel.Information => MyLogLevel.Info,
            LogLevel.Warning => MyLogLevel.Warn,
            LogLevel.Error => MyLogLevel.Error,
            LogLevel.Critical => MyLogLevel.Fatal,
            LogLevel.None => MyLogLevel.Trace,
            _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
        };
    }
    public bool IsEnabled(LogLevel logLevel) => _getCurrentConfig().IsLogLevelEnable(logLevel);
    public IDisposable BeginScope<TState>(TState state) => default;
}

建立 CustomLoggerProvider

繼承介面 ILoggerProvider, 建立 CustomLoggerProvider.

public class CustomLoggerProvider : ILoggerProvider
{
    private readonly IDisposable _onChangeToken;
    private CustomLoggerConfiguration _currentConfig;

    public CustomLoggerProvider(IOptionsMonitor<CustomLoggerConfiguration> config)
    {
        _currentConfig = config.CurrentValue;
        _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig);
    }

    public ILogger CreateLogger(string categoryName) => new CustomLogger(categoryName, GetCurrentConfig);

    private CustomLoggerConfiguration GetCurrentConfig() => _currentConfig;

    public void Dispose()
    {
        _onChangeToken.Dispose();
    }
}

註冊 CustomLoggerProvider

最後我們將剛的建立的 CustomLoggerProvider 註冊到 Microsoft.Extensions.DependencyInjectionIServiceCollection, 讓 ASP.NET Core 可以建立我們自訂的 CustomLogger.

in Startup.cs

services.AddLogging(builder =>
{

    Action<CustomLoggerConfiguration> configure = config =>
    {
        var logPath = Configuration.GetSection("Log:LogPath").Value;
        config.LogPath = logPath;
        config.AddLogLevel(LogLevel.Warning);
    }

    builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, CustomLoggerProvider>());

    LoggerProviderOptions.RegisterProviderOptions<CustomLoggerConfiguration, CustomLoggerProvider>(builder.Services);

    builder.Services.Configure(configure);
});

這樣, 使用注入的 ILogger 時就會將資訊一併寫入我們自訂的 Log 元件了.