Showing posts with label .NET Interop. Show all posts
Showing posts with label .NET Interop. Show all posts

Monday, September 9, 2019

Convert a Dynamics Enum to a C# generic collection

When you need to consume a Dynamics Ax Enum type in a .Net project you should make your project independent from the internal structure of Dynamics and certainly you do not want to create a static copy of your Dynamics Enum in your .Net project.

In order to achieve that I taught to expose the Dynamics Enum as a generic List.

To do that and adhere to the DRY principle (do not repeat yourself) I made an extension method in C# that converts any Enum in a generic list.

first you need to create a POCO class, a data transfer object

namespace MydDynamicsIntegration.Models
{
    public class AxEnumDefinition
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public string Label { get; set; }
    }
}

now here the extension method

using System;
using System.Collections.Generic;
using MydDynamicsIntegration.Models;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class AxGetValuesExtensions
    {
        public static List<AxEnumDefinition> FromAxToColl<T>(this T eEnum) where T : struct, IConvertible
        {
            var enumDefinitions = new List<AxEnumDefinition>();

            Type t = typeof(T);

            if (!t.IsEnum)
                throw new ArgumentException("T must be an enumerated type");

            try
            {
                SysDictEnum sysDictEnum = SysDictEnum.newName(t.Name);

                for (int i = 0; i < sysDictEnum.values(); i++)
                {
                    enumDefinitions.Add(new AxEnumDefinition()
                    {
                        Value = sysDictEnum.index2Value(i),
                        Name = sysDictEnum.index2Name(i),
                        Label = sysDictEnum.index2Label(i)
                    });
                }
            }
            catch
            {
                //
            }

            return enumDefinitions;
        }
    }
}

now you can use it like this

use example: 
return new HcmDiscussionStatus().FromAxToColl();
OR to a specific value
Status = new AxEnumDefinition()
   {
      Value = Global.enum2int(discussion.status),
      Name = Global.enum2Value(discussion.status),
      Label = discussion.status.FromAxToColl().Where(x => x.Value == Global.enum2int(discussion.status)).Select(x => x.Label).FirstOrDefault()
   }
OR 
Status = discussion.status.FromAxToColl().FirstOrDefault(x => x.Value == Global.enum2int(discussion.status))

Monday, September 2, 2019

c# extension method to retrieve documents from Dynamics Ax and expose them to a WebApi via a repository class

As an introduction let me show you a piece of X++ to get a document.

You can find much more if you google it.

static void JobEF_DocumentSearch(Args _args)
{
    Notes notes = "";
    DocuRef docuRefTmp;
    HcmDiscussion hcmDiscussion;
    DocuRefSearch docuRefSearchTmp;
    DocuTypeId docuTypeId;
    FilePath filePath, filePathGeneric, path;

    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('000024', HcmWorker::findByPersonnelNumber('2002450').RecId);
    if ( ! hcmDiscussion ) return;


    //docuRefSearchTmp = DocuRefSearch::newCommon( hcmDiscussion );
    //filter per document type
    docuTypeId = 'Appraisal';
    docuRefSearchTmp = DocuRefSearch::newDocuTypeId(hcmDiscussion, docuTypeId);

    filePathGeneric = Docu::archivePath(curExt());
    filePath = DocuType::find(docuTypeId).ArchivePath;

    info(filePathGeneric);
    info(filePath);

    while ( docuRefSearchTmp.next() )
    {
        docuRefTmp = docuRefSearchTmp.docuRef();

        notes = docuRefTmp.docuValue().FileName;
        path = docuRefTmp.path();
        info(strFmt('%1 --- %2', path, notes));
    }
}

now let me show you how we can do the same from c# using proxy objects

to open a session i will use the code described in this post, read it first:
https://enricoariel.blogspot.com/2019/08/proxy-classes-for-net-interop-to-x.html

