使用任务返回值时出现InvalidCastException异常

4

我已经花了将近两天的时间阅读有关async/await的教程和Stackoverflow上的答案,试图理解C#中的异步和并行执行。但是,我仍然无法使我的代码正常工作。

我需要什么

通过PrincipalSearcher异步执行Active Directory搜索,而不会阻塞WPF UI。

实现

protected async void SearchButtonClick()
{
    Task<PrincipalSearchResult<Principal>> searchTask = Task.Run(() => _activeDirectory.FindGroup(searchText.Text));

    PrincipalSearchResult<Principal> searchResult = await searchTask;

    foreach (var foundGroup in searchResult) /*exception thrown here*/
    {
        ...
    }
}

_activeDirectory类:

public PrincipalSearchResult<Principal> FindGroup(String pattern)
{
    ...
    PrincipalSearchResult<Principal> searchResult = searcher.FindAll();
    return searchResult;
}

问题

  • await 看起来并没有等待任务完成。searchTask.IsCompleted 在 await 行之后为 true,但这不可能是因为它几乎没有完成所需的时间,如果我同步运行它,则搜索需要约 5 秒。
  • 在 foreach 循环的开头抛出异常:

System.InvalidCastException 出现 HResult=-2147467262
Message=无法将类型为“System.__ComObject”的 COM 对象强制转换为接口类型“IDirectorySearch”。此操作失败,原因是由于 COM 组件上的 QueryInterface 调用请求接口 IID“{109BA8EC-92F0-11D0-A790-00C04FD8D5A8}”时失败,导致以下错误:不支持此接口 (HRESULT 为 0x80004002(E_NOINTERFACE))。 Source=System.DirectoryServices StackTrace: at System.DirectoryServices.SearchResultCollection.get_SearchObject()
InnerException:

想法

  • 我发现这种异常与无效的 SynchronizationContext 有关,但我不知道这怎么会发生在这里。我还在代码的几行上打印了 Thread.CurrentThread.ManagedThreadId,并始终返回相同的 id。
  • 我还读到异步方法不应该返回 void 而应该返回 Task<T>,但我不认为这在这里是相关的,因为没有人异步使用 SearchButtonClick()。而且 Task.Run() 可能确实返回一个 Task,如果这甚至相关的话。整个主题对我来说仍然很模糊。

问题

  • 为什么 await 不等待任务完成?
  • 异常的原因是什么?

http://stackoverflow.com/questions/18110738/passing-collection-from-backgroundworker-dowork-to-backgroundworker-completed-an - Gosha_Fighten
@Gosha_Fighten 我已经尝试过 PrincipalSearchResult<Principal> searchResult = (PrincipalSearchResult<Principal>) await searchTask;,但这并没有解决问题。 - mrplow
3个回答

3
await会等待Task完成。你在使用async\await模式和TPL库时唯一遗漏的是,Task中抛出的Exception并不会立即被抛出。它会被缓存在TaskException属性中,并在你想要获取其结果时立即被抛出。
所以问题在于你正在Task内运行的代码无法在另一个线程中运行,正如@NineBerry所说。你必须在Task内创建你的AD-对象,就像这样:
Task<PrincipalSearchResult<Principal>> searchTask = Task.Run(() =>
{
    // this have to be a local variable inside your task
    var _activeDirectory = GET_THE_AD();
    return _activeDirectory.FindGroup(searchText.Text));
}

或者,也许你需要为你的线程每次创建一个searcher变量。

2

您不能在创建对象的线程之外的另一个线程上使用_activeDirectory。这是基于你的对象内部使用的COM主机的实现方式而定。一些COM主机是以可以在多个线程中使用的方式来实现的,而有些则是以只能在创建它的同一线程上使用对象的方式来实现的。

您需要更改您的代码,在您在Task.Run中执行的代码中创建_activeDirectory或更改_activeDirectory的实现,以在搜索方法中创建它所使用的访问活动目录的COM对象。

您还需要确保该线程具有ApartmentStateApartmentState.STA。请参考此问题了解如何实现。


由于异常的存在,调用await立即返回。如果任务内发生未处理的异常,那么该任务将在那一刻结束。

当同步执行时,搜索需要5秒钟才能执行,因为在与创建它的同一线程中使用COM对象时,不会发生异常,正如上面所解释的那样。


0

即使在与Active Directory搜索相同的线程上创建了一个新的UserModel,我仍然遇到了这个异常。问题在于IEnumerable<UserModel>包含了一个嵌套类型UserPrincipal,而主线程不知道这个类型。通过使用.ToList()来移除嵌套类型,我成功地解决了这个问题。

_context = new PrincipalContext(ContextType.Domain);

public async Task<IEnumerable<UserModel>> SearchDisplayNameAsync(string searchPhrase, bool enabled = true)
{
    IEnumerable<UserModel> results = await Task.Run(() => SearchDisplayName(searchPhrase: searchPhrase, enabled: enabled));
    return results.ToList();  // <-- ToList() removes nested type
}

public IEnumerable<UserModel> SearchDisplayName(string searchPhrase, bool enabled = true)
{
    UserPrincipal userPrincipal = new UserPrincipal(_context)
    {
        DisplayName = $"{searchPhrase}*",
        Enabled = enabled
    };

    using (PrincipalSearcher searcher = new PrincipalSearcher(userPrincipal))
    {
        return searcher.FindAll()
            .OfType<UserPrincipal>()
            .Select(u => new UserModel
            {
                Guid = (Guid)u.Guid,
                DisplayName = u.DisplayName,
                EmailAddress = u.EmailAddress
            });
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接