Teams Integration with Dynamics 365 for an Entity showing not connected.

New Contributor

I need to be able to programmatically add a tab to teams that uses the Dynamics 365 teams app for an entity and I need it to show as connected to dynamics.  Here is the requests I have built for both the Graph SDK and the CRM SDK side of things as well as some black magic I caught from fiddler that makes me sad.

 

First lets start with some things that are used anytime anyone wants to make a dynamics tab.  You need the teams app id for the Dynamics 365 Teams Integration App which happens to be the same on all tenants (yay) which is  cd2d8695-bdc9-4d8e-9620-cc963ed81f41

 

next you are going to need all kinds of Organization/Environment specific info

OrganizationUrl = https://orgXXXXXXXX.crm.dynamics.com

OrganizationId (Guid) from Power Platform Admin Center is easiest
Dynamics App Id (Guid) for the app your entity is from ie Sales Hub for an Opportunity Entity
The Entity Id (Guid) for the entity you wish to connect to
The Entity Type Code (int) from the Entity Metadata for ex Opportunity is 3 and Contact is 2

The above are all things you use the CRM API to go and get.  You also need the following from Graph SDK.

Team.Id (Guid) for the team you are adding a tab too
Team.InternalId (string looks like 19:32characters@thread.tacv2) "19:00000000111122223333444444444444@thread.tacv2" also for the team you are adding the tab too
Channel.Id (string looks like 19:32characters@thread.tacv2) for the channel you want to add the tab too
A Display Name for the tab of your choice.

A Fully Qualified Entity Name, not sure what the Dynamics devs call it but that is what it looks like, its format looks like this

 

OrganizationId.ToString() + "|" + DynamicsAppId.ToString() + "|" + EntityId.ToString() + "|" + EntityObjectTypeCode.ToString() + "|" + EntityDisplayName

 

 

Lastly you need a formatted content url to the dynamics entity you want to connect too I did it with this

 

string.Format("{0}main.aspx?appid={1}&pageType=entityrecord&etn=opportunity&id={2}", OrganizationUrl, DynamicsAppId.ToString(), EntityId.ToString())

 

 

Because this is a fair amount of data to manage I made a little class to hold it all

 

 

    public class TeamTabInfo
    {
        public string DisplayName { get; set; }

        public Guid EntityId { get; set; }

        public string OrganizationUrl { get; set; }

        public Guid OrganizationId { get; set; }

        public Guid DynamicsAppId { get; set; }

        public int EntityObjectTypeCode { get; set; }

        public string EntityWebsiteUrl { get; set; }

        public string FormattedEntityWebsiteUrl
        {
            get
            {
                return string.Format(EntityWebsiteUrl, OrganizationUrl, DynamicsAppId.ToString(), EntityId.ToString(), EntityObjectTypeCode.ToString());
            }
        }

        public string FormattedFQEN
        {
            get
            {
                return OrganizationId.ToString() + "|" + DynamicsAppId.ToString() + "|" + EntityId.ToString() + "|" + EntityObjectTypeCode.ToString() + "|" + DisplayName;
            }
        }

    }

 

 

Now that I have something to hold the info that makes up a tab I needed a function that would actually make the objects for Graph to use.

 

 

public static TeamsTab CreateTabRequest(TeamTabInfo tabInfo)
        {
            TeamsTabConfiguration ttc = new TeamsTabConfiguration();
            TeamsTab tt = new TeamsTab();

            ttc.EntityId = tabInfo.FormattedFQEN;
            ttc.WebsiteUrl = tabInfo.FormattedEntityWebsiteUrl;
            ttc.ContentUrl = $"https://msteamstabintegration.crm.dynamics.com/Home/Bootstrapper#pageType=dynamics-host&dynamicsUrl={ WebUtility.UrlEncode(tabInfo.FormattedEntityWebsiteUrl + "&navbar=off&flags=FCB.AllowLegacyDialogsInUci=false") }";
            ttc.RemoveUrl = "https://msteamstabintegration.crm.dynamics.com/Home/Bootstrapper#pageType=tab-remove";

            
            tt.Configuration = ttc;
            tt.DisplayName = tabInfo.DisplayName;
            tt.ODataBind = "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/cd2d8695-bdc9-4d8e-9620-cc963ed81f41";
            return tt;
        }

 

TeamsTabConfiguration and TeamsTab come from the GraphSDK.  Now you see  those magic links in there?  They apparently hide the secret to success here but we will come back to that.

 

