Handling multiple optional parameters in Spring
Although it works, the major issue with this approach is its extreme complexity and concreteness. If a new parameter d enters the game, it will became hellish to maintain!
What can be done to improve the current implementation, without changing the approach?

there are many repetitions of .containsKey(arg) calls. They can be extracted to respective booleans like hasA, hasB -&gt; the code will be more readable.

there are many redundant casts. (String) query.get(&quot;a&quot;) and the others can be extracted into respective local var a = (String) query.get(&quot;a&quot;) -&gt; it will also become shorter and more readable.

I think that the result variable might be avoided, if myDao.get* methods already return Sets.


How the design can be improved?
For the solution below, I suppose that the interface of the myDao object cannot be changed. If it could, there might be ways to play with method overloads, attempting to apply a pattern.
The current API of myDao looks very concrete and clear. The names of the methods mention each expected parameter and the order and the number of the arguments is predictable for each case. So the solution that I suggest below is based on the calculation of the method to invoke by reflection.
The name of the method to invoke on myDao can be calculated as follows:
private String calculateDaoMethodToInvoke(boolean hasA, boolean hasB, boolean hasC) {
  var builder = new StringBuilder(&quot;getBy&quot;);
  if (hasA) {
    builder.append(&quot;A&quot;);
  }
  if (hasB) {
    builder.append(&quot;B&quot;);
  }
  if (hasC) {
    builder.append(&quot;C&quot;);
  }
  return builder.toString();
}

We can also calculate the arguments to invoke this method with:
private Object[] calculateDaoMethodArgs(String a, String b, Map&lt;String, String&gt; c) {
  return Stream.of(a, b, c)
               .filter(Objects::nonNull)
               .toArray();
}

Now, since we know the name of the target method and the parameters to use, the Java reflection features can be used to produce the results.
We can define a wrapper around the Class.getMethod call, in order to retrieve the reference to the method of myDao to be invoked:
private Method extractDaoMethod(String daoMethodName, Object[] params) {
  try {
    var paramTypes = Arrays.stream(params)
                           .map(Object::getClass)
                           .toArray(Class[]::new);
    return Dao.class.getMethod(daoMethodName, paramTypes);
  } catch (NoSuchMethodException ex) {
    throw new IllegalStateException(ex);
  }
}

And its invocation can also be wrapped:
private Collection&lt;ResultEntry&gt; getByParams(String daoMethodName, Object[] params) {
  var method = extractDaoMethod(daoMethodName, params);
  try {
    return (Collection&lt;ResultEntry&gt;) method.invoke(myDao, params);
  } catch (IllegalAccessException | InvocationTargetException ex) {
    throw new IllegalStateException(ex);
  }
}

Finally, the getByParameters method is reduced to the following:
public Set&lt;ResultEntry&gt; getByParameters(Map&lt;String, Object&gt; query) {
  var a = (String) query.get(&quot;a&quot;);
  var b = (String) query.get(&quot;b&quot;);
  var c = (Map&lt;String, String&gt;) query.get(&quot;c&quot;);

  var daoMethodName = calculateDaoMethodToInvoke(a != null, b != null, c != null);
  var daoArgs = calculateDaoMethodArgs(a, b, c);
  return new HashSet&lt;&gt;(getByParams(daoMethodName, daoArgs));
}

Please note that there are no more rigid if - else if chaining, the algo became flat.
This approach allows to easily introduce new parameters, provided that the myDao API continues to respect the same convention for its method names.
There can be further improvements. For example, the signatures of calculateDaoMethodToInvoke and calculateDaoMethodArgs can be changed in order to accept varargs (and corresponding changes in the implementation). It will make the introduction of new parameters even easier.