Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion XrmBedrock.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Project Path="src/Shared/NetCoreContext/NetCoreContext.csproj" />
</Folder>
<Folder Name="/src/Dataverse/">
<Project Path="src/Dataverse/AdhocJobs/AdhocJobs.csproj" />
<Project Path="src/Dataverse/ConsoleJobs/ConsoleJobs.csproj" />
<Project Path="src/Dataverse/Plugins/Plugins.csproj" />
<Project Path="src/Dataverse/PluginsNetCore/PluginsNetCore.csproj" />
<Project Path="src/Dataverse/SharedPluginLogic/SharedPluginLogic.shproj" />
Expand Down
14 changes: 0 additions & 14 deletions src/Dataverse/AdhocJobs/Jobs/ExampleJob.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<Features>FileBasedProgram</Features>
<NoWarn>$(NoWarn);CA1303</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -10,5 +10,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
</ItemGroup>

<Import Project="..\..\Shared\SharedContext\SharedContext.projitems" Label="Shared" />
<ItemGroup>
<ProjectReference Include="..\..\Shared\NetCoreContext\NetCoreContext.csproj" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions src/Dataverse/ConsoleJobs/IJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace ConsoleJobs;

public interface IJob
{
void Run(JobContext ctx);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using SharedContext.Dao;

namespace AdhocJobs;
namespace ConsoleJobs;

public record JobContext(
IOrganizationService OrgService,
DataverseAccessObject Dao,
Uri DataverseUri,
IServiceProvider Services);
IServiceProvider Services,
ILogger Logger);
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
using Microsoft.Xrm.Sdk;
using SharedContext.Dao;

namespace AdhocJobs;
namespace ConsoleJobs;