Here is the code I use to get the info from CRM and GraphSDK I need to be able to create the Tab objects.

 

GraphServiceClient gsc = await gf.GetClient(tenantUser, tenantPass);
            
            var myTeamsResult = await gsc.Me.JoinedTeams.Request().GetAsync();
            string contosoCoffeeTeamsId = myTeamsResult.Where(t => string.Compare(t.DisplayName, "My Team", true) == 0).FirstOrDefault()?.Id;

            var contosoCoffeeTeamResult = await gsc.Teams[contosoCoffeeTeamsId].Request().GetAsync();
            
            var channelsResult = await gsc.Teams[contosoCoffeeTeamsId].Channels.Request().GetAsync();

            string espressoId = channelsResult.Where(c => string.Compare(c.DisplayName, "Group 1", true) == 0).FirstOrDefault()?.Id;

            var installedAppsResult = await gsc.Teams[contosoCoffeeTeamsId].InstalledApps.Request().Expand("teamsAppDefinition").GetAsync();
            
            bool dynamicsAppInstalled = installedAppsResult.Where(i => i.TeamsAppDefinition.TeamsAppId == "cd2d8695-bdc9-4d8e-9620-cc963ed81f41").Any();

            if(!dynamicsAppInstalled)
            {
                var teamsAppInstallation = new TeamsAppInstallation
                {
                    AdditionalData = new Dictionary<string, object>()
                    {
                        //well known dynamics teams app id = cd2d8695-bdc9-4d8e-9620-cc963ed81f41
                        {"teamsApp@odata.bind", "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/cd2d8695-bdc9-4d8e-9620-cc963ed81f41"}
                    }
                };

                await gsc.Teams[contosoCoffeeTeamsId].InstalledApps
                    .Request()
                    .AddAsync(teamsAppInstallation);
            }

            string orgUrl = await gf.GetOrganizationUrl(tenantName, tenantPass);

            CrmServiceClient csc = gf.ConnectCRMAdminLogin(tenantName, tenantPass, orgUrl);
            
            EntityMetadata opportunityEm = csc.GetEntityMetadata("opportunity", EntityFilters.Entity);
            
            QueryExpression queryPages = new QueryExpression("appmodule");
            queryPages.ColumnSet = new ColumnSet(new String[] { "appmoduleid", "name", "url", "description" });
            queryPages.Criteria.AddCondition("name", ConditionOperator.In, new string[] { "Sales Hub" }); 

            TeamTabInfo cafeAutoTab = new TeamTabInfo();
            cafeAutoTab.DisplayName = "Opportunity 1";
            cafeAutoTab.EntityWebsiteUrl = "{0}main.aspx?appid={1}&pageType=entityrecord&etn=opportunity&id={2}";
            cafeAutoTab.OrganizationId = csc.ConnectedOrgId;
            cafeAutoTab.OrganizationUrl = orgUrl;
            cafeAutoTab.EntityObjectTypeCode = opportunityEm.ObjectTypeCode.Value;

            cafeAutoTab.DynamicsAppId = queryResult.Entities.Where(w => w.Attributes.Values.Contains("Sales Hub")).FirstOrDefault().Id;

            queryPages = new QueryExpression("opportunity");
            queryPages.ColumnSet = new ColumnSet(new String[] { "opportunityid", "name", "description" });
            queryPages.Criteria.AddCondition("name", ConditionOperator.Equal, "Opportunity 1");

            queryResult = csc.RetrieveMultiple(queryPages);

            cafeAutoTab.EntityId = queryResult.Entities.FirstOrDefault().Id;


            var tabResult = await gsc.Teams[contosoCoffeeTeamsId].Channels[espressoId].Tabs.Request().AddAsync(GraphFunctions.CreateTabRequest(cafeAutoTab));

 

 