step1: create an extension class

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.AX.Framework.Linq.Data;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class GenericRecord
    {
        public static List<DocuRefProperties> GetAttachments<T>(this T axTable, string docuTypeId = null) where T : Common
        {
            var ret = new List<DocuRefProperties>();

            var docuRefSearchTmp = docuTypeId == null ? DocuRefSearch.newCommon(axTable) : DocuRefSearch.newDocuTypeId(axTable, docuTypeId);

            while (docuRefSearchTmp.next())
            {
                DocuRef docuRefTmp = docuRefSearchTmp.docuRef();
                var docuref = new DocuRefProperties
                {
                    RecId = docuRefTmp.RecId,
                    ValueRecId = docuRefTmp.ValueRecId,
                    Path = docuRefTmp.path(),
                    FileName = docuRefTmp.docuValue().fileName(),
                    Description = docuRefTmp.Name,
                    TypeId = docuRefTmp.TypeId,
                    CreatedDateTime = (DateTime) docuRefTmp.CreatedDateTime
                };
                ret.Add(docuref);
            }

            return ret;
        }

        public class DocuRefProperties
        {
            public long RecId { get; set; }
            public long ValueRecId { get; set; }
            public string Path { get; set; }
            public string FileName { get; set; }
            public string Description { get; set; }
            public string TypeId { get; set; }
            public DateTime CreatedDateTime { get; set; }
        }
    }
}

Note: import the relevant proxy objects in your .Net project

step2: create the repository

in this example I made a repository for documents attached to HcmDiscussion

DiscussionRepository.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using MydDynamicsIntegration.DynamicsCommon;
using MydDynamicsIntegration.Models;
using MydDynamicsIntegration.Repositories.Interfaces;

namespace MydDynamicsIntegration.Repositories.Implementation
{
    public class DiscussionRepository : IAmDiscussionRepository
    {
        private const string AppraisalDocuTypeId = "Appraisal";
        public List<Attachment> GetAttachments(string discussionId, string personnelNumber)
        {
            using (var axSession = new AxSessionManager())
            {
                axSession.OpenConnection();

                var workerRecId = HcmWorker.findByPersonnelNumber(personnelNumber).RecId;
                var discussion = HcmDiscussion.findByDiscussionWorker(discussionId, workerRecId);

                return discussion.GetAttachments(AppraisalDocuTypeId).Select(attachment => new Attachment
                {
                    RecId = attachment.RecId,
                    ValueRecId = attachment.ValueRecId,
                    Path = attachment.Path,
                    FileName = attachment.FileName,
                    Description = attachment.Description,
                    TypeId = attachment.TypeId,
                    CreatedDateTime = attachment.CreatedDateTime
                }).ToList();
            }
        }       
    }
}

Attachment.cs:

using System;

namespace MydDynamicsIntegration.Models
{
    public class Attachment
    {
        public long RecId { get; set; }
        public long ValueRecId { get; set; }
        public string Path { get; set; }
        public string FileName { get; set; }
        public string Description { get; set; }
        public string TypeId { get; set; }
        public DateTime CreatedDateTime { get; set; }
    }
}

I omitted to copy the IAmDiscussionRepository, but is quite intuitive.

step3: use the repository in the WebApi controller class:

        [Authorize]
        [GET("api/worker/{personnelNumber}/discussions/{discussionId}/attachments/{attachmentRecId}")]
        [HttpGet]
        public HttpResponseMessage Attachment(string personnelNumber, string discussionId, long attachmentRecId)
        {
            var attachment = _discussionRepository.GetAttachment(discussionId, personnelNumber, attachmentRecId);

            return attachment.IsNullOrEmptyObject() ?
                Request.CreateErrorResponse(HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToStringWithSpaces()) : 
                Request.CreateResponse(HttpStatusCode.OK, _attachmentMapping.Map(attachment));
        }

Note: in the webApi project I used Ninject and Attribute routing.

finally here the mapping class:

using System.Collections.Generic;
using System.IO;
using System.Linq;
using IDHGroup.DynamicsApi.CIT.Services.Interfaces;
using IDHGroup.DynamicsDataTransfer.CIT.Models.Dtos.RequestView;
using MydDynamicsIntegration.Models;

namespace IDHGroup.DynamicsApi.CIT.Services.Implementations
{
    public class AttachmentMappingService : IAmAttachmentMapping
    {
        public List<AttachmentViewDto> Map(List<Attachment> attachments)
        {
            return attachments.Select(MapAttachmentViewDto).ToList();
        }

        public AttachmentViewDto Map(Attachment attachment)
        {
            return MapAttachmentViewDto(attachment);
        }