public static class JobSetup
{
#pragma warning disable S1075 // URIs should not be hardcoded
private const string DefaultUri = "https://YOUR-ENV.crm4.dynamics.com";
#pragma warning restore S1075 // URIs should not be hardcoded

public static JobContext Initialize(string[] args)
{
return Initialize(args, null);
Expand All @@ -21,7 +17,7 @@ public static JobContext Initialize(string[] args)
public static JobContext Initialize(string[] args, Action<IServiceCollection>? configureServices)
{
ArgumentNullException.ThrowIfNull(args);
var dataverseUri = args.Length > 0 ? new Uri(args[0]) : new Uri(DefaultUri);
var dataverseUri = args.Length > 0 ? new Uri(args[0]) : throw new InvalidOperationException("Dataverse Url is needed as the first argument");

var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
Expand All @@ -36,10 +32,10 @@ public static JobContext Initialize(string[] args, Action<IServiceCollection>? c

var sp = services.BuildServiceProvider();
var orgService = sp.GetRequiredService<IOrganizationService>();
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("BatchJob");
var logger = sp.GetRequiredService<ILoggerFactory>().CreateLogger("ConsoleJob");
var dao = new DataverseAccessObject(orgService, logger);

var ctx = new JobContext(orgService, dao, dataverseUri, sp);
var ctx = new JobContext(orgService, dao, dataverseUri, sp, logger);

logger.LogInformation($"Connected to {ctx.DataverseUri}");

Expand Down
66 changes: 66 additions & 0 deletions src/Dataverse/ConsoleJobs/Jobs/SolutionComponentJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Extensions.Logging;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace ConsoleJobs.Jobs;

public class SolutionComponentJob : IJob
{
public void Run(JobContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);

ctx.Logger.LogInformation("Fetching solution components modified in last 7 days...");

var sevenDaysAgo = DateTime.UtcNow.AddDays(-7);

var query = new QueryExpression("solutioncomponent")
{
ColumnSet = new ColumnSet("objectid", "componenttype", "modifiedonbehalfby", "createdonbehalfby", "modifiedon"),
Criteria = new FilterExpression
{
Conditions =
{
new ConditionExpression("modifiedon", ConditionOperator.GreaterEqual, sevenDaysAgo),
},
},
};

var results = ctx.OrgService.RetrieveMultiple(query);

ctx.Logger.LogInformation("Found {Count} solution components", results.Entities.Count);

var grouped = results.Entities
.GroupBy(e => e.GetAttributeValue<EntityReference>("modifiedonbehalfby")?.Id ?? Guid.Empty)
.ToList();

foreach (var group in grouped)
{
var firstItem = group.First();
var modifiedBy = firstItem.GetAttributeValue<EntityReference>("modifiedonbehalfby");
var createdBy = firstItem.GetAttributeValue<EntityReference>("createdonbehalfby");

var modifiedByName = modifiedBy?.Name ?? "(none)";
var createdByName = createdBy?.Name ?? "(none)";

ctx.Logger.LogInformation(
"Modified by: {ModifiedBy} | Created by: {CreatedBy} | Count: {Count}",
modifiedByName,
createdByName,
group.Count());

foreach (var component in group)
{
var objectId = component.GetAttributeValue<Guid>("objectid");
var componentType = component.GetAttributeValue<OptionSetValue>("componenttype")?.Value;
var modifiedOn = component.GetAttributeValue<DateTime>("modifiedon");

ctx.Logger.LogInformation(
" ObjectId: {ObjectId} | ComponentType: {ComponentType} | ModifiedOn: {ModifiedOn}",
objectId,
componentType,
modifiedOn);
}
}
}
}
20 changes: 20 additions & 0 deletions src/Dataverse/ConsoleJobs/Jobs/WhoAmIJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Extensions.Logging;

namespace ConsoleJobs.Jobs;

public class WhoAmIJob : IJob
{
public void Run(JobContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);

ctx.Logger.LogInformation("Executing WhoAmI request...");

var response = (WhoAmIResponse)ctx.OrgService.Execute(new WhoAmIRequest());

ctx.Logger.LogInformation("User ID: {UserId}", response.UserId);
ctx.Logger.LogInformation("Business Unit ID: {BusinessUnitId}", response.BusinessUnitId);
ctx.Logger.LogInformation("Organization ID: {OrganizationId}", response.OrganizationId);
}
}
35 changes: 35 additions & 0 deletions src/Dataverse/ConsoleJobs/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Reflection;
using ConsoleJobs;

static string GetJobName(Type t)
{
var name = t.Name;
return name.EndsWith("Job", StringComparison.Ordinal) ? name[..^3] : name;
}

var jobs = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => typeof(IJob).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
.ToDictionary(GetJobName, t => t, StringComparer.OrdinalIgnoreCase);

if (args.Length == 0 || !jobs.TryGetValue(args[0], out var jobType))
{
Console.WriteLine("Usage: ConsoleJobs <jobname> [dataverse-url]");
Console.WriteLine();
Console.WriteLine("Available jobs:");
foreach (var name in jobs.Keys.OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
{
Console.WriteLine($" {name}");
}

return 1;
}

var job = (IJob)Activator.CreateInstance(jobType)!;

var jobArgs = args.Skip(1).ToArray();
var ctx = JobSetup.Initialize(jobArgs);

job.Run(ctx);

return 0;
46 changes: 46 additions & 0 deletions src/Dataverse/ConsoleJobs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ConsoleJobs

Ad-hoc console jobs for Dataverse operations.

## Usage

```bash
# List available jobs
dotnet run --project src/Dataverse/ConsoleJobs/ConsoleJobs.csproj

# Run a job with specific Dataverse URL
dotnet run --project src/Dataverse/ConsoleJobs/ConsoleJobs.csproj -- SolutionComponent https://your-env.crm4.dynamics.com
```

## Authentication

Uses `AzureCliCredential` - authenticate via Azure CLI before running:

```bash
az login
```

## Adding a new job

1. Create a new class in the `Jobs` folder
2. Implement `IJob` interface
3. Name the class with a `Job` suffix (e.g., `MyCustomJob`)

The job will be auto-discovered and available as `MyCustom`.

```csharp
using Microsoft.Extensions.Logging;

namespace ConsoleJobs.Jobs;

public class MyCustomJob : IJob
{
public void Run(JobContext ctx)
{
ArgumentNullException.ThrowIfNull(ctx);

ctx.Logger.LogInformation("Running my custom job...");
// Use ctx.OrgService or ctx.Dao for Dataverse operations
}
}
```