Assume GraphServiceClient gsc and CrmServiceClient csc are functional as I have other huge functions that do all the auth stuff and give me back a functional client.  The above code adds a tab to teams but apparently it is only half added, just the teams half.  There is a CRM half I learned and this involves an insertion into the logical dataverse entity table "msdyn_teamscollaboration"  and that ends up looking a bit like the following

 

 

 Microsoft.Xrm.Sdk.Entity teamsCollabToAdd = new Microsoft.Xrm.Sdk.Entity();
            teamsCollabToAdd.LogicalName = "msdyn_teamscollaboration";
            
            teamsCollabToAdd.Attributes.Add("msdyn_teamid", contosoCoffeeTeamResult.InternalId);
            teamsCollabToAdd.Attributes.Add("regardingobjecttypename", "opportunity");
            teamsCollabToAdd.Attributes.Add("msdyn_groupid", new Guid(contosoCoffeeTeamsId));
            teamsCollabToAdd.Attributes.Add("msdyn_tenantid", csc.TenantId);
            teamsCollabToAdd.Attributes.Add("msdyn_channelid", espressoId);
            teamsCollabToAdd.Attributes.Add("regardingobjectid", cafeAutoTab.EntityId);
            teamsCollabToAdd.Attributes.Add("statecode", "Active");
            teamsCollabToAdd.Attributes.Add("msdyn_channelfolderrelativeurl", "/sites/My Team/Shared Documents/Group 1");
            teamsCollabToAdd.Attributes.Add("msdyn_teamsiteurl", string.Format("https://{0}.sharepoint.com/sites/My%20Team", tenantName.ToLower()));

            teamsCollabToAdd.Attributes.Add("msdyn_channelname", "Group 1");
            teamsCollabToAdd.Attributes.Add("msdyn_channeltype", "regular");
            teamsCollabToAdd.Attributes.Add("msdyn_teamname", "My Team");
            teamsCollabToAdd.Attributes.Add("regardingobjecttypecode", cafeAutoTab.EntityObjectTypeCode);
            teamsCollabToAdd.Attributes.Add("msdyn_appid", "cd2d8695-bdc9-4d8e-9620-cc963ed81f41");
            teamsCollabToAdd.Attributes.Add("msdyn_pipedentityid", cafeAutoTab.FormattedFQEN);
            teamsCollabToAdd.Attributes.Add("msdyn_weburl", cafeAutoTab.FormattedEntityWebsiteUrl);
            teamsCollabToAdd.Attributes.Add("msdyn_contenturl", $"https://msteamstabintegration.crm.dynamics.com/Home/Bootstrapper#pageType=dynamics-host&dynamicsUrl={WebUtility.UrlEncode(cafeAutoTab.FormattedEntityWebsiteUrl)}");

            CreateRequest cdsRequest = new CreateRequest();
            cdsRequest.Target = teamsCollabToAdd;

            CreateResponse cdsResponse = (CreateResponse)csc.Execute(cdsRequest);

 

 

Now this also successfully runs and I get a record added to the Dataverse.  Not only do I get a record, but if I remove this created tab and use the UI I get the exact same record added to the Dataverse if everyway except a new primary key.

Everything looks in place and it should be solid, but all is not well in the land of no documentation.  Remember that magic link I mentioned before?  Well it turns out that if you use my code you get a tab added that has an angry bit.

mallen81_0-1629923319874.png

This record is not connected to Dynamics 365. Repin the tab and try again. And that learn more points to https://msteamstabintegration.crm.dynamics.com/Home/undefined which goes nowhere and looks suspiciously familiar to a magic link.  If you use the UI you get

mallen81_1-1629923444775.png

This record is successfully connected to Dynamics 365. and that Learn More points to https://go.microsoft.com/fwlink/?linkid=2019594 which is at least a functional.

 

So I did what anyone else would do when you have functional UI and non functional API calls after making sure the backend data I had access too was identical.  I hit it with Fiddler.  A behold a call is made to the following API endpoint.

 

https://msteamstabintegration.crm.dynamics.com/api/TeamsTabService/PostPinTasks

 

This is a secure endpoint we don't have access too.  This appears the difference between using the UI and the 2 API calls to add the tab and the record to msdyn_teamscollaboration is the call to the above endpoint.

 

So after all the preparation and all the hunting this is the question for the MS group that owns

 

https://msteamstabintegration.crm.dynamics.com

 

WHAT DOES THAT ENDPOINT DO?  What are these "Post Pin Tasks"?


BTW pinning a view (savedquery entity) doesn't have this issue since it isn't kept in sync the same way, it is specific to Direct Entity Connections.

2 Replies
Even I am facing similar issue.
We have added the dynamics tab from the endpoint
POST /teams/{team-id}/channels/{channel-id}/tabs
add the record in msdyn_teamsCollaboration entity.
but still we are geting the error
"This record is not connected to Dynamics 365. Repin the tab and try again"

As an update, I have not heard anything yet from internal contacts on how to resolve this issue.  If I do I will post an update.

www.000webhost.com