Out-of-box Acumatica has Twilio and Amazon SNS providers to send SMS. However, we have a business relationship with different provider (Plivo for example) and would like to utilize them for SMS service. Is it possible to use different provider?
Yes, it is possible to use different provider to send SMS.
In Acumatica, SMS service is used for
sending access code during two-factor authentication
sending business notifications
Acumatica ERP provides set of interfaces to implement SMS provider for sending SMS message.
1. PX.SmsProvider.ISmsProvider
Classes implementing this interface are automatically discovered by Acumatica ERP and are available for selection in the Provider Type box on the SMS Provider (SM203535) screen. Classes must be part of Library (DLL).
Define class implementing PX.SmsProvider.ISmsProvider interface and implement methods of ISmsProvider interface.
public class MySmsProvider : ISmsProvider
{
public IEnumerable<PXFieldState> ExportSettings
{
// Implement definition of each setting/parameter and add to settings list
get
{
return new List<PXFieldState>();
}
}
public void LoadSettings(IEnumerable<ISmsProviderSetting> settings)
{
// Retrieve value of each setting/parameter and assign to corresponding member variable
}
public async Task SendMessageAsync(SendMessageRequest request, CancellationToken cancellation)
{
// Implement logic to send SMS
}
}
2. PX.SmsProvider.ISmsProviderFactory
Class implementing constructor to initialize provider. And public properties to hold Name and Description for this provider – the way you need it to be displayed in Provider Type box on the SMS Provider (SM203535) screen.
Define class implementing PX.SmsProvider.ISmsProviderFactory interface and implement methods and properties of ISmsProviderFactory interface.
public class MySmsProviderFactory : ISmsProviderFactory
{
//Create Provider and initialize with settings
public ISmsProvider Create(IEnumerable<ISmsProviderSetting> settings)
{
var provider = new MySmsProvider();
provider.LoadSettings(settings);
return provider;
}
public ISmsProvider Create()
{
var provider = new MySmsProvider();
return provider;
}
public string Description { get; } = "My Provider";
public string Name { get; } = typeof(MySmsProvider).FullName;
}
Below example illustrates creating SMS Provider using Plivo Service.
In C# class library project, add references of PX.Common.dll, PX.Data.dll and PX.SmsProvider.Core.dll from your Acumatica Site’s bin folder.
Definition of PlivoSmsProvider class implementing PX.SmsProvider.ISmsProvider interface:
We will need Auth ID, Auth Token and From Number parameters to work with Plivo. So, we will set them up in ExportSettings method and assign them to member variables in LoadSettings method. And we will implement logic to send SMS in SendMessageAsync.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PX.Data;
namespace PX.SmsProvider.Plivo
{
public class PlivoSmsProvider : ISmsProvider
{
#region DetailIDs const
private const string AuthID_DetailID = "AUTH_ID";
private const string AuthToken_DetailID = "AUTH_TOKEN";
private const string FromPhoneNbr_DetailID = "FROM_PHONE_NBR";
#endregion
#region DetailID_Display const
private const string AuthID_DetailID_Display = "Auth ID";
private const string AuthToken_DetailID_Display = "Auth Token";
private const string FromPhoneNbr_DetailID_Display = "From Number";
#endregion
private string m_AuthID;
public string AuthID { get { return m_AuthID; } }
private string m_AuthToken;
public string AuthToken { get { return m_AuthToken; } }
private string m_FromPhoneNbr;
public string FromPhoneNbr { get { return m_FromPhoneNbr; } }
public IEnumerable<PXFieldState> ExportSettings
{
get
{
var settings = new List<PXFieldState>();
var authID = (PXStringState)PXStringState.CreateInstance(
m_AuthID,
null,
false,
AuthID_DetailID,
null,
1,
null,
null,
null,
null,
null
);
authID.DisplayName = AuthID_DetailID_Display;
settings.Add(authID);
var authToken = (PXStringState)PXStringState.CreateInstance(
m_AuthToken,
null,
false,
AuthToken_DetailID,
null,
1,
"*",
null,
null,
null,
null
);
authToken.DisplayName = AuthToken_DetailID_Display;
settings.Add(authToken);
var fromPhoneNbr = (PXStringState)PXStringState.CreateInstance(
m_FromPhoneNbr,
null,
false,
FromPhoneNbr_DetailID,
null,
1,
null,
null,
null,
null,
null
);
fromPhoneNbr.DisplayName = FromPhoneNbr_DetailID_Display;
settings.Add(fromPhoneNbr);
return settings;
}
}
public void LoadSettings(IEnumerable<ISmsProviderSetting> settings)
{
foreach (ISmsProviderSetting detail in settings)
{
switch (detail.Name.ToUpper())
{
case AuthID_DetailID: m_AuthID = detail.Value; break;
case AuthToken_DetailID: m_AuthToken = detail.Value; break;
case FromPhoneNbr_DetailID: m_FromPhoneNbr = detail.Value; break;
}
}
}
public async Task SendMessageAsync(SendMessageRequest request, CancellationToken cancellation)
{
// implement logic to send SMS
}
}
}
Definition of PlivoSmsProviderFactory class implementing PX.SmsProvider.ISmsProviderFactory interface.
using System.Collections.Generic;
namespace PX.SmsProvider.Plivo
{
public class PlivoSmsProviderFactory : ISmsProviderFactory
{
public ISmsProvider Create(IEnumerable<ISmsProviderSetting> settings)
{
var provider = new PlivoSmsProvider();
provider.LoadSettings(settings);
return provider;
}
public ISmsProvider Create()
{
var provider = new PlivoSmsProvider();
return provider;
}
public string Description { get; } = "Plivo SMS Provider";
public string Name { get; } = typeof(PlivoSmsProvider).FullName;
}
}
Once this library is published via customization, this New provider will be available in SMS Providers (SM203535) screen.
Download Acumatica Source code and Customization deployment package
Related
Scenario:
I need to load work items from two project management systems (VSTS and Jira) through their REST API. Each of them has different authorization mechanism:
VSTS: Using access token that load from database, varied by team/group of users
Jira: Using personal token that load from user claims, varied by user
Basic classes and an interface:
public interface IProjectManagementClient
{
ProjectManagementType Type { get; }
Task<WorkItem> GetWorkItemAsync(ProjectManagement projectManagement, int id, CancellationToken token = default(CancellationToken));
Task<List<WorkItem>> GetAllWorkItemsAsync(ProjectManagement projectManagement, string term, int limit = 10, CancellationToken token = default(CancellationToken));
}
public enum ProjectManagementType
{
VSTS,
Jira
}
public class ProjectManagement
{
public int Id { get; set; }
public ProjectManagementType Type { get; set; }
public string BaseAddress { get; set; }
public string AccessToken { get; set; }
}
Setup:
services.AddHttpClient<IProjectManagementClient, VstsClient>();
services.AddHttpClient<IProjectManagementClient, JiraClient>()
.AddHttpMessageHandler(provider => new BasicAuthHandler(provider.GetService<IHttpContextAccessor>(), CustomClaimTypes.TrackerToken));
Usage example:
public TimesheetsController(IEnumerable<IProjectManagementClient> projectManagementClients)
{
_projectManagementClients = projectManagementClients;
}
public void DoWork()
{
foreach (var timesheet in _timesheets)
{
var projectManagementClient = _projectManagementClients.First(o => o.Type == timesheet.ProjectManagement.Type);
timesheet.WorkItem = timesheet.WorkItemId.HasValue
? await projectManagementClient.GetWorkItemAsync(timesheet.ProjectManagement, timesheet.WorkItemId.Value, token)
: null;
}
}
Issues:
Even though I've only registered the BasicAuthHandler to the JiraClient. It has been applied to both VSTS and Jira clients.
Thus it has overridden the VSTS authorization that has been set previously in VstsClient.
Both works fine if I only setup and use only of them at a time. Please help.
JiraClient gets overriden by VstsClient because they are both registered on the same (null) name, which is default. Try to use named registrations:
services.AddHttpClient<IProjectManagementClient, VstsClient>("VSTS");
services.AddHttpClient<IProjectManagementClient, JiraClient>("JIRA")
.AddHttpMessageHandler(provider => new BasicAuthHandler(
provider.GetService<IHttpContextAccessor>(),
CustomClaimTypes.TrackerToken));
In this case IEnumerable<IProjectManagementClient> should give two items, configured as above, without override.
I have a API which connects to my dynamo db. My API has quite a few endpoints for GET, POST, Delete etc. I am using the following code:
var awsCredentials = Helper.AwsCredentials(id, password);
var awsdbClient = Helper.DbClient(awsCredentials, "us-east-2");
var awsContext = Helper.DynamoDbContext(awsdbClient);
List<ScanCondition> conditions = new List<ScanCondition>();
var response = await context.ScanAsync<MyData>(conditions).GetRemainingAsync();
return response.ToList();
The first three lines of my code ie setting awsCredentials, awsdbClient & awsContext are repeated in each of my WEB API call.
And this is my static helper class:
public static class Helper
{
public static BasicAWSCredentials AwsCredentials(string id, string password)
{
var credentials = new BasicAWSCredentials(id, password);
return credentials;
}
public static AmazonDynamoDBClient DynamoDbClient(BasicAWSCredentials credentials, RegionEndpoint region)
{
var client = new DBClient(credentials, region);
return client;
}
public static DynamoDBContext DynamoDbContext(AmazonDynamoDBClient client)
{
var context = new DynamoDBContext(client);
return context;
}
}
I use this helper class in my API to initialize AWS.
Is there a better way to initialize this?
Let's take advantage of ASP.Net's built-in Dependency Injection.
We need to make a quick interface to expose the values you need.
public interface IDynamoDbClientAccessor
{
DynamoDBContext GetContext();
}
And a settings class that we'll use in a bit.
public class DynamoDbClientAccessorSettings
{
public string Id { get; set; }
public string Password { get; set; }
public string Region { get; set; }
}
Now the concrete class.
public class DynamoDbClientAccessor : IDynamoDbClientAccessor
{
private readonly DynamoDbClientAccessorSettings settings;
public DynamoDbClientAccessor(IOptions<DynamoDbClientAccessorSettings> options)
{
settings = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public DynamoDBContext GetContext()
{
// You have the option to alter this if you don't
// want to create a new context each time.
// Have a private variable at the top of this class
// of type DynamoDBContext. If that variable is not null,
// return the value. If it is null, create a new value,
// set the variable, and return it.
var awsCredentials = Helper.AwsCredentials(settings.Id, settings.Password);
var awsdbClient = Helper.DbClient(awsCredentials, settings.Region);
var awsContext = Helper.DynamoDbContext(awsdbClient);
return awsContext;
}
}
Hook all of this up in your Startup class
services.AddSingleton<IDynamoDbClientAccessor, DynamoDbClientAccessor>();
services.Configure<DynamoDbClientAccessorSettings>(c =>
{
c.Id = "YOUR ID";
c.Password = "YOUR PASSWORD";
c.Region = "YOUR REGION";
});
Now in your controller or other DI service you ask for a IDynamoDbClientAccessor instance in the constructor.
Once you get more familar with Dependency Injection you'll be able to break apart more things into their own dependent services. As Daniel says, the AWS SDK even provides some interfaces for you to use which can help as well.
I want to change the connection to a database at runtime in a REST Api. I want to put a variable of the request and let the Api decide which connectionstring to use.
For example:
I put the variable "dbid" with the value "develop" in the request header and send it to the Api.
The Api sees the header and gets the correct connectionstring from the web.config.
I have three layers (data, business, api). The data contains EntityFramework to get and set data. Like this:
public class WebsiteContext : IocDbContext, IWebsites
{
public DbSet<Website> Websites { get; set; }
public IEnumerable<Website> GetAll()
{
return Websites.ToList();
}
}
(IoCDbContext.cs)
public class IocDbContext : DbContext, IDbContext
{
public IocDbContext() : base("develop")
{
}
public void ChangeDatabase(string connectionString)
{
Database.Connection.ConnectionString= connectionString;
}
}
In the business I have a class to retrieve data from the datalayer and do some logical stuff (not needed here, but still good for the story).
public class Websites : IWebsites
{
private readonly Data.Interfaces.IWebsites _websiteContext;
#region Constructor
public Websites(Data.Interfaces.IWebsites websiteContext)
{
_websiteContext = websiteContext;
}
#endregion
#region IWebsites implementation
public IEnumerable<Website> GetWebsites()
{
List<Data.Objects.Website> websiteDtos = _websiteContext.GetAll().ToList();
return websiteDtos.Select(web => web.ToModel()).ToList();
}
#endregion
}
public static class WebsiteMapper
{
public static Website ToModel(this Data.Objects.Website value)
{
if (value == null)
return null;
return new Website
{
Id = value.Id,
Name = value.Name
};
}
}
And, last but not least, the controller:
public class WebsiteController : ApiController
{
private readonly IWebsites _websites;
public WebsiteController(IWebsites websites)
{
_websites = websites;
}
public IEnumerable<Website> GetAll()
{
return _websites.GetWebsites().ToList();
}
}
My Unity configuration:
public static void RegisterComponents()
{
var container = new UnityContainer();
container.RegisterType<Business.Interfaces.IWebsites, Websites>();
container.RegisterType<IDbContext, IocDbContext>();
container.RegisterType<IWebsites, WebsiteContext>();
// e.g. container.RegisterType<ITestService, TestService>();
GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container);
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
So as you can see the connection string with the name "develop" is used by default. This will return a website with the name "website". Now I would change the header variable "dbid" to "live". The api should see this and should get the connectionstring that corresponds with the name "live". This last part is something I am trying, but nothing works.
This I tried:
Adding session to webapi. This means I break the stateless idea of REST api: not done
Statics cannot work either, because everyone could get the same connectionstring, but its user specific
Google, but most of the examples don't work for me
Searching StackOverflow... See previous point.
This is driving me crazy! There should be a way to change the connectionstring given by a value in a request header, right?
I have the same scenario in a multi-tenant application I created where I use a different connection string for each tenant.
It doesn't matter the implementation you choose, but you have to determine how you are going to differentiate each request per connection string. In my application, I created a custom route value, and used it in the url to differentiate each request. The important thing is to create whatever this mechanism is, and it needs to be the 1st thing you register in your DI framework, on a per request basis.
For example (using Ninject):
private static void RegisterServicdes(IKernel kernel)
{
kernel.Bind<ISiteContext>().To<SiteContext>().InRequestScope();
kernel.Bind<IDbContextFactory>().To<DbContextFactory>().InRequestScope();
// register other services...
}
Rather than your implementation of your DbContext, I would change to be this, then always create your DbContext instance via a DbContextFactory.
public class IocDbContext : DbContext, IDbContext
{
public IocDbContext(string connectionStringType) : base(connectionStringType) { }
}
Then you need to create a DbContextFactory that you use when you create your DbContext, and take the above class as a dependency. Or you can take the dependency into your services, and pass it into the DbContextFactory instead.
public interface IDbContextFactory
{
TestModel CreateContext();
}
public class DbContextFactory : IDbContextFactory
{
private string _siteType;
public DbContextFactory(ISiteContext siteContext)
{
_siteType = siteContext.Tenant;
}
public TestModel CreateContext()
{
return new TestModel(FormatConnectionStringBySiteType(_siteType));
}
// or you can use this if you pass the IMultiTenantHelper dependency into your service
public static TestModel CreateContext(string siteName)
{
return new TestModel(FormatConnectionStringBySiteType(siteName));
}
private static string FormatConnectionStringBySiteType(string siteType)
{
// format from web.config
string newConnectionString = #"data source={0};initial catalog={1};integrated security=True;MultipleActiveResultSets=True;App=EntityFramework";
if (siteType.Equals("a"))
{
return String.Format(newConnectionString, #"(LocalDb)\MSSQLLocalDB", "DbOne");
}
else
{
return String.Format(newConnectionString, #"(LocalDb)\MSSQLLocalDB", "DbTwo");
}
}
}
Then you can use it like so when accessing your DbContext:
public class DbAccess
{
private IDbContextFactory _dbContextFactory;
public DbAccess(IDbContextFactory dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public void DoWork()
{
using (IocDbContext db = _dbContextFactory.CreateContext())
{
// use EF here...
}
}
}
ISiteContext interface implementation (for using route).
public interface ISiteContext
{
string Tenant { get; }
}
public class SiteContext : ISiteContext
{
private const string _routeId = "tenantId";
private string _tenant;
public string Tenant { get { return _tenant; } }
public SiteContext()
{
_tenant = GetTenantViaRoute();
}
private string GetTenantViaRoute()
{
var routedata = HttpContext.Current.Request.RequestContext.RouteData;
// Default Routing
if (routedata.Values[_routeId] != null)
{
return routedata.Values[_routeId].ToString().ToLower();
}
// Attribute Routing
if (routedata.Values.ContainsKey("MS_SubRoutes"))
{
var msSubRoutes = routedata.Values["MS_SubRoutes"] as IEnumerable<IHttpRouteData>;
if (msSubRoutes != null && msSubRoutes.Any())
{
var subRoute = msSubRoutes.FirstOrDefault();
if (subRoute != null && subRoute.Values.ContainsKey(_routeId))
{
return (string)subRoute.Values
.Where(x => x.Key.Equals(_routeId))
.Select(x => x.Value)
.Single();
}
}
}
return string.Empty;
}
}
API action:
[Route("api/{tenantId}/Values/Get")]
[HttpGet]
public IEnumerable<string> Get()
{
_testService.DoDatabaseWork();
return new string[] { "value1", "value2" };
}
you need to create a factory class for Dynamic picking of connection string.
It is the responsibility of that class to give correct connectionString based on the certain Parameter.
I wrote a generic Extension helper method to initialize some parameters of a SoapHeader, however it is not updating the returned object.
What am I missing?
using System.Web.Services.Protocols;
public class Header: SoapHeader {}
public class WS {
public Header securityHeader {
get;
set;
}
}
public static class SecurityHeaderExtensions {
public static T GetSecurityHeader < T > (this T header, string actor, string role) where T: SoapHeader, new() {
T result = new T() {
Actor = actor, Role = role
};
Console.WriteLine("Actor: " + actor); //prints actor
Console.WriteLine("Actor: " + result.Actor); //prints blank
return result;
}
}
void Main() {
var ws = new WS();
ws.securityHeader = ws.securityHeader.GetSecurityHeader("actor", null);
}
It seems that Actor and Role are the same field internally.
Setting it to actor then setting it to null means that it's null.
The recipient of that data, known as the SOAP Role in version 1.2 of the SOAP specification and the SOAP Actor in version 1.1
Signalr Hub Client trying to call Subscribe method of Hub with object of TopicFilter which doesn't de-serialize properly in hub. The type received in Subsribe is Filter whereas i want TopicFilter object in there. It works if i manual serialize and deserialize using JsonSerializer class but i want signalr jasonSerilizer to do it as there will be lot of places if i have to do manually. Any thoughts on this?
hub = connection.CreateHubProxy("DebugDispatcherHub");
hub.JsonSerializer.TypeNameHandling = TypeNameHandling.Auto;
hub.JsonSerializer.TypeNameAssemblyFormat = FormatterAssemblyStyle.Full;
var topicfilter = new TopicFilter() { Topic = new Guid("5D5B26AD-5E6A-4B96-95C8-06540FC17E53") };
hub.Invoke("Subscribe", filter);
public class Filter { }
public class TopicFilter : Filter
{
public Guid Topic { get; set; }
}
[HubName("DebugDispatcherHub")]
public class DebugDispatcherHub : Hub
{
public void Subscribe(Filter filter)
{
//some code here
}
}
internal class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
var service = (Newtonsoft.Json.JsonSerializer)GlobalHost.DependencyResolver.GetService(typeof(Newtonsoft.Json.JsonSerializer));
service.TypeNameHandling = TypeNameHandling.Auto ;
service.TypeNameAssemblyFormat = FormatterAssemblyStyle.Full;
}
}