        private static AttachmentViewDto MapAttachmentViewDto(Attachment attachment)
        {
            return new AttachmentViewDto
            {
                RecId = attachment.RecId,
                FilePath = Path.Combine(attachment.Path, attachment.FileName),
                Description = attachment.Description,
                TypeId = attachment.TypeId,
                CreatedDateTime = attachment.CreatedDateTime
            };
        }
    }
}

Wednesday, August 28, 2019

update record from c# Linq to Ax with impersonation runAs as a Active Directory Windows User

when you update a record you might want to store who made the last change

image

when you are within ax this is done automatically as long as you have set on your table the ModifyBy property to yes.

if you use Linq to Ax using .Net interop to X++ it becomes a bit more complex.

on the example you find here https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp
they connect using this code

         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

which will cause that it will always mark the modifiedBy user to the AOT user.

there is a different way to open the session in C# which is axSession.LogonAs
so you can do:

U23.Session axSession = new U23.Session();
System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
var strUserName = nc.UserName;
axSession.LogonAs(strUserName.Trim(), "yourDomain.com", nc, "fch", "en-GB", null, null);

which works perfectly fine if you have the Active Directory username and password.

If you authenticate your client web site using forms authentication and the user enters his username and password you are fine, you have them in clear (just don’t tell it to a security expert…). Otherwise if you use for example windows authentication you need to start messing up with impersonation to pass the network credentials to you the API.

here an introduction: How To: Use Impersonation and Delegation in ASP.NET 2.0

it is quite a complex subject, so I decided to use a different approach and leverage the runAs method.

here the class I created:

//This class is useful when doing a record update from Linq to Ax. otherwsie it would take the AOS user as the user making the change.
class MYD_UpdateImpersonated
{
    Common common;
    UserId userId;
}
private void new()
{
}
public static MYD_UpdateImpersonated construct()
{
    return new MYD_UpdateImpersonated();
}
protected Common parmCommon(Common _common = common)
{
    common = _common;

    return common;
}
protected UserId parmUserId(UserId _userId = userId)
{
    userId = _userId;

    return userId;
}
public RecId update()
{
    container args;
    container result;
    container recordBuf;
    TableId tableId;
    RecId recId;

    RunAsPermission permission;

    recordBuf = buf2Con(common);
    args = [common.TableId, recordBuf, common.RecId];

    permission = new RunAsPermission(userId);
    permission.assert();

    // Need use the runas method; we want to run as the user to update records
    // BP deviation documented
    result = runAs(userId, classNum(MYD_UpdateImpersonated), staticMethodStr(MYD_UpdateImpersonated, updateAsUser), args);
    CodeAccessPermission::revertAssert();

    [recId] = result;
    return recId;
}
private static container updateAsUser(container _args)
{
    DictTable dictTable, dictTableBuf;
    Common common, commonBuf;
    TableId tableId;
    container recordBuf;
    RecId recId;

    [tableId, recordBuf, recId] = _args;

    dictTable = new DictTable(tableId);
    dictTableBuf = new DictTable(tableId);

    common = dictTable.makeRecord();
    commonBuf = dictTableBuf.makeRecord();

    con2Buf(recordBuf, commonBuf);

    ttsBegin;
    common.selectForUpdate(true);
    select common where common.RecId == recId;
    buf2Buf(commonBuf, common);
    common.update();
    ttsCommit;

    return [common.RecId];
}
public static server MYD_UpdateImpersonated newFromCommon(Common _common, UserId _userId)
{
    MYD_UpdateImpersonated updateImpersonated;

    updateImpersonated = MYD_UpdateImpersonated::construct();
    updateImpersonated.parmCommon(_common);
    updateImpersonated.parmUserId(_userId);

    return updateImpersonated;
}

now in c# you can create an extension class like this:

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.AX.Framework.Linq.Data;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class GenericRecord
    {
        public static long UpdateRecord<T>(this T axTable, string windowsUser) where T : Common
        {
            var userInfo = AifPortUser::getAxaptaUser(windowsUser);// in the format  DOMAIN\USERNAME
            //if (!userInfo.isActiveDirecroyUser()) return 0;//if you prefer rather that an error is thrown from MYD_UpdateImpersonated class if the user is invalid
            var updateImpersonated = MYD_UpdateImpersonated.newFromCommon(axTable, userInfo.getUserId());
            return updateImpersonated.update();
        }
}

now we create our repository class like this:

public void UpdateDiscussion(Discussion discussion, string windowsUser)
{
    //see: https://enricoariel.blogspot.com/2019/08/proxy-classes-for-net-interop-to-x.html
    //on how to open a session inside a using statement
    using (var axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        var workerRecId = HcmWorker.findByPersonnelNumber(discussion.Worker).RecId;
        var hcmDiscussion = HcmDiscussion.findByDiscussionWorker(discussion.DiscussionId, workerRecId);

        hcmDiscussion.status = (HcmDiscussionStatus) discussion.Status.Value;
        hcmDiscussion.UpdateRecord(windowsUser);  
    }
}

finally here my discussion dto Poco classes

namespace MydDynamicsIntegration.Models
{
    public class Discussion
    {
        public string DiscussionId { get; set; }
        public AxEnumDefinition Status { get; set; }
        public string Worker { get; set; }
    }
}

namespace MydDynamicsIntegration.Models
{
    public class AxEnumDefinition
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public string Label { get; set; }
    }
}

Tuesday, August 27, 2019

Proxy Classes for .NET Interop to X++: session open inside using statement

When you start using Proxy Classes for .NET Interop to X++ you need to open and close a session and in order to adhere to the DRY principle (do not repeat yourself) I created a new c# class to use in my project.

if you look in the examples of how to use Linq to Ax (https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp) you can see in the code they have to open and close the session at the end:

using       System;  // C#
using       System.Linq;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;
using       Microsoft.Dynamics.AX.Framework.Linq.Data; // .ForUpdate() needs this.

namespace LinqProviderSample
{
   class Program  // C#, LINQ to AX.
   {
      static void Main(string[] args)
      {
         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

    //more code...

    axSession.Logoff();
      }
   }
}

but what I want is to use a using statement like this so that I do not have to worry about opening and closing the session and repeat every time the same code:

public string GetTableLabel()
{
    using (AxSessionManager axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        MyTable record = new MyTable();

        var x = new SysDictTable(record.TableId);

        return x.label();
    }
}

Here the class you need to add to the project:

using System;
using System.Net;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public class AxSessionManager : IDisposable
    {
        private readonly U23.Session _axSession = new U23.Session();

        public U23.Session Connection
        {
            get { return _axSession; }
        }

        /// <summary>
        ///     Checks to see if the AX session is connected. If it's null we return false.
        ///     Then we check to see if it's logged in, then return true. Otherwise it's not logged in.
        /// </summary>
        public bool Connected
        {
            get { return _axSession != null && _axSession.isLoggedOn(); }
        }

        public void Dispose()
        {
            CloseConnection();
        }

        /// <summary>
        ///     This connects to the AX session. If it's already connected then we don't need to connect
        ///     again, so we return true. Otherwise we'll try to initiate the session.
        /// </summary>
        /// <returns>
        ///     True: Connection openned successfully, or was already open.
        ///     False: Connection failed.
        /// </returns>
        public bool OpenConnection()
        {
            if (Connected)
            {
                return true;
            }

            try
            {
                _axSession.Logon("fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool OpenConnectionAs(NetworkCredential nc, string domain)
        {
            //System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
            var strUserName = nc.UserName;

            if (Connected)
            {
                return true;
            }

            try
            {
                //_axSession.LogonAs(strUserName.Trim(), "domain.com", nc, "fch", "en-GB", null, null);
                _axSession.LogonAs(strUserName.Trim(), domain, nc, "fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        ///     If the session is logged on we will try to close it.
        /// </summary>
        /// <returns>
        ///     True: Connection closed successfully
        ///     False: Problem closing the connection
        /// </returns>
        public bool CloseConnection()
        {
            bool retVal;
            if (Connection.isLoggedOn())
            {
                try
                {
                    _axSession.Logoff();
                    retVal = true;
                }
                catch
                {
                    retVal = false;
                }
            }
            else
            {
                retVal = true;
            }

            Connection.Dispose();
            return retVal;
        }
    }
}


as you see from the code you can either open the connection using
axSession.OpenConnection();
or use OpenConnectionAs where you need to pass the network credentials of the logged in user that match the user in Dynamics to impersonate that user in the session.